Order cancellation, returns and refunds
This page covers what happens when an order goes wrong: a cancellation, a return or a refund. Cancelling on the customer’s behalf has a current route. Only a v1 route exists for the other tasks, so v1 is the path for them.
What you’ll build
Section titled “What you’ll build”Four pieces of your order flow:
- a call that tells Zentail when a customer cancels on your channel,
- a check on your order poll that picks up cancellations the seller made,
- a call that tells Zentail when a buyer asks to return items on your channel,
- a poll that reads back the seller’s decision on each return, and any refund.
Before you start
Section titled “Before you start”- You hold the seller’s access token. See Authentication.
- You have injected orders into Zentail and you poll for shipped orders.
1. Cancel when the customer cancels
Section titled “1. Cancel when the customer cancels”When a customer cancels on your channel, tell Zentail with
POST /v2/storefront/orders/cancel.
We call this a customer-initiated cancellation.
{ "channelOrderId": "113-4567890-1234567", "cancelQuantities": { "88001": "1" }}channelOrderIdis your channel’s id for the order, the one you sent when you injected it. The seller is taken from your token.cancelQuantitiesmaps each ZentaillineItemIdto the number of units to cancel. Leave out any line you are not cancelling. Each quantity is a JSON string, because this route sends every 64-bit integer as one.
A success returns an empty object. Zentail puts the cancelled units back into stock at the warehouses the line was routed to, so you don’t send a restock list. You don’t send a reason either.
An error comes back as a google.rpc.Status body with code, message and details.
An order that doesn’t match channelOrderId returns NOT_FOUND.
2. Pick up cancellations the seller made
Section titled “2. Pick up cancellations the seller made”Sometimes the seller cannot fulfil an order and cancels it in Zentail. We call this a
merchant-initiated cancellation. Only a v1 route exists for this. The shipped-orders poll,
GET /v2/storefront/orders/shipped, returns only shipped orders, and its order view has no
cancel quantities.
So poll
GET /v1/salesOrder
with lastUpdatedTs and status=CANCELLED. On each order, read cancelQuantity on each
line item in products to see which part of the order was cancelled. The order’s
cancellationReason says why.
3. Report a return the buyer requested
Section titled “3. Report a return the buyer requested”When a buyer asks to return items on your channel, tell Zentail with
POST /v1/salesOrder/{orderNumber}/returnOrder,
with the Zentail order number in the path. Zentail creates a return order, which the seller
then works in Zentail. Only a v1 route exists for this.
Your token needs the sales:orders:self scope. This route admits no other, so a token
that can inject and poll orders may still get a 403 here. Scopes are fixed when you
register your application. See Scopes.
{ "channelReturnId": "RMA-20261020-0042", "requestTs": "2026-10-20T14:05:00Z", "returnItems": [ { "lineItemId": "88001", "quantity": 1, "refundReason": "Item does not meet expectations" } ]}channelReturnIdis your channel’s id for the return. Zentail keys the return order on it, so sending the same return again changes nothing. Retry freely.returnItemsnames each line being returned, by thelineItemIdyou sent when you injected the order, and thequantity. It is required to create a return order, and ignored once the return order exists.requestTsis when the buyer asked. It defaults to now.
When your channel cancels or rejects the return, send the same channelReturnId with
cancelled: true. Zentail cancels the return order if it is still open. A return order
the seller already completed or cancelled is left as it is, because that decision is the
seller’s. The call still succeeds in that case and returns the return order as it stands,
so read cancelled and completedTs from the response.
A success returns the return order. Zentail checks every line before it writes anything, so one bad line gets a 400 and nothing is created. A line is bad when:
- its
lineItemIdis unknown, or appears twice in the request, - its status is not
SHIPPEDorRETURN_REQUESTED. A line that is already partly or wholly returned or refunded can’t take another return, - its
quantityis below 1, or more than can still be returned.
A create with no returnItems also gets a 400. A channelReturnId already used on
another order gets a 409. An order number Zentail doesn’t have for your integration gets
a 404. A cancel also gets a 404 when this order has no return with that channelReturnId,
including one you reported on a different order. v1 errors use their own body; see
Errors.
4. Read the seller’s decision, and any refund
Section titled “4. Read the seller’s decision, and any refund”Zentail publishes each return order, and the seller’s decision on it, on
GET /v1/salesOrder.
Only a v1 route exists for this. Poll it with lastUpdatedTs and no status filter.
Step 2’s status=CANCELLED poll never returns an order with a return on it. A rejected
return also puts its lines back to SHIPPED, so filtering on the return statuses would
miss it.
Some decisions don’t move the order’s lastUpdatedTs. When a line has more than one open
return and the seller rejects one, no line changes, so the poll doesn’t send the order
again. The poll also leaves out orders placed more than a year ago. So also re-read each
order that still has an open return. Pass its channel order id in channelOrderIds.
Leave out lastUpdatedTs, so that neither limit applies.
There is no route for issuing a refund or sending a replacement. The seller starts those
on your channel. Once the seller records the refund in Zentail, it shows on this poll.
Use it to reconcile against a refund your channel already issued, not as a request to
refund again. The order’s accounting block, which carries the refund total, comes back
only when your token also holds sales:orders or sales.
On the order:
| Field | What it carries |
|---|---|
status |
RETURN_REQUESTED, PARTIALLY_RETURNED, RETURNED, PARTIALLY_REFUNDED or REFUNDED, alongside the statuses you already handle |
returnOrders |
One entry per return on the order, each with channelReturnId, requestTs, completedTs, a cancelled flag and its returnItems |
accounting.refund |
The total refunded, if the order was partly or wholly refunded |
Don’t rely on the order’s status alone. On an order with more than one line, it is
rolled up from the lines’ statuses, so it doesn’t say which lines a return or refund
covers. One returned line can make the whole order read RETURNED, and a partial return
can leave it at PARTIALLY_SHIPPED. Read each line’s status and the order’s
returnOrders instead.
Not every entry is a return you reported. A seller can also open a return in Zentail, and
that entry has a null channelReturnId. You can’t cancel it through step 3, but you read
its decision the same way.
Read the seller’s decision off each entry in returnOrders:
completedTsset, and not cancelled, means the seller accepted the return and completed it in Zentail. It is null while the return is still open.cancelled: truewith nocompletedTsmeans the return was cancelled before anyone completed it. Either the seller rejected it, or your owncancelled: truefrom step 3 landed.- Both set means the seller cancelled the return after completing it. Zentail puts
each of its lines back to
SHIPPED, unless another return that isn’t cancelled still covers the line. The units still count as returned, so they can’t be returned again. A refund already issued still stands. Treat it as a correction to raise with the seller, not as a rejection. - Neither means the seller has not decided yet.
On each line item in products:
| Field | What it carries |
|---|---|
status |
the same status values, per line item |
refund |
the amount refunded for that line item |
refundReason |
why |
On each entry in a return’s returnItems: the SKU, the quantity being returned
against the originalQuantity ordered, and the refundAmount.
See also
Section titled “See also”API changelog · Built f1d143e6