Orders
Finding an order, shipping it whole or line by line, cancelling, refunding and restocking, tags, returns, and editing a paid order before it ships.
Written 2026-09-22, extended 2026-09-23.
Your orders — the order, its lines and how much of each line has shipped — live with your store's own data. Formahand keeps no copy of its own: it talks to the payment processor, sends the buyer's email, and does what you or your agent asked for. Every action below is the same action whichever way you reach it, so the dashboard, an API token and the MCP tools get the same answer, the same event and the same email.
1. Finding an order
Your store answers the filters, the search and the page; nothing is filtered in the browser, so a merchant with four hundred orders sees the same truth as one with four.
GET /api/orders (and the list_orders tool) takes:
| Parameter | Values | Means |
|---|---|---|
status | all, placed, paid, pending, payment_failed, refunded, abandoned | Where the money stands. placed is real orders — unpaid storefront checkouts are left out, orders the merchant created themselves are kept. paid includes orders later refunded. abandoned is pending or payment_failed. |
fulfillment | all, open, unfulfilled, partially_fulfilled, fulfilled, cancelled | Where the parcel stands. open is both unfulfilled and partially_fulfilled. |
q | text | Order number (with or without the #), customer email, customer or shipping name, order id, product name. |
limit | 1–5000 (250 default; the dashboard asks for 50) | Page size. |
cursor | the previous answer's nextCursor | The next page. |
The answer is { orders, limit, nextCursor }. Paging follows a cursor rather than an offset, so an order placed while you page never shuffles a row onto two pages or hides one. An empty nextCursor is the last page. The dashboard's pills are exactly the status and fulfillment parameters, and its pager walks the cursors: it never asks for a page it has not been handed.
2. Partial fulfilment
The order itself is unfulfilled, fulfilled or cancelled. How much of each line has shipped is kept per line, beside the lines, and survives a reload — a partly shipped order reads as partly shipped everywhere.
POST /api/order-actions { orderId, action: "fulfill", lines: [{ lineId, quantity }] }— orfulfill_order { lines }— adds those quantities to what has already shipped.lineIdisitems[].idfrom the order detail.- While something is still open the answer carries
fulfillment: { status: "partially_fulfilled", lines, complete: false }and the order detail reportsfulfillmentStatus: "partially_fulfilled", worked out from the per-line quantities. The order itself still counts as unfulfilled until the last line goes out, which is what keeps tracking, the fulfil guard and the storefront behaving as they always did. - Carrier, tracking and the buyer's "your order shipped" email belong to the whole order, so they go with the call that ships the last line (or through
set-trackingafterwards). A partial call that carries them is refused. - The call that ships the last line marks the order
fulfilledand forgets the per-line rows. - A partly shipped order can no longer be cancelled — refund it instead.
Every line of the order detail carries fulfilledQuantity, and fulfillment.lines repeats it, so get_order and the dashboard read the same numbers. The dashboard's "Mark as fulfilled" dialog shows one row per line with what is left; leaving the quantities alone ships everything and behaves exactly as it always did.
3. Cancelling, refunding and restock
- Cancel an unpaid order — its checkout reservation is released and the stock goes back, once, whatever order the cancel and the expiry webhook arrive in.
- Cancel a paid order — there is no reservation left to release (payment consumed it), so the stock is returned from the order's own lines. Product lines restock the product, variant lines the variant, and the product's variant mirror is recomputed.
- Refund — money only, unless the merchant asks for the goods back:
refund_order { restock: true }, or the "Put these items back in stock" tick in the dashboard's refund dialog. It is off by default, because a refund is not a return.
Only what never shipped comes back. A line that has already gone out is in the buyer's hands, not the stockroom, so its fulfilled quantity is subtracted: ship 3 of 5 and refund the order with a restock, and 2 return to stock. The per-line record is cleared at the same time — the order's stock has been settled one way or the other.
Restock happens at most once per order. It is recorded the first time, so a refund followed by a cancel, a repeated cancel, or a payment notification delivered twice cannot put the same units back twice.
4. Tags
Tags are your own labels on an order — "gift", "fragile", "call first". They are yours alone; the buyer never sees them. Flows have been able to set them since automations shipped; now a merchant and an agent can too:
POST /api/order-actions { orderId, action: "tags", tags: [...] }replaces the whole list (up to 30 labels of 40 characters, de-duplicated).tag_order { id, tags }is the MCP tool.- The order page has a tag row, and the order list and detail both return
tags.
Because the list is replaced, a caller reads the order first and sends the tags it should end up with. An empty list clears them.
5. Returns and refunds
A buyer sends something back from their own order page — the tracking link in their shipping email, or the order in their account. No login, no form to find, no email to write.
The window
Checkout → Returns (and set_return_policy) carries three settings:
| Setting | Means |
|---|---|
| Return window | Days after the parcel ships that a buyer may ask. 30 for a new store. 0 takes returns off and the block never appears. |
| Policy on the order page | A short paragraph the buyer reads above the form. Your full policy still belongs on a content page. |
| Default return instructions | What an approved buyer is told to do when you attach no label. |
The clock starts at fulfilment, because that is when the buyer could first look at what they bought; an order that never shipped counts from payment.
What the buyer can ask for
The block shows every line with anything left to return, the fixed reasons (damaged, wrong item, not as described, doesn't fit, no longer needed, arrived too late, something else) and a free-text note. It is refused for an unpaid or a cancelled order, outside the window, and while a return is already open — one open return per order. Quantities are checked against the order, so nobody can send back more than they bought, and a declined return gives its units back so the buyer can ask again.
It is also refused while the order is being edited (§6): an edit reprices and restocks the very lines a return is about, so the two are never open at the same time. The buyer is told the order is being changed and to come back.
The form posts with the order page's own token (shipping). Without that token the address is a 404, so no one can walk a store's order ids, and a store accepts at most 30 requests an hour.
What you do about it
Orders → Returns (#orders/returns) is the list; one return opens its own page (#orders/returns/<id>), and each move below asks in a dialog before it happens. The return policy is #orders/returns/policy.
| Move | What it does |
|---|---|
| Approve | Sends the buyer the instructions, and the return label if you attach one. requested → approved. |
| Decline | Needs a reason, which is what the buyer is told. requested → declined. |
| Mark received | Per line: how many arrived, their condition (unopened, opened or used, damaged, not in the parcel) and whether that line goes back on the shelf. approved → received. |
| Refund | Money back through the processor that took the payment. approved or received → refunded. |
| Restock | Adds the units you recorded as received, for the lines you ticked, onto the product or the variant. Once per return. |
| Close | The end. A closed return cannot be reopened, refunded or restocked. |
Anything else is refused with the reason — an approved return cannot be approved again, a declined one cannot be received, a closed one is final.
Return labels. Formahand does not sell one: every shipping mode (shipping) quotes and buys outbound labels only. A merchant who has a return label buys it in their carrier's own tools and pastes the link (and the carrier and number); everyone else sends instructions.
The money. A refund takes the order's own refund path (§3), so the cap is the same one: never more than the order total minus what was already refunded, whatever the returned goods were worth. A return that is not in a state that can become refunded — declined, closed, or already refunded — is refused before anything reaches the processor, so a second click on Refund never sends a second refund. Tax comes back the way the processor already handles it on any refund — gross, with the tax share pro-rated out of analytics. An agent's refund obeys the owner's money limits like any other. Card, PayPal and manual orders each refund through their own processor; a manual order is simply recorded as refunded, because the merchant moved the money themselves.
Restock is not automatic. A refund does not mean the parcel came back, so stock only moves when you mark lines received and tick them — and never more than the order line still holds, so an edit that already put units back cannot be counted twice.
If the order changed after the request. A return's lines are a snapshot: the price and the quantity the buyer asked about. If an edit reprices, shrinks or removes one of those lines afterwards, the panel shows what the goods are worth on the order as it stands now beside what was asked for, and "refund the returned goods" is refused rather than quietly trimmed. Type the amount you mean.
What it writes
Each move records return.requested, return.approved, return.declined, return.received or return.refunded on the event log, so flows and subscriptions can act on them (automations.md), and each one adds a line to the order's own timeline, so the order page and get_order tell one story.
From day one a store has the flow Return requested → email me, so the merchant hears about every request. The buyer's emails — return-approved, return-declined, return-refunded — go out with the move, from the store's own sender, and are editable like every other template. The recipe **Return approved → email the buyer** does the same from a flow of your own.
6. Editing an order
A buyer calls an hour after paying: "can you add one more, and send it to my office instead?" Before order editing, the only honest answers were "cancel and start again" or "I'll refund you and you re-order". Now the merchant changes the order itself, and Formahand works out what that does to the money.
What can be edited, and when
An order can be edited while it is paid and has not shipped. On a partly shipped order only the lines that have not gone out can change — a line in the buyer's hands is not a line you can take off the order.
| Not editable | Because |
|---|---|
| Unpaid | There is nothing to adjust yet. Collect the payment, or cancel it. |
| Cancelled | The sale is over. |
| Refunded (even in part) | Money has already moved on this order; an edit on top of it could not be reconciled. Refund the rest, or start a new order. |
| Fully shipped | Everything is with the buyer. |
| An edit is waiting to be paid | Collect or waive that balance first, so two open balances can never exist at once. |
| A return is open on it | A return moves the same lines and the same money an edit does, from the other direction — and a return's lines are frozen at the moment the buyer asked for them. Finish or close the return first (§5). |
| Worth nothing after the edit | An order that comes to zero is a cancellation, not an edit. Cancel it and refund it instead — the edit is refused before any money moves. |
The dashboard disables Edit order (in the order's More menu; the editor itself is #orders/<id>/edit) with the sentence above; get_order carries the same one on edits.reason, and the agent tools refuse with it.
What an edit may change:
- Lines — a quantity up (if stock allows; the units are taken from the catalog) or down to 0 (which removes the line and puts the units back). A line that has already shipped cannot be removed, and cannot go below what shipped: those units are in the buyer's hands, so they are neither refundable nor stock. The store refuses it, not just the screen.
- Products — add a line from your catalog at its current price. A merchant may set a price of their own, never above the catalog price (today's catalog price, which is also the cap when they lower a line the buyer already has); an agent may not set one at all.
- Discount — a code from your store, or an amount or percentage you decide here with the reason you gave.
- Shipping — a different method from the rates you configured, re-quoted against the edited cart (a "free over" threshold is applied exactly as it would be for a shopper buying these goods today).
- The address — re-checked the way checkout checks one: a real country, one you ship to, and a postal code wherever the tax and the rate depend on it.
- The buyer's email and phone.
The merchant's note and tags are not part of an edit: they change nothing about the sale and already have their own actions (§4), which work whatever state the order is in.
Stock moves the way a paid order's stock moves (§3): a paid order has no live checkout reservation left — payment consumed it — so an added or increased line takes its units from the catalog and a removed or decreased one gives them back, on the product or the variant, with the product's variant mirror recomputed.
Never editable: the currency, the tax engine and the payment processor. Those would be a different sale, not an edited one.
The money
The edited cart is priced by the same code the checkout uses. Your customer groups, your pricing rules, the discount code and your own cart pricing function all price a line an edit adds exactly as they would price it for a shopper buying it today, and the tax is recalculated by the same engine that taxed the order the first time.
An existing line keeps the price the buyer agreed to. It is never re-priced by today's rules, because the price stored on the line is already what the checkout charged — a group's price list, a pricing rule and a discount code all come off the line price before the cart is reserved, so pricing it through the engine a second time would take the same discount off twice and refund the buyer your own pricing rule. An edit that changes nothing but an address therefore moves no money at all. A merchant can still lower an existing line by hand (never above the catalog price); nothing else touches it.
Which also means an edit's difference is exactly the difference: the store re-prices the cart itself when the edit is applied and refuses it if the answer is no longer the one the screen showed, so the money that moved and the order's recorded delta_minor are the same number by construction.
Before you apply anything, the screen says in one sentence what it costs:
| The new total is | What happens |
|---|---|
| Higher | The buyer is emailed a page to pay the difference on, and the order waits on that balance. A saved card is never charged: the buyer agreed to the first total, not this one. |
| Higher, on a bank transfer / cash on delivery order | The balance is recorded as owed; mark it settled when the money arrives. |
| Higher, on a PayPal order | There is no extra payment to take here. Ask the buyer yourself, then waive the balance. |
| Lower | The difference is refunded through the processor that took the payment, under the same cap every refund obeys (§3). There is no store credit. |
| The same | Nothing moves. |
Waiving a balance (Orders → the order → Mark the balance as settled, or waive_order_edit_balance) is how a merchant closes an unpaid difference — because they forgave it, or because they collected it another way. Any payment link that was sent stops working, and the order can be edited and shipped again. Forgiving a balance is giving up money you are owed, so an agent doing it obeys the owner's money limit exactly as applying an edit does — and an edit that raised a total counts against that limit the same way a refund does, so an agent cannot walk round the daily figure one order at a time.
If a payment link is somehow paid after the balance was settled — or for an amount that is not the one it asked for — the store does not treat it as the balance. The payment is left where it is and the order gets a note saying what arrived, so you can refund it or keep it knowingly.
One transaction, one record
The lines, the stock, the totals, the tax and the address are written in a single transaction: a quantity the store cannot stock refuses the whole edit and leaves the order exactly as it was. There is no half-edited order.
Every edit is recorded with your store's own data: who asked, the order before, the order after, the money difference, the reason and how the difference was settled. The order's timeline prints one line per edit, and list_order_edits reads the lot — which is the whole answer to "why is this order not what the buyer first paid for?".
What it writes
order.edited carries the order as it now stands and order_edit.applied the record with its diff; an edit that raised the total also emits order.payment_requested, and order_edit.paid / order_edit.waived follow when the balance is settled (automations.md). The buyer gets Order edited — what changed, the new total, and a link to pay a difference if there is one — and, when a balance is owed, Order payment requested. Both are editable like every other template, and the recipe **Order edited → email the buyer** sends the first from a flow of your own.
6a. Tracking, and the way back in
Every fulfilled order has a tracking page of its own at /orders/<id>/track?t=<token>, where the token is an HMAC of the order id with the store's own tracking secret. The shipping email carries that link; nothing else does, and an order id on its own opens nothing.
That is the right shape and it left one gap: a buyer who deleted the email had no way back in at all. They wrote to the merchant, who looked the order up by hand. So the store answers one more public address.
**/orders/track** is a form: order number and the email address the buyer ordered with. If the two belong together, the store emails that address the signed link. It is linked from the footer of every store and from the seeded shipping policy.
Three things about it matter more than the form:
- The answer never depends on whether they matched. Same page, same sentence — "If that order number and email address belong together, we have emailed the tracking link to that address." — whether the order exists, whether the address is right, or whether the whole thing was a guess. Order numbers are short and run in sequence, so a page that said "no such order" would be a way to enumerate a shop's customers.
- The link goes to the address on the order, never to the address in the box. Getting the pair right is what proves the asker is the buyer; typing an address is not.
- It is rate limited, durably. Five attempts an hour per address, twenty per address it is asked from and two hundred per store, counted on every attempt whether it matched or not — so guessing costs exactly as much as being right, and the count holds wherever the request lands. A hidden field catches the scripts. What is kept is one-way digests, never an address, and an attempt is forgotten after a day.
A cancelled order has nothing to track and is not found, exactly as the tracking page itself decides.
7. What each surface can do
| Action | Dashboard | MCP tool | Scope |
|---|---|---|---|
| List, filter, search, page | ✅ | list_orders | orders:read |
| One order with lines, tags, per-line fulfilment | ✅ | get_order | orders:read |
| Fulfil, whole or by line | ✅ | fulfill_order | orders:write |
| Tracking after fulfilment | ✅ | set_order_tracking | orders:write |
| Cancel (restocks) | ✅ | cancel_order | orders:write |
| Refund, optionally with restock | ✅ | refund_order | orders:refund |
| Note | ✅ | add_order_note | orders:write |
| Tags | ✅ | tag_order | orders:write |
| CSV export | ✅ | export_orders | orders:read |
| Packing slip | ✅ | print_packing_slip | orders:read |
| Returns: list and read | ✅ | list_returns, get_return | orders:read |
| Approve, decline, receive, restock, close a return | ✅ | approve_return, decline_return, mark_return_received, restock_return, close_return | orders:write |
| Refund a return | ✅ | refund_return | orders:refund |
| The return window, policy and instructions | ✅ | set_return_policy | settings:write |
| Start, change and preview an order edit | ✅ | begin_order_edit, edit_order_draft, preview_order_edit | orders:write |
| Apply, cancel or waive an edit; list them | ✅ | apply_order_edit, cancel_order_edit, waive_order_edit_balance, list_order_edits | orders:write |
| Apply an edit that lowers a total | ✅ | apply_order_edit | orders:write and orders:refund |
Applying an edit that lowers a total refunds the buyer, so it needs the same scope refund_order needs. preview_order_edit says so on needsRefundScope before the agent tries, next to agentLimit.
Bulk actions on the list are deliberately not built yet.
8. Gift cards
Orders → Gift cards (#orders/gift-cards) is a third list on this screen, beside the orders and the returns, for the same reason returns are here and not somewhere else: a gift card is money somebody already paid you, waiting to be spent. Putting it beside the discount codes would say the wrong thing about what it is.
The list shows every card, what it is worth now, what it was worth when it was made, who made it — you, an agent, or an add-on you installed — and whether it is still good. It never shows a code. Your store keeps a one-way value and the last four characters, so a card that has gone astray is stopped and replaced rather than re-sent.
History on a card is the whole of it: issued, held at a checkout, spent, given back because that checkout was never paid, put back by a refund, changed by hand, stopped. A balance nobody can explain is the thing this list exists to make impossible.
Issue a gift card gives somebody money to spend in your shop, so your shop owes that amount until it is spent. The code is shown once.
Stop it ends a card. Its balance stays on the record so your books still add up, and stopping it moves no money: if somebody is owed this, issue them another.
On an order
An order paid partly with a card carries the whole bill as its total, with the split beside it — what the card paid, and what the processor charged. A refund goes back on the card first, capped at what the card actually paid on that order, and the rest goes back through the processor.
payments.md § Gift cards has the rest: what stops a card being used, where tax lands, the scope an agent needs, and the add-on that sells them.