Skip to content

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 the limit cap 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, a sales_payment_transaction row of type refund is 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's additional_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.