Skip to content

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.

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.

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"
}
}
  • channelOrderId is your channel’s id for the order, the one you sent when you injected it. The seller is taken from your token.
  • cancelQuantities maps each Zentail lineItemId to 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.

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.

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"
}
]
}
  • channelReturnId is your channel’s id for the return. Zentail keys the return order on it, so sending the same return again changes nothing. Retry freely.
  • returnItems names each line being returned, by the lineItemId you sent when you injected the order, and the quantity. It is required to create a return order, and ignored once the return order exists.
  • requestTs is 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 lineItemId is unknown, or appears twice in the request,
  • its status is not SHIPPED or RETURN_REQUESTED. A line that is already partly or wholly returned or refunded can’t take another return,
  • its quantity is 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:

  • completedTs set, and not cancelled, means the seller accepted the return and completed it in Zentail. It is null while the return is still open.
  • cancelled: true with no completedTs means the return was cancelled before anyone completed it. Either the seller rejected it, or your own cancelled: true from 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.

API changelog · Built f1d143e6