# Order tools
**This group is read only.** Nothing in it can change an order, issue a refund, cancel
a fulfilment or touch a customer record. That is a decision rather than a gap, and the
reasons are set out under [What this group deliberately cannot do](#what-this-group-deliberately-cannot-do).
Every other group on this surface reads or writes something **the merchant authored**: a
theme, a page, a price, a broadcast they wrote. This one reads records about **shoppers**,
who never agreed to your integration. So it discloses less than the REST API does, on
purpose, and the difference is not configurable:
| | What you get here | Where the rest lives |
|---|---|---|
| Shopper email | Masked (`so***@example.com`), on `get_order` only | `GET /api/v1/orders/{id}` |
| Shopper name | Not returned | `GET /api/v1/orders/{id}` |
| Street address | Not returned | `GET /api/v1/orders/{id}` |
| Phone number | Not returned | `GET /api/v1/orders/{id}` |
| Delivery region | City, province, postal code, country | same |
| Order metadata | Not returned | `GET /api/v1/orders/{id}` |
| Buyer form responses | Not returned | `GET /api/v1/orders/{id}` |
| Gift recipient details | Not returned | `GET /api/v1/orders/{id}` |
**`list_orders` returns no contact details at all**, not even masked ones. A list is where
bulk disclosure happens: a page of a hundred orders carrying one email each is a hundred
addresses in a single tool result, and a tool result lands verbatim in a model's context
window and in whatever transcripts and logs its host keeps. When a program genuinely needs
the delivery details, for example to print a label, read the REST API under `read_orders`,
where the caller is a program and the bytes go where that program puts them.
**There is no reveal parameter, on any tool, and there will not be one.** A parameter is
set by the model, not by you: its description is metadata an agent reads and a human never
sees, and a switch that widens disclosure is one an agent can flip on its own initiative.
Every other input on this surface (`?features=`, `?read_only=true`) can only take
capability away.
`?read_only=true` strips write scopes. It is a **mutation** control, not a **disclosure**
control: it leaves every tool in this group fully available, because none of them writes
anything in the first place. Do not treat it as a privacy setting.
### get_order
Reads one order: what was bought, the money, the status, and the region it was shipped to. The buyer's email is MASKED and their name, street address and phone number are not returned at all, so this is enough to confirm you have the right order and not enough to contact anyone.
**Scope:** `read_orders`
From `list_orders`. Not the order number a shopper sees: the only lookup by order number the platform has also requires the shopper's email, so that a caller cannot walk the order numbers.
### list_orders
Lists this store's orders, newest first, with each one's id, order number, status, total and how many lines it has. Call this to get the `order_id` that `get_order` and `list_refunds` need.
**Scope:** `read_orders`
Only orders in this state: `pending`, `paid`, `fulfilled`, `completed`, `cancelled` or `refunded`. Omit for every state. A value outside this list is refused rather than matched, because an unmatched status answers an empty page that reads as "this store has no orders like that".
Only this buyer's orders. The id comes from a previous row of this list; there is no tool here that looks a customer up by email or name.
How many, 1 to 100. Defaults to 20.
The cursor printed at the end of a previous page. Pass it back verbatim; it is not a number and not an id.
### list_refunds
Lists what has been refunded against one order, newest first, with each refund's amount, status and which processor moved the money back. An order that was never charged is reported as such rather than as an empty list, because "charged and nothing given back" is a different fact.
**Scope:** `read_payments`
This is the one tool in the group that does not read the order tables, so it carries the
scope that `GET /api/v1/orders/{id}/refunds` already carries. That is deliberate: a key
should see the same rows through both doors, and gating it on `read_orders` would have
quietly widened what `read_orders` means. `read_payments` is not a customer-data grant, and
the refund ledger holds no card number, no last four and no shopper.
From `list_orders`.
How many, 1 to 100. Defaults to 20.
The cursor printed at the end of a previous page, verbatim.
### sales_report
Aggregate sales for a period: order counts, gross merchandise value, average order value and units, with an optional daily series and an optional best-sellers list. Every figure is a total and no individual appears in any of it.
**Scope:** `read_orders`
GMV is **gross**, so money that later left the merchant is counted in it and reported on
its own line: `refunded` is the amount refunded, and `disputed` is the charge amount of the
chargebacks the merchant lost. The report prints the net figure, gross minus both, so it
does not have to be re-derived. A sale means an order that reached payment, so pending and
cancelled orders are excluded from everything except `total_orders`. Figures are in minor
units and are summed across every currency the store took, so on a multi-currency store
they are not one currency's total.
RFC3339 timestamp, inclusive. Omit for the whole history.
RFC3339 timestamp, exclusive. Must be after `from`. Omit for now.
Optional extra sections: `timeseries` adds one row per day, `top_products` adds the best-selling variants. The headline summary is always returned. Ask for a section only when you will use it: a 90 day series is 90 rows to carry.
How many best-sellers, 1 to 100. Defaults to 10. Ignored unless `include` names `top_products`.
## What this group deliberately cannot do
**Issue a refund.** The platform's refund path is genuinely safe to call from a thin
client: over-refund protection, the two row locks, the reason validation and the non-cash
tender legs all live inside the service and none of them can be bypassed. The problem is
the idempotency key. A model that retries a tool call will mint a fresh key, and the second
call moves money a second time while every guard passes, because the two requests are
honestly distinct. Deriving the key server-side from the order and the amount would break
the legitimate "refund 500 now, 500 more later" case. Issue refunds from the dashboard or
the REST API, where the key and the person responsible are both real.
**Cancel, fulfil or complete an order.** Each is terminal or near-terminal, and a cancel
reverses compensations with no undo. They would need `write_orders`, which this group leaves
entirely unused.
**Read or write a customer record.** No `list_customers`, no `get_customer`, no customer
notes. They would need `read_customers`, which exports email, name, phone and postal
addresses, and advertising it would ask every merchant connecting an agent to change a font
colour for their whole customer directory, pre-ticked on the consent screen. Staff notes
are worse again: free text written about a shopper, by name. Read customers over the REST
API under `read_customers` when a program needs them.
## Narrowing a connection to this group
Add `?features=orders` to the connection URL to give an agent these four tools and nothing
else. The dashboard's Connect panel writes it for you.
`?features=` is a **context-window convenience, not a security boundary.** A URL that
omits the parameter gets every group, including this one, because a connection made before
a group existed must not silently lose tools. What actually gates this group is the scope
on each tool: a token that never asked for `read_orders` cannot list orders, and one that
never asked for `read_payments` cannot read the refund ledger, whatever the URL says.