Webhooks
Register an endpoint once and Skuflo calls you the moment a piece or an order changes state, signed, so your own systems never have to poll.
Creating an endpoint#
In the app, an admin goes to Settings, then Webhooks and API keys, and adds the endpoint with the events it should receive. The signing secret is shown once: store it, because Skuflo only keeps a hash of it too. Endpoints are managed only by a signed in admin, never with an API key, so a leaked key can never send your events somewhere new.
Choose every event type to receive them all. The URL must be https and reach a public internet address: private, local and internal network addresses are refused, and redirects are not followed.
What gets delivered#
Every delivery is a POST with a JSON body and four headers.
| Header | Meaning |
|---|---|
x-webhook-id | Stable across every retry of the same event. Use it to dedupe. |
x-webhook-event | The event type, for example item.dispatched. |
x-webhook-timestamp | Unix seconds when this attempt was sent. Part of the signature. |
x-webhook-signature | sha256=HMAC_SHA256(secret, "<timestamp>.<raw body>") |
The body carries the same id at the top level, alongside the event type, a timestamp and the event’s data.
Events#
Every payload is the real shape the API builds. Pick an event in the payload panel to see it.
| Event | When it fires |
|---|---|
order.created | An order lands on a production day, from a sheet upload, the retailer portal or POST /v1/orders/import. |
label.printed | The labels for an order are printed for the first time (GET /v1/labels only fires this once per piece). |
item.produced | A produce station scans a piece as made (POST /v1/scan). |
item.in_stock | A stock station books a piece onto a rack bay (POST /v1/scan with a valid location). |
item.dispatched | The loading bay scans a piece onto a vehicle (POST /v1/scan on a dispatch station). |
order.produced | Every piece on the order has been made (the roll up recomputes after every scan). |
order.dispatched | Every piece on the order has been loaded. |
item.remade | A label is voided and reprinted as a replacement (POST /v1/items/{barcode}/remake). |
load.closed | A load (vehicle trip) is closed out (POST /v1/loads/{id}/close). |
order.short_loaded | A load is closed with an order still missing pieces (one event per short order, alongside load.closed). |
retailer.updated | A retailer's settings change (PATCH /v1/retailers/{id}): name, email, delivery days, ordering or price visibility. |
return.requested | A retailer asks to send pieces back from their portal. It is outstanding straight away: there is no approval and no expected date. |
return.received | A returned piece is scanned in at a stock station by its original label (POST /v1/scan with return_condition), or marked received in the app. One event per piece, with its condition. |
return.resolved | Every piece of a return has an outcome: back into stock (also fires item.in_stock for that piece) or written off. |
Verifying a signature#
Never trust a webhook you have not verified. Recompute the signature from the raw body and compare it to the header. Every sample in the column on the right uses the timing safe comparison its language provides, so a wrong signature takes as long to reject as a nearly right one.
You can also reject a delivery whose x-webhook-timestamp is more than a few minutes old, which stops someone replaying a captured request later.
Retries and dedupe#
A delivery that does not get a 2xx response is retried with a growing gap between attempts, for around a day in total, then given up on. Every retry of the same event carries the same x-webhook-id, so storing ids you have already processed, even just for a day, makes your handler idempotent against retries. You can see recent attempts with GET /v1/webhooks/deliveries.
Factory and retailer endpoints#
A factory endpoint, registered with your API key, receives every event for every retailer you supply. A retailer’s own endpoint, registered from inside their portal, only ever receives events for their own orders. Both are signed the same way, each with their own secret.