Commerce · REST API
Order writes
Confirm an offline payment, cancel an unpaid order, add internal notes and record shipments with tracking, without moving money.
Overview
Five endpoints let an integration work an order the way staff do on the order page, without ever moving money. They need commerce.write.orders together with commerce.read (edit permission Orders, Bestellingen, in Settings > API tokens; a token with it lives at most 30 days). Each is the MCP tool of the same name, described in Order tools, with the same data, the same change log (channel api) and the same write limit. The order's activity trail (the Activiteit tab in the admin) names the token. The order id is the internal id of GET /orders; it is in the URL and must not be in the body.
No refunds, no paid cancellations
These endpoints never move money. There is no refund and no cancellation of a paid order in this API. Internal notes and tracking codes can hold personal data: they are only returned with commerce.read.pii and are never written to the change log. See Scopes and masking.
Confirm an offline payment
/api/v1/orders/{id}/confirm-payment
Needs commerce.write.orders. Equivalent MCP tool: confirm_offline_payment.
No body. Records that the full amount of an unpaid draft order (status awaiting-payment) was received outside the payment provider, for example by bank transfer. The order is placed (status payment-offline), its stock is committed and, after the commit, the customer gets the order confirmation and the shop its notification. The order's activity trail names the token as approver.
- Result:
id,reference,fulfilment_state,status,placed_at,payment_state,changed,changes. - An order this already confirmed answers
200withchanged: falseand sends no second mail. - Any other order (paid through the provider, cancelled, held for payment review) is
409. So is a draft whose online payment is still open at the provider; staff in the admin may still confirm a bank transfer next to an abandoned online attempt, a token may not.
Cancel an unpaid order
/api/v1/orders/{id}/cancel
Needs commerce.write.orders. Equivalent MCP tool: cancel_unpaid_order.
Body: reason, one of customer_request, payment_not_received, duplicate_order, suspected_fraud or other. Cancels an unpaid draft order and releases its stock hold. This cannot be undone. The result is the same as for confirming.
- An already cancelled order answers
200withchanged: false. - A draft held for payment review (money may have arrived: settle it on the order page), a draft whose online payment is still open, and a placed or paid order are
409.
Add an internal note
/api/v1/orders/{id}/notes
Needs commerce.write.orders. Equivalent MCP tool: add_order_note.
Body: body, up to 2000 characters. Adds an internal note for staff, shown on the order's activity trail and never to the customer or in a customer mail. It is stored as the text that was sent: never interpreted as HTML (angle brackets stay as they are, every view escapes them), control characters removed, line breaks kept. Result: order (id, reference, fulfilment_state), note (id, body as stored, author, author_type token, created_at), changed (always true), changes.
- Not idempotent. Every call adds a note. Do not repeat a call that succeeded.
- Read notes back in
internal_notesofGET /orders/{reference}; that needscommerce.read.pii. - Notes may hold personal data. They are part of a data-subject access export and are deleted when the customer's data is erased. The change log keeps the note id, never the text.
curl -sS -X POST "https://<your-shop-domain>/api/v1/orders/1042/notes" \
-H "Authorization: Bearer $COMMERCE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "Customer called: deliver after 17:00."}'
Record a shipment
/api/v1/orders/{id}/shipments
Needs commerce.write.orders. Equivalent MCP tool: add_shipment.
Records a shipment with track and trace for an order that is paid in full, through the staff rules. Without lines everything still to ship goes out. The order must not be cancelled, held for review or fully refunded, and a quantity may not exceed what is left (409 otherwise). The shipment names no staff member; the order's activity trail names the token.
| Name | Type | Required | Description |
|---|---|---|---|
idempotency_key |
string | Required | 1 to 64 of A-Z a-z 0-9 . _ : -. The same key with the same carrier, tracking code and lines answers changed: false and sends no second mail. The same key with other data, or on another order, is 409. |
carrier |
string | Required | A plain carrier name. |
tracking_code |
string | Optional | A plain code. Leave it out for letter post without track and trace. |
tracking_url |
string | Optional | https only, at most 500 characters, and only together with a tracking_code. |
shipped_at |
string | Optional | ISO 8601 with an offset, not in the future, not before the order was placed. Default now. |
lines |
array | Optional | [{line_id, quantity}], physical lines of this order, each once. Omitted: everything still to ship. |
notify_customer |
boolean | Optional | Default true: the customer gets the shipping mail after the commit. false records it silently. |
curl -sS -X POST "https://<your-shop-domain>/api/v1/orders/1042/shipments" \
-H "Authorization: Bearer $COMMERCE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"idempotency_key": "agent-1042-1", "carrier": "PostNL", "tracking_code": "3SABCD1234567", "tracking_url": "https://track.example.com/3SABCD1234567"}'
Result: order, shipment (id, carrier, tracking_code, tracking_url, status, shipped_at, notify_customer, lines), changed, changes. tracking_code and tracking_url are null without commerce.read.pii, as in GET /orders/{reference}. The change log keeps a fingerprint (sha256:...) of a changed code or link, never the value.
Correct the tracking of a shipment
/api/v1/orders/{id}/shipments/{shipmentId}
Needs commerce.write.orders. Equivalent MCP tool: update_shipment_tracking.
Send at least one field. Only those fields change; status, lines and dates stay, and no mail is sent. Equal values answer 200 with changed: false. A shipment of another order is 404; a cancelled shipment, or one created through a carrier integration (the carrier owns its tracking), is 409. shipmentId is fulfilment.shipments[].id of the order.
| Name | Type | Required | Description |
|---|---|---|---|
carrier |
string | Optional | A plain carrier name. |
tracking_code |
string|null | Optional | A plain code. null removes it. |
tracking_url |
string|null | Optional | https only, at most 500 characters, only together with a tracking code. null removes it. |
Errors
| Status | error.code | When |
|---|---|---|
403 |
forbidden_scope |
The token does not have commerce.write.orders. |
404 |
not_found |
Unknown order, or a shipment that belongs to another order. |
409 |
conflict |
The order is not in a state the action allows (see the rules above); an idempotency_key already used with other data or on another order; a quantity above what is left to ship; a cancelled shipment or one made through a carrier integration on PATCH. |
422 |
invalid_argument |
An empty note, an http tracking link, a tracking link without a tracking code, a reason that is not in the list, a body field that does not exist, or no field at all on PATCH. |
429 |
too_many_requests |
The write limit. The write did not happen. |