# Order tools

Confirm an offline payment, cancel an unpaid order, add internal notes and record shipments with tracking, without moving money.

Section: Commerce · MCP
Canonical URL: https://docs.omniretailconnect.nl/commerce/mcp/order-tools

## Overview

Five tools let a client work an order the way staff do on the order page, without ever moving money. They need `commerce.write.orders` (with `commerce.read`), issued as the edit permission **Orders** (*Bestellingen*) in **Settings** > **API tokens**, and exist since server version 1.5.0. They are also [REST endpoints](https://docs.omniretailconnect.nl/commerce/api/order-writes.md) with the same payload. Each runs the staff action behind the order page, under the order's row lock, and the order's activity trail names the token.

*Order tools*

| Tool | Scope | Purpose |
| --- | --- | --- |
| `confirm_offline_payment` | `commerce.write.orders` | Record that an unpaid order was paid outside the payment provider. |
| `cancel_unpaid_order` | `commerce.write.orders` | Cancel an unpaid draft order and release its stock. |
| `add_order_note` | `commerce.write.orders` | Add an internal note for staff. |
| `add_shipment` | `commerce.write.orders` | Record a shipment with track and trace. |
| `update_shipment_tracking` | `commerce.write.orders` | Correct the carrier or tracking of a shipment. |

> **Security — No refunds, no paid cancellations, no customer data:** No tool refunds money or cancels a paid order. The scope gives no customer details: internal notes (`get_order` `internal_notes`) and tracking codes are only returned with `commerce.read.pii`. Notes are free text that may describe the customer; they are never written to the change log (it keeps the note id). See [Scopes and masking](https://docs.omniretailconnect.nl/commerce/reference/scopes-and-masking.md).

## confirm_offline_payment

Input: the order `id`. Records that the full amount of an unpaid draft order (status `awaiting-payment`) was received outside the payment provider. 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.

- An order this already confirmed: `changed: false`, no second mail.
- Any other order (paid through the provider, cancelled, held for payment review) is `conflict`. 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.
- The tool is advertised with `openWorldHint: true`, because it sends e-mail.

## cancel_unpaid_order

Inputs: `id` and `reason` (`customer_request`, `payment_not_received`, `duplicate_order`, `suspected_fraud` or `other`). Cancels an unpaid draft order and releases its stock hold. Destructive: it cannot be undone.

- An already cancelled order: `changed: 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 `conflict`. Staff in the admin are refused the reviewed draft too, but may still cancel a draft with an open online payment.

Output of both tools: `id`, `reference`, `fulfilment_state`, `status`, `placed_at`, `payment_state`, `changed`, `changes` (`status`).

## add_order_note

Inputs: `id` and `body` (up to 2000 characters). Adds an internal note for staff. It is stored as the text that was sent: never interpreted as HTML (angle brackets stay, every view escapes them), control characters removed, line breaks kept. Shown on the order's activity trail with the token id and a snapshot of its name; never to the customer and never in a customer mail. It is not the customer's own checkout note.

- Not idempotent: every call adds a note. An empty note is `invalid_argument`.
- Output: `order` (`id`, `reference`, `fulfilment_state`), `note` (`id`, `body`, `author`, `author_type` `token`, `created_at`), `changed` (`true`), `changes`.
- Read notes back with `get_order`; that needs `commerce.read.pii`.
- Notes are part of a data-subject access export and are deleted when the customer's data is erased.

## add_shipment

*Inputs of add_shipment*

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | integer | Required | The order id. |
| `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: `changed: false`, no second mail. With other data or on another order: `conflict`. |
| `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, 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. |

- The order must be paid in full and not cancelled, held for review or fully refunded, and a quantity may be at most what is left to ship (`conflict` otherwise).
- The shipment names no staff member; the order's activity trail names the token.
- The tool is advertised with `openWorldHint: true`, because it e-mails the customer.

## update_shipment_tracking

Inputs: `id`, `shipment_id` and at least one of `carrier`, `tracking_code`, `tracking_url` (https; `null` removes it). It changes only those fields: status, lines and dates stay, no mail is sent. Equal values: `changed: false`. A shipment of another order is `not_found`; a cancelled shipment, or one created through a carrier integration (the carrier owns its tracking), is `conflict`. The `shipment_id` is `fulfilment.shipments[].id` of `get_order`.

Output of both shipment tools: `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`. The change log keeps a fingerprint (`sha256:...`) of a changed code or link, never the value.

## Errors

- `invalid_argument`: an empty note, an `http` tracking link, a tracking link without a tracking code, a reason that is not in the list, or no field to change.
- `not_found`: an unknown order, or a shipment of another order.
- `conflict`: the order is not in a state the action allows; an `idempotency_key` used with other data; a quantity above what is left.
- The general write errors apply: see [errors of the write tools](https://docs.omniretailconnect.nl/commerce/mcp/tools.md#write-errors).
