Sales Returns API
Create a sales return line by line, complete it with or without a refund, and list returns. Includes the refund breakdown, limits and side effects.
A sales return is built in two steps, matching the two steps of the Sales Return screen in the app:
- Step 1 – add lines. Each Return action on a line calls
POST /order/sales-return. The server keeps one draft return per order and adds the line to it. - Step 2 – complete. Complete Sales Return calls
POST /order/complete-return-orderwith the refund choice (Has Payment, method, amount, Delivery Charge Refundable).
All paths are relative to {{server}}. Send X-Tenant: <your-tenant-key>, Authorization: Bearer <access-token> and Content-Type: application/json.
Endpoint summary
Method | Path | Used by |
|---|---|---|
GET |
| Sales Return step 1 (items with returned qty so far) |
POST |
| Return on one line (return dialog) |
POST |
| Complete Sales Return (step 2) |
GET |
| Sales > Sales Return list (Sales Return Orders) |
POST |
| Cancel Order with refund (Sales list) |
The demo return
Order ORD-004851 (receipt 2-11-261027-0003, total 658.00) had 2 × classic chicken bucket at 220.00 and 1 × crispy chicken bucket at 218.00. The customer returned 1 × classic chicken bucket with reason "Customer changed mind", Delivery Charge Refundable off, Has Payment on, CASH refund 220.00. The return then showed in Sales Return Orders as 220.00 Completed.
GET /api/v1/order/list-return-order-items
Lists every line of an order with what has already been returned.
Name | Required | Notes |
|---|---|---|
| yes | Server order number ( |
| no | Limit to the active store |
| no | Sent by the app |
{
"status": "success",
"message": "Return Order Items",
"data": [
{
"order_id": 7037,
"order_number": "ORD-004851",
"receipt_number": "2-11-261027-0003",
"client_sale_id": "<uuid-v7>",
"cart_item_id": 15120,
"product_id": 15490,
"product_variant_id": null,
"product_name": "classic chicken bucket",
"quantity": 2,
"unit_price": 220,
"total_price": 440,
"returned_quantity": 0,
"remaining_quantity": 2,
"returned_total": 0,
"is_returned": false,
"return_order_id": null,
"variant_attributes": null
}
]
}*Illustrative ids; field list from the server code.* Use the returned order_id and cart_item_id (not the receipt number) when you create the return. Unknown order → 422 "Order or Cart not found".
POST /api/v1/order/sales-return
Adds one line to the order's draft return.
Field | Required | Notes |
|---|---|---|
| yes | Server order id |
| yes | Line id from |
| yes | ≥ 0.01, not more than sold − already returned |
| yes | Unit return price, ≥ 0.01, not more than the original unit price |
| no | Free text (the dialog's Reason) |
| no | Used only to compute the preview breakdown |
{
"order_id": 7037,
"price": 220.0,
"quantity": 1,
"cart_item_id": 15120,
"reason": "Customer changed mind",
"is_delivery_refundable": false
}Response
data is the draft return (status: false) plus a refund_breakdown. The app needs data.id — it is the return_order_id for step 2.
{
"status": "success",
"message": "Item Returned Successfully",
"data": {
"id": 512,
"order_id": 7037,
"user_id": 1497,
"total_amount": 220,
"status": false,
"refund_breakdown": {
"order_items_total": 658.0,
"returned_items_total": 220.0,
"pro_rata_discount": 0.0,
"delivery_refund": 0.0,
"net_refund": 220.0,
"maximum_cash_refund": 220.0,
"refund_available": true
}
}
}*Illustrative ids; amounts follow the demo return.*
Rules
- The line must belong to the order (
"The cart item does not belong to this order."). - Quantity above what is left →
"Return quantity exceeds available quantity." - Price above the sold unit price →
"Return price cannot exceed the original unit price." - Returning the same line again in the same draft adds to its quantity and total.
- Stock is not changed here. Stock is restored only when the return is completed.
- Errors come back as 422
{"status":"failed","message":"Error Returning Item","data":"<reason>"}.
POST /api/v1/order/complete-return-order
Finalises the draft return.
Field | Required | Notes |
|---|---|---|
| yes |
|
| yes |
|
| if |
|
| if | Refund amount |
| no | Add the order's delivery charge to the refund |
{
"return_order_id": 512,
"is_delivery_refundable": false,
"payment_method": "CASH",
"paid_amount": 220.0,
"has_payment": true
}Without a refund:
{ "return_order_id": 512, "is_delivery_refundable": false, "has_payment": false }Response
{
"status": "success",
"message": "Return Order Completed",
"data": {
"id": 512,
"order_id": 7037,
"total_amount": 220,
"status": true,
"refund_breakdown": {
"order_items_total": 658.0,
"returned_items_total": 220.0,
"pro_rata_discount": 0.0,
"delivery_refund": 0.0,
"net_refund": 220.0,
"maximum_cash_refund": 220.0,
"refund_available": true
}
}
}*Illustrative ids.*
How the refund is calculated
Field | Meaning |
|---|---|
| Sum of all line totals of the order (tax-inclusive) |
| Sum of returned quantity × return price |
| Share of the order discount: returned total ÷ order items total × order discount |
| The order's delivery charge when |
| returned total − pro-rata discount + delivery refund (never below 0) |
|
|
|
|
PRICE_ROUND_OFF is not applied to refunds.
Rules and side effects
- A return can be completed once (
"Return Order already completed") and must have items ("No items found in this return order."). - Customer debt first. If the customer owes the store, the return is first set off against that balance. If nothing is left: 422 "No refund is available: the customer has an outstanding balance that exceeds this return, so the return is adjusted against that balance." with
data.maximum_cash_refund = 0. - A refund larger than the customer's credit → 422 "Refund cannot exceed the customer credit balance." with the allowed
maximum_cash_refund. - A refund above the maximum → 422 "Paid amount cannot exceed the maximum cash refund (…)."
- On success the server:
- restores stock in base units to the right store and variant,
- reverses the cost of goods sold (a
COGScredit line "Reverse COGS for Sales Return #…"), - writes the sales-return ledger lines for the customer,
- when
has_paymentis true, creates a customer voucher of typesales_return(statuspaid) linked to the original invoice, with one line per returned item and a "Delivery Charge" line when refundable, - updates the order status after the return.
- The app then prints a Return Bill, Credit Note or Sales and Return Bill, depending on Printer Settings.
RETURN_WINDOW_DAYS(company prop, default 7) sets how long packing photos and video are kept after delivery. The return endpoints do not reject returns by age.
GET /api/v1/order/list-return-orders
Used by: Sales > Sales Return (Sales Return Orders list). 10 per page, newest first.
Name | Type | Notes |
|---|---|---|
| integer | One customer |
| string | Order number or offline receipt number |
| integer | Active store |
| integer | Page |
{
"status": "success",
"message": "Order Returns Listed Successfully",
"data": {
"current_page": 1,
"data": [
{
"id": 512,
"order_id": 7037,
"total_amount": "220.000",
"status": true,
"items": [
{ "cart_item_id": 15120, "quantity": "1.000", "price": "220.000", "reason": "Customer changed mind", "cart_item": { "product": { "name": "classic chicken bucket" } } }
],
"order": {
"order_number": "ORD-004851",
"payment_method": ["CASH", "CARD"],
"payment_id": ["<cash-id>", "<card-id>"],
"customer": { "id": 2767, "user": { "name": "Layla Al-Harbi" } }
}
}
],
"per_page": 10,
"total": 1
}
}*Illustrative row (the captured response for the test filter was empty). status: true shows as Completed, false as Pending. Payment method ids are converted to names, and the original ids are kept in payment_id.*
POST /api/v1/order/cancel-order
Cancels a whole order, optionally with a refund. Use it instead of a return when nothing has shipped.
Field | Required | Notes |
|---|---|---|
| yes | Server order id |
| no | Up to 500 characters |
| no |
|
| no | ≥ 0 |
| no | Refund the delivery charge too |
{ "order_id": 7037, "refund_method": "<cash-id>", "refund_amount": 658.0, "delivery_charge_refundable": false }Shipped or delivered orders, and orders that already have a processed sales return, cannot be cancelled — create a sales return instead. See Cart and orders API for order statuses.