RMA
Hyvä POS is in closed beta
Hyvä POS is currently in a closed beta (pilot phase) with a small group of merchants. It is not yet generally available: the App Store release follows the pilot, and features and configuration may still change - possibly in backwards-incompatible ways - before the general release. Want to take part? Sign up at hyva.io/pos.
| Method | Route | Purpose |
|---|---|---|
GET |
/V1/pos/rma/order/:orderId |
Existing returns for an order |
GET |
/V1/pos/rma/:rmaId |
A single return |
GET |
/V1/pos/rma/open |
Open returns across all orders (the pending-returns inbox) |
POST |
/V1/pos/rma |
Create a return; optionally auto-approve and receive in one call |
PUT |
/V1/pos/rma/:rmaId/status |
Transition a return's status |
POST |
/V1/pos/rma/:rmaId/comment |
Add a comment |
POST |
/V1/pos/rma/:rmaId/receive |
Staged flow: record physical receipt and inventory routing |
POST |
/V1/pos/rma/:rmaId/resolve |
Staged flow: record per-line resolutions |
POST |
/V1/pos/rma/:rmaId/settle |
Settle an awaiting_refund return: create the credit memo, close |
POST |
/V1/pos/rma/process |
Collapsed flow: create RMA + credit memo + resolution in one call |
POST |
/V1/pos/rma/:rmaId/process |
Fast-track an existing (e.g. online-submitted) return in store |
All routes require the Hyva_Pos::locations ACL resource - there is no separate approve ACL; cashier authority is enforced by the POS staff-role check on the device. The service delegates to the single configured returns provider (a bridge module implementing RmaProviderInterface, such as the Adobe Commerce RMA bridge). POST /V1/pos/rma/process also works on stores without any RMA module: it skips RMA creation and creates the credit memo directly.
Status Vocabulary
Every bridge reports status using the canonical vocabulary from Hyva\Pos\Api\RmaStatusVocabulary; the vendor's own human-readable name goes in status_label.
- Open (in flight, needs attention):
pending,authorized,partially_authorized,received,received_partially,approved,awaiting_refund - Terminal (settled end state):
resolved,closed,processed_closed,rejected,denied
Terminal states are sticky: a provider never moves a return out of one. That is what makes settle retries idempotent - settling an already-closed return is a no-op, not an error.
The Return Shape
Every route that returns a return uses this shape:
{
"id": 18,
"increment_id": "000000018",
"order_id": 2043,
"order_increment_id": "000002043",
"customer_id": 1287,
"status": "awaiting_refund",
"status_label": "Awaiting Refund",
"items": [
{
"entity_id": 31,
"order_item_id": 5120,
"product_name": "Strive Shoulder Pack",
"product_sku": "24-MB04",
"qty_requested": 2,
"qty_authorized": 2,
"qty_approved": 2,
"qty_returned": 2,
"reason": "Wrong Size/Color",
"condition": "Unopened",
"resolution": "Refund",
"status": "authorized"
}
],
"comments": [],
"created_at": "2026-08-12 11:02:44",
"updated_at": "2026-08-12 11:02:47",
"refund_method": "cash",
"credit_memo_id": null,
"credit_memo_increment_id": null
}
refund_method is the settlement channel (original_payment, cash, terminal, exchange, store_credit), derived server-side; null when unknown. credit_memo_id / credit_memo_increment_id are set once the return has been settled with a memo.
Read Routes
GET /V1/pos/rma/order/:orderId- array of returns for one order. The POS order-detail view uses it to show return history and block double returns.GET /V1/pos/rma/:rmaId- a single return.GET /V1/pos/rma/open?limit=200- returns in non-terminal statuses across all orders, one request. Powers the POS pending-returns inbox so a cashier can complete an online-initiated return without hunting for the source order. When thelimitcap is hit, offer "search by order number" for older returns.
Create a Return
POST /V1/pos/rma - creates a return. This is the endpoint behind the POS walk-in return wizard.
{
"orderId": 2043,
"customerId": 1287,
"items": "[{\"order_item_id\":5120,\"qty_requested\":2,\"reason\":\"Wrong Size/Color\",\"condition\":\"Unopened\",\"resolution\":\"Refund\",\"inspected_condition\":\"pristine\",\"return_to_stock\":true}]",
"comment": "Walk-in return",
"autoApprove": true,
"locationId": 3,
"refundMethod": "cash"
}
items is a JSON-encoded string, not a nested array - the service contract declares it as a string. Each row carries:
| Field | Purpose |
|---|---|
order_item_id |
The order item being returned |
qty_requested |
Quantity the customer is returning (drives the refund) |
reason |
Return reason, free vocabulary (the POS sends e.g. Wrong Item, Defective, Wrong Size/Color) |
condition |
Declared condition (Unopened, Opened, Damaged) |
resolution |
Per-line resolution label (Refund, Exchange, Store Credit) |
inspected_condition |
Inspection verdict: pristine, damaged, or dispose. Ignored unless autoApprove is true |
return_to_stock |
Whether the line restocks; defaults to true when absent |
qty_received |
Optional partial receive - see the quantity contract below. Capped at qty_requested |
target_source_code |
Optional explicit MSI destination source, overriding the condition-based routing |
With autoApprove: false (the default) the return is created in pending and the back office takes it from there. With autoApprove: true the server flips it through requested → approved → items_received in one round-trip using the inspection metadata on each row: pristine lines restock to their origin source, damaged lines route to the location's configured damaged-stock source, dispose lines are written off. locationId is required in this mode because damaged-stock routing reads the location's damaged_stock_source_code.
refundMethod (honored only with autoApprove: true) decides what happens next: original_payment settles synchronously through the original gateway and closes the return in the same call; cash, terminal, exchange, and store_credit leave it in awaiting_refund for the cashier to finish at the register.
Response: the created return in the shape above. The auto-approve fast path is best effort - if the approve or receive step fails, the return is already persisted and the error surfaces to the caller so the client can show a partial-success warning; a manager finishes it from admin.
The Quantity Contract
The refund follows the requested/approved quantity, not the received one. A cashier receiving 3 of 5 items as restockable still refunds all 5 - only the restock drops to the received count. qty_received feeds inventory routing, never the credit memo. Do not build integrations that expect a partial receive to shrink the refund.
Status and Comments
PUT /V1/pos/rma/:rmaId/status - body { "status": "approved", "comment": "Approved at the register" } (comment optional). Returns the updated return. Use vocabulary codes for status.
POST /V1/pos/rma/:rmaId/comment - body { "comment": "Customer will pick a replacement tomorrow", "notifyCustomer": false }. Returns true.
Staged Flow: Receive and Resolve
For back-office style processing the lifecycle can be walked step by step instead of collapsed into create.
POST /V1/pos/rma/:rmaId/receive - records physical receipt and routes inventory; transitions approved → items_received. itemsJson rows are keyed by rma_item_id (the entity_id from the return's items, not the order item id):
{
"itemsJson": "[{\"rma_item_id\":31,\"condition\":\"pristine\",\"qty_received\":2,\"return_to_stock\":true}]",
"locationId": 3
}
POST /V1/pos/rma/:rmaId/resolve - records each line's resolution (refund, store_credit, exchange, repair, dispose) and triggers the downstream action; transitions to resolved once every line carries one:
{
"resolutionsJson": "[{\"rma_item_id\":31,\"resolution_mode\":\"refund\",\"refund_method\":\"cash\"}]"
}
Settle a Refund
POST /V1/pos/rma/:rmaId/settle - finishes an awaiting_refund return once the cashier has actually handed money back, reversed on the card terminal, or closed the exchange sale. Creates the credit memo (offline for cash / terminal / exchange / store_credit, online for a resumed original_payment) and closes the return.
{
"method": "terminal",
"reference": "re_4qqhO89gsT",
"rawPayload": "{\"resource\":\"refund\",\"id\":\"re_4qqhO89gsT\"}",
"adjustmentNegative": 5.0
}
reference(optional): PSP refund id or linked exchange-order increment. When present and the memo creates successfully, asales_payment_transactionrow of typerefundis written so the refund shows in Magento's Transactions tab and accounting exports. Omit for cash.rawPayload(optional): raw PSP response stored on the transaction'sadditional_information.adjustmentPositive/adjustmentNegative(optional, ex-tax): goodwill credit on top of, or a restocking fee subtracted from, the items refund - stamped on the memo's adjustment fields.shippingAmount(optional): explicit refunded shipping; omitted or zero lets Magento distribute per its memo defaults.
Response: the settled return, now carrying credit_memo_id and credit_memo_increment_id. The credit memo quantities follow the quantity contract above. Idempotent: settling an already-closed return returns it unchanged - safe to retry after a timeout.
Collapsed Processing
POST /V1/pos/rma/process - create RMA (when a returns module is available), create the credit memo, and handle the resolution in one call. Body: { "orderId": 2043, "customerId": 1287, "items": "[{\"order_item_id\":5120,\"qty\":2,\"reason\":\"Defective\",\"condition\":\"Opened\",\"resolution\":\"Refund\"}]", "resolution": "refund", "isOnline": true, "comment": null }. resolution is the primary resolution (refund, store_credit, or exchange); isOnline: true refunds through the payment gateway, false creates an offline memo.
POST /V1/pos/rma/:rmaId/process - the same fast-track for an existing return (for example one submitted online). Body: { "resolution": "refund", "isOnline": true }.
Both respond with a process result rather than a return:
{
"success": true,
"error": null,
"rma_id": 18,
"rma_increment_id": "000000018",
"credit_memo_id": 77,
"credit_memo_increment_id": "000000077",
"refund_total": 118.9,
"store_credit_issued": 0.0,
"exchange_credit_amount": 0.0,
"is_online_refund": true,
"payment_method": "mollie_methods_ideal",
"transaction_id": "tr_WDqYK6vllg"
}
Business failures return 200 with success: false and error set. rma_id is null on stores without a returns module.
Error Behavior
Standard Magento web API envelope: 401 for a bad token, 404 (NoSuchEntityException) for unknown return or order ids, 400 for invalid payloads and provider-rejected transitions (LocalizedException). Routes that report a success flag (/process) put business failures in the body instead.