Ring up a till sale
- Credentials, one of
- Station token
- OAuth access token
- Scopes
orders:write- Subscription
- Needs an active plan. Otherwise
403 subscription_locked. - Idempotency
- Send an
Idempotency-Keyheader. A retry with the same key does the work once.
A sale at the counter in one call: the order, every payment and the hand-over, so it lands in Orders as done. The same rule as the console's Sell page. Lines are priced from the catalogue as the till prices them (a keyed-in custom line, a price agreed at the till and the shelf check included); the client never sends a total.
Rung up once. clientSaleId is the till's own id for the sale, made on the device when the sale starts. Send it as the Idempotency-Key too. A repeat (a retry, a dropped connection, a queue sent again after the signal came back) returns the first sale with replayed: true and makes no second order. A sale whose order exists but never finished is 422 naming the order; one still being worked on is 409 in_flight.
Tenders. One or more ways the customer paid (cash, card on the merchant's own machine, EFT, Other), at most 6. Every part but the last needs an amountMinor; the last takes what is left. tenderedMinor on a cash part is what was handed over, for the change. These are recorded, never charged, and counter sales carry no platform fee. Scan to pay is startScanToPay, not this.
Offline. A sale kept on the device with no signal sends offline: true and its soldAt. It keeps its own time (never the future, at most a week back), is rung up even when stock has since run out (the order notes it), and belongs to the cashier cashierTicket named at soldAt.
Till PINs: when the store uses them, the cashier is the one cashierTicket names (403 cashier_required without one).
Access: OAuth scope orders:write; write-gated by the subscription.
AuthorizationBearer <token>It may call only the operations whose x-stations lists its kind, on its own store, and acts through the access of the owner or admin who paired it. Unpairing it, or that person leaving the business, ends it (401 station_revoked).
storeId*stringA store the person can reach. Any other id answers 404, never 403.
Idempotency-Key*stringRequired on every write. Minted on the device when the person taps, reused on every retry. A write without one, or with one that does not match the pattern, is refused with 422 validation_error. Scoped to the token's subject, remembered 24 hours. A replay returns the first 2xx response with Idempotent-Replay: true; a request still running answers 409 in_flight; the same key with a different method, path or body answers 409 idempotency_key_reused; non-2xx results are not remembered. If the key store cannot be read nothing is processed and the answer is 503 idempotency_unavailable: retry with the same key.
^[A-Za-z0-9_-]{8,128}$application/json- body
lines*array<>items <= 50tenders*array<>1 <= items <= 6customer?allowOversell?booleanSell an item that shows out of stock.
discountMinor?integerOff the whole sale, in cents. Not more than the sale.
0 <= valueclientSaleId*stringThe till's own id for this sale, made on the device. Sent twice, it rings up once.
^[A-Za-z0-9_-]{8,64}$soldAt?integerEpoch millis the sale was rung up, for a sale kept offline. Defaults to now.
offline?booleanThe sale was kept on the device with no signal and is being sent now.
cashierTicket?stringNeeded when the store uses till PINs.
Created
application/json- response
data*curl -X POST 'https://api.shopeazy.co/v1/merchant/stores/<storeId>/till/sales' \ -H 'Authorization: Bearer <access_token>' \ -H 'Idempotency-Key: <unique_key>' \ -H 'Content-Type: application/json' \ -d '{ "lines": [ { "itemId": "string" } ], "tenders": [ { "method": "string" } ], "clientSaleId": "string"}'{ "data": { "order": { "id": "string", "number": "string", "channel": "online", "customerName": "string", "soldByName": "string", "total": { "amount": 0, "currency": "ZAR" }, "status": "open", "paymentStatus": "unpaid", "fulfillmentStatus": "unfulfilled", "placedAt": 0, "lineCount": 0, "kinds": [ "physical" ], "currency": "string", "customerEmail": "string", "customerPhone": "string", "lines": [ { "id": "string", "kind": "physical", "requiresShipping": true, "title": "string", "variantTitle": "string", "customisations": [ { "groupName": "string", "choiceName": "string", "price": { "amount": 0, "currency": "ZAR" } } ], "quantity": 0, "unitPrice": { "amount": 0, "currency": "ZAR" }, "lineTotal": { "amount": 0, "currency": "ZAR" }, "fulfilled": true, "imageUrl": "string" } ], "subtotal": { "amount": 0, "currency": "ZAR" }, "shipping": { "amount": 0, "currency": "ZAR" }, "discount": { "amount": 0, "currency": "ZAR" }, "paid": { "amount": 0, "currency": "ZAR" }, "refunded": { "amount": 0, "currency": "ZAR" }, "kept": { "amount": 0, "currency": "ZAR" }, "refundable": { "amount": 0, "currency": "ZAR" }, "pendingRefunds": [ { "amount": { "amount": 0, "currency": "ZAR" }, "providerName": "string" } ], "eft": { "reference": "string", "overdue": true, "overdueAt": 0, "bankName": "string", "account": "string" }, "customerNote": "string", "giftNote": "string", "notes": [ { "at": 0, "message": "string", "author": "string" } ], "timeline": [ { "at": 0, "type": "created", "message": "string", "author": "string" } ], "fulfilment": { "method": "delivery", "serviceName": "string", "lockerName": "string", "address": "string", "provider": "string", "serviceCode": "string", "locker": { "id": "string", "name": "string", "provider": "string", "courier": "string", "address": "string", "hours": "string" }, "fellBack": true }, "shippingAddress": "string", "shippingAddressParts": { "addressLine": "string", "localArea": "string", "city": "string", "province": "string", "postalCode": "string" }, "shippingAddressMissing": [ "string" ], "needsCourier": true, "shipment": { "provider": "string", "trackingReference": "string", "state": "pending", "estimate": "string" } }, "receipt": { "storeName": "string", "address": "string", "phone": "string", "vatNumber": "string", "orderId": "string", "number": "string", "at": 0, "servedBy": "string", "customerName": "string", "customerEmail": "string", "customerPhone": "string", "lines": [ { "title": "string", "quantity": 0, "totalMinor": 0 } ], "discountMinor": 0, "totalMinor": 0, "taxMinor": 0, "payments": [ { "method": "string", "amountMinor": 0, "tenderedMinor": 0, "changeMinor": 0 } ] }, "replayed": true }}Void a sale rung up at the till by mistake POST
Moves money. A full refund with the stock put back (the lines already handed over too: the goods come back across the counter), posted to the ledger like any refund: the same VAT, platform-fee and stock rules as `refundOrder`, which it runs. The `Idempotency-Key` is the refund's own key, so a retry books the money back once. Already voided is not an error: the order comes back with `alreadyVoided: true`. Only a till sale (channel `manual`), only within a day of being rung up, and only money taken at the counter (Cash, Card, EFT, SnapScan, Zapper, Other): a sale paid through the card partner (Scan to pay) is refunded with `refundOrder`, where the partner is asked to pay it back. Otherwise 422 `validation_error` with the sentence. An order that is not paid is 422. The order gets the note "Voided at the till" and the call joins the Activity log. No email goes to the customer, as at the console's till. Till PINs: when the store uses them, the cashier is the one `cashierTicket` names (403 `cashier_required` without one). Access: OAuth scope `orders:refund` (opt-in on the consent screen, owner/admin grants only, never in a preset); write-gated by the subscription. Unlike `refundOrder` a staff member can void at the till, as in the console.
Start a Scan to pay sale POST
The customer pays on their own phone. The sale is priced from the catalogue as the till prices it (a keyed-in `custom` line, a price agreed at the till and the shelf check included), made as an UNPAID order, and the answer is the link for its pay page to show as a QR. It charges through the merchant's own card partner (Yoco, iKhokha or Peach: the first connected one the storefront charges through); the link carries a pass for this one order and works with the online shop switched off. Nothing is charged by this call: the partner confirms the payment like any online one, so VAT, the platform fee and stock follow the online rules. Poll `getScanToPayStatus`. The `Idempotency-Key` makes a retry return the first answer, so one tap makes one order and one link. No card partner connected is 422 `validation_error`; an item that shows out of stock is 409 `out_of_stock` (send `allowOversell` to sell it anyway). An order the customer never pays is cancelled with `cancelOrder`; a paid one is handed over with `fulfilOrder`. Till PINs: when the store uses them, the cashier is the one `cashierTicket` names (403 `cashier_required` without one). Access: OAuth scope `orders:write`; write-gated by the subscription.