01API
Webhooks
A webhook is an address on your server that Rollmark calls when something happens. You do not poll; you are told. Each call is a POST of JSON, signed, so your server can tell it is from Rollmark.
Set one up
- In the dealer app: Settings, Webhooks, Add endpoint. Or with the API (
webhooks:manage):POST /webhooks. - Give an
https://address, pick the events, and copy the signing secret. It is shown once. If you lose it, rotate it. - Press Send test event. Within seconds your server gets a
ping; the delivery log shows whether it worked.
A store has up to 10 endpoints. An endpoint made with an API key can only subscribe to events that key could read (order.* needs orders:read; distributor_order.* needs distributor_orders:read), and it is switched off for good if that key is revoked.
The address must be public
The address must be https://, on the standard port (443), with no user name, and name a public host. Rollmark refuses an address that is, or resolves to, a private, loopback, link-local (including the cloud metadata address), carrier-grade-NAT, reserved or multicast address, in any spelling, IPv6 included. It checks when you register and again at every delivery, connects only to the address it checked, and never follows a redirect (a 3xx is a failed delivery). During development use a public tunnel to your machine, not localhost.
The events
| Event | When | data |
|---|---|---|
order.created | An order is made (API, FFL Checkout or staff) | order_id, intake_ref, source, status |
order.matched | A pending order is matched to the store's own order | order_id, store_order_number, status |
order.cancelled | An order is cancelled | order_id, status |
order.ready | An order is marked ready to ship | order_id |
order.shipped | An order is marked shipped | order_id, tracking, shipped_at |
order.delivered | An order is finished as delivered | order_id |
distributor_order.status_changed | A distributor order changes status | distributor_order_id, po_number, from, to, distributor, confirmation_number, tracking, order_ids |
ping | You pressed Send test event | webhook_id |
The full payloads are in the events reference.
A payload is small on purpose. It holds ids and statuses. It never holds the shopper's name, email, phone or address, a serial number, or the store's cost. When you need the order, read it: GET /orders/{order_id} with your orders:read key. This keeps a customer's details out of your logs and your receiver's retries.
Every event has the same envelope:
What Rollmark sends
Verify the signature
The signature is HMAC-SHA256 with the endpoint's secret over the text <timestamp>.<raw body>, as lowercase hex, in Rollmark-Signature: v1=<hex>. The timestamp (Rollmark-Timestamp, unix seconds) is inside the signed text, so a captured request cannot be replayed later: reject a timestamp more than 5 minutes from your clock. Verify against the raw body bytes as received, before you parse the JSON. Compare in constant time.
While you rotate a secret, Rollmark signs with the new and the old for 24 hours: Rollmark-Signature: v1=<new>,v1=<old>. Accept any v1= value that matches any secret you hold.
A test vector to check your code against: secret whsec_test_secret, timestamp 1700000000, body {"id":"evt_test"}, signature 13941114bb88ac44a76abcfddea5b92aa6182a4b63d8be3aae908a616083bd7e.
Node
In Express, read the body as text, not parsed JSON: app.post('/rollmark', express.text({ type: 'application/json' }), (req, res) => { if (!verify(req.body, req.headers, [SECRET])) return res.sendStatus(400); … res.sendStatus(200); }).
PHP
Python
Answer, and be ready for repeats
- Answer with any
2xxwithin 10 seconds, before you do slow work: verify, store the event, answer, then process. - Anything else, a timeout, a connection error, a TLS error or a redirect, is a failure and is tried again.
- Delivery is at least once. The same event can arrive more than once (a retry after your answer was lost, or a person pressing Resend). Deduplicate on
id(theevt_value, alsoRollmark-Event-Id): it is the same on every retry and every resend. Keep the ids you have handled for a few days. - Events are not guaranteed to arrive in order. Use
created_at, and read the order itself (GET /orders/{id}) for its current state.
Retries
Eight attempts over about two days: at once, then 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours after the previous failure (each wait varies by up to 10%). After the eighth failure the delivery is dead: it stays in the log with a Resend button. An endpoint whose deliveries fail 20 times in a row is switched off (disabled_failing) with the reason shown; fix it, then switch it on. A delivery that waited more than 72 hours is dropped, not sent in a flood.
The log
Settings, Webhooks, Recent deliveries, or GET /webhooks/{id}/deliveries: each delivery's event, time, number of tries, the HTTP status or a short reason, and its state (pending, retrying, delivered, dead). The payload is not shown. Delivered deliveries are kept 30 days, dead ones 90.
Secrets
Keep the secret on your server, never in a web page or a repository. Rotate it with Rotate secret or POST /webhooks/{id}/rotate-secret: you get a new secret (shown once) and both sign for 24 hours, so you can switch without a gap.