Order handoff
When a SimplePath shop sells something the owner brought in from a supplier, we POST the order to a web address the owner chose. This page is everything you need to receive it.
The request
POSTwithContent-Type: application/json, over HTTPS only.Idempotency-Keyis the order id, and it is identical on every attempt. Use it to make retries safe.X-SimplePath-Signatureisv1=followed by the hex HMAC-SHA256 of the raw request body, keyed with the signing secret shown to the site owner when they set this up.X-SimplePath-Eventrepeats the event name, so you can route without parsing.- If the owner added a credential, it is sent the way that service expects it: a bearer token, a named header, or a query parameter. A credential is only ever sent to the address it was saved for, so the destination must be on that same address.
Answer with a 2xx
Any 2xx means we are done. A 5xx or 429 is treated as temporary and retried with a widening gap for up to five days. Anything else, including a redirect, is treated as final and shown to the site owner with whatever text you returned, so a short plain-English reason in the body is worth sending.
We never follow redirects. The request carries a customer’s address, so it goes to the address we checked and nowhere else.
Events
order.paid— money has arrived. This is the one to act on.order.refunded— the sale was refunded. CarriesrefundedCents. Stop the shipment if you still can.order.test— the owner pressed the test button. Accept it and create nothing.
Every line is sent
A shop can sell your products and its own on one order. We do not guess which lines are yours: every line arrives, each carrying source (the name the owner gave your connection) or null. Filter on source, and you can still see the whole order, which is usually what the packing slip needs.
Telling us what happened
Every payload carries callbacks.status. When you ship, deliver or cancel a line, POST there and the shop owner sees it on that line in their workspace, and the buyer is emailed the tracking number when you report shipped. Sign it exactly as you verify ours: an HMAC-SHA256 of the raw body with the same signing secret, in X-SimplePath-Signature.
POST https://simplepath.ai/api/store/handoff/status
Content-Type: application/json
X-SimplePath-Signature: v1=<HMAC-SHA256 of the raw body, hex>
{
"orderId": "cs_live_abc123",
"lines": [
{ "productId": "merch-hoodie", "externalId": "4711", "status": "shipped",
"carrier": "USPS", "tracking": "9400 1000 0000 0000 0000 00" }
]
}statusis one ofsent,shipped,delivered,canceled.- Name the line by
productId, byexternalId, or both, and addvariantIdwhen the order had a choice. Only lines that carry yoursourcecan be changed by you; a report naming anything else is ignored and the answer says how many lines were matched. tracking,carrierandnoteare optional. The note is shown to the owner, never to the buyer.- The answer is
200 { "ok": true, "matched": 1, "ignored": 0 }. A bad signature is401; an order we do not know is404.
The buyer's email may be missing
Site owners can switch it off. When they do, buyer.email is absent rather than empty, so do not depend on it. Name and delivery address are always sent.
Example payload
{
"version": 1,
"event": "order.paid",
"sentAt": "2026-09-07T14:02:11.000Z",
"order": {
"id": "cs_live_abc123",
"paidAt": "2026-09-07T14:02:10.000Z",
"currency": "usd",
"subtotalCents": 6998,
"shippingCents": 0,
"taxCents": 0,
"discountCents": 0,
"totalCents": 6998
},
"buyer": {
"name": "Ann Buyer",
"email": "ann@example.com",
"shipping": {
"name": "Ann Buyer",
"line1": "1 Example Street",
"line2": "",
"city": "Cumming",
"state": "GA",
"postalCode": "30040",
"country": "US"
}
},
"lines": [
{
"productId": "merch-hoodie",
"externalId": "4711",
"source": "MY_PRINTER",
"variantId": null,
"name": "Signature Hoodie",
"option": "L",
"quantity": 1,
"unitCents": 6499
},
{
"productId": "signed-book",
"externalId": null,
"source": null,
"variantId": null,
"name": "Signed book",
"option": null,
"quantity": 1,
"unitCents": 499
}
],
"site": {
"projectId": "abc12345",
"name": "Example Shop",
"url": "https://example.com"
},
"callbacks": {
"status": "https://simplepath.ai/api/store/handoff/status"
}
}Verifying the signature
Compare in constant time, and verify the raw body before you parse it.
// Node. Verify BEFORE parsing: the signature covers the raw bytes.
const crypto = require('crypto')
function verify(rawBody, headerValue, signingSecret) {
const expected = 'v1=' + crypto
.createHmac('sha256', signingSecret)
.update(rawBody, 'utf8')
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(headerValue || '')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Versioning
version is 1. We will not remove or rename a field in version 1; anything new arrives as an additional field, so parse tolerantly and ignore what you do not recognize.