Correct, hold or cancel an order after it is placed
A buyer rings up with a wrong delivery address, a missing PO number, or three fewer pieces than they ordered. This guide shows what Orders lets you change on a placed order, how long you can change it, and what to do when the change you need is not one of them: the answer there is to cancel the order and place it again, and this page says what that costs.
It is for developers building a service desk tool, a storefront's "change my order" button (behind your own backend) or an ERP sync. It covers the time before anything ships. Shipping, cancelling after a shipment and returns are in Ship, cancel and return an order.
You need a tenant with the Orders app, a placed order and an API key with the scopes of the operations you call (orders.get, orders.update, orders.shippable, orders.items.cancel, orders.cancel, orders.hold, orders.unhold, orders.events.list, orders.place, and promotions.redemptions.release if you release a promotion in step 5; see which scope covers which app). Make the calls from your backend. Every call goes through the API gateway with the same two headers:
export REVENEXX_TENANT="<TENANT_SLUG>"
export REVENEXX_API_KEY="rvxk_..."
A value in angle brackets is an id from your own data. <ORDER_ID> is the order's uuid, not its order number.
Which call for which change
| The buyer wants | Call | Section |
|---|---|---|
| A different address, name, PO number, delivery date, or your own notes on the order | PUT /v1/orders/{id} | Step 1 |
| Fewer pieces, or one line gone | POST /v1/orders/{id}/items/cancel | Step 3 |
| The order stopped for a check, not cancelled | POST /v1/orders/{id}/hold | Step 4 |
| More pieces, a new line, a different price, or any change after the ERP took the order over | Cancel, then place a new order | Step 5 |
| The whole order off | POST /v1/orders/{id}/cancel | Ship, cancel and return |
1. Correct the order
PUT /v1/orders/{id} replaces whole columns on a placed order. It is for corrections, not for changing what was bought:
curl -s -X PUT "https://api.revenexx.com/v1/orders/<ORDER_ID>" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"shipping_address": {
"street": "Werkstraße 2",
"zip": "04103",
"city": "Leipzig",
"country": "DE"
}
}'
The answer is 200 with the order record as it stands now (not its lines, shipments or returns: GET /v1/orders/{id} returns those). These are the columns the call accepts, and no others:
| Columns | What they are |
|---|---|
customer_order_number, buyer, billing_address, shipping_address, user_data, metadata | The order's own copies and your notes |
requested_delivery_date | The one date a service desk moves. null withdraws the request |
external_id, external_refs, source_data, source_synced_at | Where a source system last confirmed the order |
Send only the columns you change, but send each one whole:
buyer, metadata, user_data and external_refs: send every key you want to keep.status, items, a quantity or a price next to a valid column and the call answers 200, applies the valid column and ignores the rest. If a quantity change "did not take", this is why: see step 3 and step 5. A body with only such keys is a 400.A few more rules decide what the call does with your body:
- A body with none of the eleven columns is a
400("no modifiable columns") that lists them. A body with onlysender_countryis one too: that value only controls how a corrected address is printed and is never stored (see Sender country). created_atandupdated_atare a400with the codeserver_owned_field. The attribution fields (attribution,utm_campaign,campaign_ref) are a400with the codeattribution_frozen: they are copied when the order is placed.- An order that is cancelled or completed is a
422("order is cancelled — cannot modify"). If it was also acknowledged, the answer is the "order is acknowledged" message instead, because that check runs first. A pending order, one that waits for approval, and a held order can be corrected. - A repeat is safe. The same body sets the same values again. It does add one more entry to the trail (step 4).
2. Check that the window is open
The correction window ends when the ERP acknowledges the order. Whatever books the order into the ERP calls POST /v1/orders/{id}/acknowledge with the ERP's own id, and from then on the ERP holds the copy that ships. Hand an order to the ERP builds that call.
After the acknowledgement, PUT answers 422 with "order is acknowledged — modifications now go through the fulfilling system" (the ERP). Before you offer an edit, read the order and look at acknowledged_at:
curl -s "https://api.revenexx.com/v1/orders/<ORDER_ID>" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" | jq '{status, acknowledged_at, on_hold}'
Three things follow:
- An empty
acknowledged_atmeans the window is open. The order stays correctable until it is acknowledged, cancelled or completed. - The tenant can keep the window open. With the setting
allow_modification_after_acknowledgeswitched on (off by default, as of 2026-11),PUTkeeps working on an acknowledged order. Then the ERP's copy and the order can drift apart, and telling the ERP is your job. The setting is listed with the others on Settings. - A closed window does not close everything. Cutting quantities (step 3), holding (step 4) and cancelling still work on an acknowledged order. The ERP has to hear about them: step 10 of the ERP guide passes cancellations on.
3. Cut quantities
PUT cannot touch a quantity. The way to take pieces off an order that is otherwise going ahead is the cancellation by position:
curl -s -X POST "https://api.revenexx.com/v1/orders/<ORDER_ID>/items/cancel" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "positions": [ { "order_item_id": "<ORDER_ITEM_ID>", "quantity": 4 } ], "reason": "Buyer needs fewer" }'
<ORDER_ITEM_ID> is the line's id, and the quantity must not exceed what is still open. Both are in GET /v1/orders/<ORDER_ID>/shippable (scope orders.shippable):
{
"shippable": true,
"positions": [
{ "order_item_id": "5b0c…", "sku": "ACME-4711-BLK", "quantity": 6, "quantity_shipped": 0, "quantity_cancelled": 0, "quantity_open": 6 }
]
}
positionsis required. A call without it cannot cancel the whole order by accident. A quantity above what is still open (ordered minus shipped minus cancelled) is a422.- The order keeps its status while anything is open. The line's
quantitystays what was ordered, andquantity_cancelledgrows. Read both, notquantityalone. - If every line ends up fully cancelled, the order becomes
cancelled. That firesorder.item_cancelledfor each line and noorder.cancelled. - It answers
422with the codeorder_pendingon an order that waits for approval. Cancel that order whole instead. - A tenant can require a reason (
cancel_requires_reason, a400without one), and can refuse cancellations on a held order (step 4). - Cancelling books nothing else. Stock, payment and a promotion are untouched: Ship, cancel and return an order names the follow-up calls.
Only a reduction is possible. To raise a quantity or add a line, go to step 5.
4. Hold an order and read the trail
A hold puts a flag with a reason on an order while someone checks it, without cancelling it or changing its status. By default it stops shipments only (see below):
curl -s -X POST "https://api.revenexx.com/v1/orders/<ORDER_ID>/hold" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Credit check pending" }'
curl -s -X POST "https://api.revenexx.com/v1/orders/<ORDER_ID>/unhold" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
- The order shows
on_hold: trueand thehold_reason. Send a reason: a shipment that is refused quotes it back, and a hold without one is a hold nobody can resolve. - What a hold blocks is the tenant's
on_hold_blockssetting. The default,shipping, refuses shipments only.shipping_and_cancelrefusescancelanditems/canceltoo, with a422that names the reason.nothingmakes the flag advisory. - A hold never blocks
PUT. Correcting an address on a held order is allowed. - Holding twice replaces the reason. Releasing an order that is not on hold is a
422("order is not on hold"), so do not callunholdin a blind loop. The call takes{}, clears the reason, and restores nothing else. - A cancelled or completed order cannot be held (
422).
Each correction, cancellation and hold writes a row on the order's trail, which is a history of what happened, not an audit log: it keeps no old values and no user. Read it with GET /v1/orders/{id}/events: oldest first, paginated, and each parameter is an exact match on its column:
curl -s "https://api.revenexx.com/v1/orders/<ORDER_ID>/events?name=order.updated" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" | jq .
The names to look for are order.updated, order.held, order.unheld, order.item.cancelled, order.acknowledged and order.cancelled. Three limits matter for a service desk:
- A correction row names the columns, not the values. The payload of
order.updatedis{ "columns": ["shipping_address"] }. The old address is not kept anywhere on the trail. If you need before and after, write it down yourself, for example in an order comment, before you send thePUT. - Nobody is named. The
actorcolumn is filled only onorder.placedandorder.requested, and is empty on every row the calls on this page write, including theorder.cancelledrow. - A correction fires no named event.
order.held,order.unheldandorder.item_cancelledare events a workflow can subscribe to by topic.order.updatedis on the trail and on the raworder_event.createdfeed only (as of 2026-11). Do not build an ERP update on a topic calledorder.updated. See Events and comments.
5. Cancel and place again
Some changes have no call: more pieces, a new line, a different product, a different price, or any change once the ERP has the order and the window is shut. For those, call the order off and place a new one.
- Cancel the old order with
POST /v1/orders/{id}/canceland areason. It only works while nothing has shipped (a422otherwise). Once anything has shipped, cut the open quantities with step 3 and treat the shipped part as a return: Ship, cancel and return an order. - Place the corrected order with
POST /v1/orders/place: the same buyer, addresses and payment as the old order, with the changed lines.itemsis the only required field; Placing an order has the full body.
curl -s -X POST "https://api.revenexx.com/v1/orders/place" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customer_order_number": "PO-2026-0815",
"items": [
{ "sku": "ACME-4711-BLK", "name": "Cordless drill, black", "quantity": 10, "unit_price": 19.90, "tax_rate": 19 }
],
"shipping_address": { "street": "Werkstraße 2", "zip": "04103", "city": "Leipzig", "country": "DE" },
"metadata": { "replaces_order": "<OLD_ORDER_NUMBER>" }
}'
- Put a pointer to the old order in the new one. Orders has no field that links the two. The old number in the new order's
metadatais the simplest way to keep the link.
What re-placing costs, so you can decide whether a PUT and a phone call is cheaper:
- A new order number. The old number is not reused and the old order stays on record as
cancelled: there is no delete (DELETE /v1/orders/{id}answers405). The buyer sees two numbers for one purchase, so tell them which one counts. - A second
order.placedevent. Whatever reacts to placements reacts again. The ERP workflow in Hand an order to the ERP books the new order as a new order, because its duplicate check is by order number. The old booking is cancelled by theorder.cancelledevent, if the ERP side handles it (its step 10), so check that it does before you rely on re-placing. - Nothing moves for you. Stock, payment and promotions of the old order stay where they are until you release, cancel or refund them, and the new order needs its own reservation and payment. A promotion that was spent on the old order is released with
POST /v1/promotions/release; see the cancel step of Ship, cancel and return an order. - Who may cancel is limited. Cancelling needs the
orders.cancelscope, and a tenant can require a reason (cancel_requires_reason) or block cancelling while an order is held (on_hold_blocks). Two refusals are separate. A buyer's own cancel is refused once the merchant has taken the order over or part of it has shipped (422, codeorder_not_cancellable), and only your back office can cancel then. A whole-order cancel is refused for every caller once any position has shipped (422, "order has shipped positions"); cut the open quantities withitems/cancelinstead.
Troubleshooting
PUTanswers422"order is acknowledged". The window is closed. Change it in the ERP, ask the tenant to allow modification after acknowledgement, or cancel and place again (step 5).PUTanswers200and the quantity did not change. Quantities,statusanditemsare not columns: they are dropped without an error. Use step 3 to reduce, step 5 to raise.PUTanswers400and lists the allowed columns. The body had none of the eleven. Check the spelling, and that you did not send onlysender_country.PUTanswers422"order is cancelled" or "order is completed". A final order cannot be corrected. An acknowledged one answers "order is acknowledged" first.items/cancelanswers422order_pending. The order still waits for approval. Cancel it whole, or approve it first.- A shipment, or on some tenants a cancel, answers
422"order is on hold". Readhold_reason, then callunhold. A cancel is refused only when the tenant'son_hold_blocksisshipping_and_cancel; with the defaultshipping, only the shipment is. unholdanswers422"order is not on hold". Someone already released it. Read the order first.401or403. The tenant slug or key is wrong, or the key lacks the scope for that operation. See API keys and Errors.
Where to go next
Start with Ship, cancel and return an order: it is what comes after the order ships, and the stock, promotion and money steps that cancelling skips.
- Orders — the correction window, hold and cancel, route by route.
- Hand an order to the ERP — the acknowledgement that closes the window, and passing cancellations on.
- Events and comments — the trail and the comments an operator reads.
- Configure order and quote numbers per market — what an order number promises.