Shipping

Formahand Shipping labels, self-shipping with flat rates and courier platforms: rates at checkout, labels from orders, tracking and the buyer page.

Written 2026-09-13. How a store gets shipping rates at checkout, how labels are bought and paid for, and how tracking reaches the merchant and the buyer. Every change here is an event (events.md); the money side is in payments.md and billing.md.

1. Three modes

A store ships in exactly one of three ways. The mode is which shipping integration is active (self means none), so switching it is the same write as any other integration (PUT /api/integrations, PUT /api/shipping { mode }, set_shipping { mode }).

ModeidRates at checkoutLabelsTracking sourceWho pays whom
Formahand ShippingformahandLive carrier rates through Formahand, priced at carrier cost × (1 + markup)Bought from Orders in one click; postage + the plan's markup accrues on the merchant's monthly statement (card on file required, charged once on the 1st)Included with the label, at no extra costShopper pays the store for shipping (through the store's processor); Formahand bills the merchant's card once a month
I ship it myselfself (default for new stores)The store's flat rates by country with free-over thresholdsNone; the merchant prints their ownFormahand's own tracking service where it is available, otherwise the carrier's tracking page linkNothing changes hands with Formahand
Use a courier platformkarrio, shippo, easypostLive rates from the merchant's own accountNone here (the merchant buys in their platform)The merchant's platform, with their credentialsThe merchant pays their platform

Checkout → Shipping shows the three as a mode switch at the top of the tab, with Formahand Shipping first; Integrations only lists the carrier platforms a merchant can connect with their own account. The ship-from address carries an email and a phone as well as the address: carriers print the sender contact on labels and USPS refuses a label without both, so "ship-from complete" means address + email + phone. New stores start on "I ship it myself".

Why included tracking is only for labels bought here. Tracking a parcel whose label Formahand sold is free; tracking any other number is charged per parcel, which would turn tracking into a cost no shipping fee covers. So a tracking number is only registered when the order's live label carries it. Self-shipped parcels are tracked through Formahand's own tracking service where that is available, and otherwise get the carrier's tracking page link only: the status stays "unknown", the dashboard says so, and nothing re-checks them. A merchant who connected their own courier platform is tracked through it, as before.

2. Money flow and markup

  • Markup. The plan sets it (billing.md §1): postage + 8% list / 5% promoted on Free, 5% / 4% on Growth, 4% / 4% on Pro (3% once statements can be paid by bank debit), with a $0.10 minimum per label. Promotions (SHIP5, the ten-paid-orders activation reward) lower it. The markup is applied in whole minor units, rounded half-up: 12.35 USD at 5% is 12.97, and a zero-decimal currency rounds to whole units.
  • Checkout. Unchanged: the shopper's estimate is quoted from the ship-from address, and every rate comes back in the store's currency at cost + markup, sorted, at most 5. Each rate keeps a reference so the order remembers which one the shopper chose. The shopper pays the store; nothing is charged to Formahand at checkout.
  • Currency. Rates in another currency than the store's are dropped; when nothing is left the shopper sees FORMAHAND_CURRENCY_ERROR. The label purchase makes the same check against the order's currency.
  • Labels: statement billing (since 2026-09-14). Buying a label re-confirms the rate, requires a card on file (test labels are free and need none), and records the cost, the markup that applied and the currency against the store. No card is charged per label: the line reads "Billed on your monthly statement". On the 1st, one charge covers every label accrued that month at max(markup, $0.10) each (billing.md §5). A card declined at that point is recorded on the statement as failed and the merchant is emailed; labels already bought are never revoked.
  • Voids. Voiding asks the carrier for a refund. It finishes at once when the carrier accepts, or parks the label as refund_pending until the refund is confirmed — a rejected refund leaves the label purchased. The label statuses are purchased, refund_pending, voided and refunded; refunded is final. Finishing a void drops the label's accrual while it is still unbilled; a label already billed on a past statement is not credited automatically — ask us and we will credit it. Labels bought before statement billing carry their own payment and are refunded through it, as before.

4. The merchant's card

Formahand Shipping needs a card on file, because postage is money Formahand lays out for the merchant. Add card (POST /api/shipping/formahand { action: "card-setup" }) opens the payment processor's own hosted page; the card is entered there and Formahand stores nothing but the brand, the last four digits and the expiry. On return the card becomes the store's default and the panel shows it. Closing the tab before the page returns does not lose the card — it is picked up either way. Replace card runs the same flow.

The monthly statement charges that card once, off-session, for the whole month's labels. A decline is recorded on the statement with the reason the processor gave, and the merchant is emailed to replace the card. Card data never touches Formahand.

5. Labels from Orders

GET /api/shipping/labels?orderId=[&quote=1] and POST /api/shipping/labels { action: "quote" | "buy" | "void", rateId? } (session, or orders:read / orders:write tokens). A quote re-prices the order from the ship-from address to the order's address with the order's item weights (items[].weightGrams on the order detail) and flags the rate the shopper chose at checkout. Buying marks the order fulfilled with the label's carrier and tracking number (the order detail names the live label as shippingLabel), starts tracking it, and emails the buyer the shipping confirmation (the store's order-shipped template, automations.md §2) with the tracking page unless notifyCustomer: false. Print label opens labelUrl, the carrier's PDF.

6. Tracking

An order's tracking comes from the first of these that applies:

  1. Formahand Shipping: the order's live label carries the number, and the carrier is asked directly. Later status changes arrive on their own.
  2. The merchant's own courier platform (karrio, shippo, easypost), with their credentials, as before.
  3. Formahand's own tracking service, for self-shipped parcels, where it is available.
  4. Links only: nothing to fetch, so the order links to the carrier's page and the status stays "unknown".

"Refresh tracking" (POST /api/order-actions { action: "track" }, get_order_tracking) works in every mode. Marking an order fulfilled with a number, action: "set-tracking" and set_order_tracking start tracking the same way, and setting a number on an unfulfilled order fulfils it.

Tracking refreshes itself. A parcel that is still moving is re-checked about every 30 minutes, oldest check first, while the order was fulfilled within the last 60 days and is not yet delivered or returned; orders that have only a carrier link are never re-checked, because there is nothing to ask. Labels waiting on a void settle the same way. Nothing you do is needed in between, and status changes the carrier pushes are recorded as they arrive.

Events. Every tracking record — a refresh, an automatic re-check, a carrier update, set-tracking, a label purchase or void — is a write on the order, and therefore emits order.updated with the changed fields (trackingStatus, trackingEta, trackingEvents, trackingCheckedAt, …) and the actor that caused it — the session user, token:<id>, system:shippo-webhook for a carrier update, or the scheduled actor Formahand records for an automatic re-check (events.md lists them) — plus the aliases order.tracking_updated when status, ETA or events changed and order.delivered when the status becomes delivered; a re-check that only refreshes trackingCheckedAt emits order.updated alone. Labels emit shipping_label.created / shipping_label.updated alongside the order. Subscribe with `create_subscription { topics: ["order.delivered"], target } or filter order.updated on changed contains "trackingStatus"` (events.md).

7. The buyer tracking page

https://<store host>/orders/<orderId>/track?t=<token> shows the buyer their own order: order number, carrier, tracking number, status, ETA, the carrier's event timeline and a link to the carrier's page. It is drawn in the store's own theme.

The t= token is unguessable, tied to that one order, and compared in constant time — a wrong token, or a cancelled order, is a 404, and the page is never cached, so nobody can walk a store's order ids. The order detail exposes trackingToken and trackingPageUrl; the shipping email (the store's editable order-shipped template) links this page first and the carrier's page underneath, and /checkout/success?session_id= shows the link once the order has a number.

8. Dashboard, API, agents

  • Checkout → Shipping: the three-mode control at the top of the tab; the Formahand card explains "labels at discounted rates, postage + your plan's markup on one monthly statement, tracking included" and shows the checklist (availability, ship-from address, card, parcel defaults) from GET /api/shipping/formahand, whose markupBps is the store's effective markup; "Use Formahand Shipping" activates it. Where Formahand Shipping is not available the card reads "Not available yet" and cannot be switched on (PUT /api/integrations answers 503).
  • Orders: the label panel (rates, buy, print, void), the tracking section for any order with a number (status, ETA, timeline, refresh, buyer page link, "Edit/Add tracking").
  • Checkout → Shipping: flat rates, ship-from address, and the parcel defaults every quote falls back to — 20 × 15 × 10 cm and 500 g until you change them.
  • GET /api/shipping adds mode and modes; PUT /api/shipping accepts mode and parcel. Token scopes: /api/shipping/formahand settings:*, /api/shipping/labels orders:read / orders:write.
  • MCP: get_shipping / set_shipping (with mode, parcel), get_shipping_label_rates, buy_shipping_label, void_shipping_label, get_order_tracking, set_order_tracking (see agents.md).
  • Events: tracking, fulfilment and label changes appear in Settings → Events & automations and reach subscriptions as order.updated, order.tracking_updated, order.delivered, shipping_label.* (events.md).

9. Local pickup and local delivery

Two options for shoppers near the store, both checkout rules (checkout-rules.md) rather than shipping rates, so they narrow or add exactly like every other rule and never touch the flat-rate table. Checkout → Shipping → Pickup and local delivery has one on/off card for each, and each card writes a single rule through PUT /api/checkout-rules.

RuleWhat the shopper seesWhat it costs
Local pickupkind: "pickup"Pickup at <ship-from address>, or target.address when the merchant typed their own line (a different door, opening hours, "ring the bell")Always 0. A pickup rule can never carry a price.
Local deliverykind: "local_delivery"target.rateName, default Local deliverytarget.feeMinor, the merchant's own fee

Postal-code prefixes, because Formahand geocodes nothing. There is no distance lookup anywhere, so a delivery radius is written as the list of postal-code prefixes it covers: conditions.destinationPostalCodes: ["SW1", "SW2", "SE1"]. A shopper matches when their own postal code starts with one of them, compared with spaces, hyphens, dots and case removed, so sw1a 2aa, SW1A-2AA and SW1A2AA are one address. A merchant who thinks in kilometres writes down the prefixes that ring their shop; the dashboard says so in as many words. A minimum order is the ordinary conditions.minSubtotalMinor, not a second grammar.

The destination reaches the rules the same way everywhere: the cart's "Ship to" estimate travels to the store's quote as the destination country and postal code, and only the rules this cart already matched are applied. A cart with no estimate matches no destination condition at all, which is the behaviour every store had before this release.

Shipping addresses at the payment page. The payment page is given a shipping-address box whenever the session carries any shipping options, and a pickup-only cart is no exception: the processor documents the two together and gives no supported way to show a shipping option without collecting an address, so the box stays. It costs the pickup shopper one form; in exchange the merchant keeps a real address on the order, which is what they need if the parcel ends up being posted after all.