SellstampWebhooks
Signed callbacks for 110 events,
on one delivery ledger.
A webhook here is another delivery channel for an event that already exists — not a second event system. The subscribable set is exactly the workspace-scoped half of the notification catalogue, so there is no mapping layer and no route anywhere that can invent an event name.
POST to your endpoint
x-sellstamp-signature: 9f2c…
x-sellstamp-event: order.paid
x-sellstamp-delivery: 018f…
user-agent: Sellstamp-Webhooks/1.0
{
"id": "018f…",
"event": "order.paid",
"createdAt": "2026-09-15T09:12:04.881Z",
"data": {
"type": "order",
"id": "3b7c…",
"shopId": "a41e…",
"orderNumber": "STMP-2026-00042",
"status": "paid",
"totalCents": 4900,
"currency": "EUR"
}
}What you can subscribe to
110 events across orders, payments, fulfilment, products, licensing, support, courses, pre-orders, wishlists, loyalty, badges, billing and system. They are the workspace's events — the ones belonging to a person's own account, like a password change, are excluded structurally rather than by a deny-list somebody maintains, because they carry no workspace and a workspace endpoint has no business being told.
An endpoint is yours to shape
A label, a URL, the events it wants, and either one store or the whole workspace. A store-scoped endpoint and a workspace-wide one are independent subscribers: if both asked for an event, both get it.
Send yourself a test
One button posts a signed test event to the endpoint, so you can confirm it is reachable and that your verification works before a real sale depends on it.
Retry one by hand
Any delivery in the history can be replayed from the dashboard — useful after you have fixed the bug that made it fail.
Verifying a request
One HMAC-SHA256 over the request body, hex, in x-sellstamp-signature. Compare it in constant time against your own digest of the bytes you received.
- Over the raw bytes, as sent
- Not over a re-serialised object. A receiver that parses the JSON first and re-encodes it produces a different string and a mismatching digest — this is the single most common way a webhook integration fails, so: hash the body you actually received, before anything parses it.
- There is no timestamp in the header
- Nothing but the body is in the HMAC input, and a
t=element would imply otherwise. The event’s owncreatedAtis in the body, inside the signature. - Rolling the secret costs no downtime
- Roll it and the old one keeps verifying for 24 hours — long enough to deploy inside one working day, short enough that a leaked secret is not honoured for a week. You can end the overlap early once you are done.
- During an overlap the header carries both
v1=<hex>,v1=<hex>, newest first — labelled so a receiver iterates rather than guessing at positions. With no roll in flight it is a single bare digest, byte-identical to what it always was, so nothing you wrote against the original contract has to change.- Two more headers you will want
x-sellstamp-eventnames the event type, andx-sellstamp-deliveryis a stable id — the same value asidin the body — to deduplicate against when a retry arrives after a success your server did not manage to acknowledge.
How delivery behaves
- The body is built when it is sent
- Not when it was queued. What is stored on the queue is identifiers; the body is assembled from a fresh read at send time, so a delivery that sat in a backoff for an hour tells your system what is true now rather than what was true then.
- A deleted resource says so
- If the thing an event was about is gone by the time the request goes out,
datais{ "deleted": true }. Telling your system an order still exists when it does not is worse than telling it nothing. - Retries
- Six attempts per delivery, on a 30-second doubling backoff with jitter. A delivery that never succeeds exhausts every attempt and dead-letters about sixteen minutes after its first try. Two workers cannot POST the same event to the same endpoint twice — a delivery is claimed with a conditional update before it is sent.
- One event, one row
- The enqueue happens inside the same transaction that claims the event’s deduplication key, so a webhook row exists exactly when the event does — never twice for a redelivered internal message, and never at all if the transaction behind it rolled back.
- Your server's own reply, kept
- Delivery history shows the HTTP status and the first 500 characters of what your endpoint answered. Everything else in this product stores no third-party response body; this one does, because it is yours and it is the only way to debug “my endpoint 500s”.
When your endpoint goes down
Per-delivery retry and per-endpoint health are two different clocks, and conflating them is the bug. The first asks whether this event has had a fair chance. The second asks whether your endpoint is still there.
An hour of failing, and you are told
Not a minute. An hour is a delivery that already exhausted all six attempts and gave up, plus about three more behind it that also did. Under that, a deploy or a certificate reload is the likelier explanation and an email about it is noise you would learn to filter.
A day, and delivery stops
Twenty-four hours after the first failure is twenty-three hours after you were emailed. Every timezone has crossed a working-day boundary in that window, so nobody is switched off without a waking hour in which to act. The dashboard says we switched it off, and when.
Recovery clears the incident
An endpoint that breaks, gets fixed, and breaks again next month is two incidents and gets two alerts — the record is a timestamp that can be cleared, not a flag that stays lit.
The SSRF boundary
Your endpoint is any address on the internet, so an allow-list is not available here. This is the reject-list variant of the same policy, and it is the only place in the product that deliberately makes an HTTP request to a host somebody else chose.
- HTTPS only, and no credentials in the URL
- No fragment, a real hostname, and a length bound. Making a request to an address you chose is the one place in the product that deliberately does that, so the defence around it is real rather than assumed.
- Every resolved address is checked at connect time
- Not in an earlier, separate lookup whose answer is then thrown away — the addresses approved here are the exact addresses the socket then dials. That is what closes DNS rebinding: a name that answers a public address during validation and a private one a second later never reaches a connection.
- Redirects are never followed
- A 302 pointing at a link-local metadata address is simply a non-2xx you see in your delivery history. There is no second request to defend.
- Bounded time and bounded read
- Five seconds, and 8 KB of your response. A hostile or hung endpoint costs one socket and a little memory, never a worker.
Limits worth knowing before you build on it
What this does not do
- No automatic replay of everything missed while an endpoint was disabled. The backlog is held, not replayed and not discarded; a single delivery can be retried by hand.
- No per-event-type health, no endpoint SLOs and no public status page for your own endpoint.
- No webhook endpoints on Trial or Starter. Growth allows five active endpoints, Scale twenty-five, Enterprise as many as you need.
- No plain HTTP destinations, and no way to opt out of the address checks — the endpoint has to be reachable over TLS on a public address.
- No custom headers on an outgoing request, and no mutual TLS. The signature is the authentication.
Open a store and try it.
Everything on this page is in the product today. Connect a provider in test mode and put a real order through it before you decide.