Signed outbound webhooks
SnapCount posts a signed JSON event to your HTTPS endpoint every time a counter moves.
Updated
How delivery works
- Someone countsA tap on Gate A: 186 to 187
- SnapCount signs itHMAC-SHA256 with your endpoint secret
- Your endpoint answersVerify, then reply 2xx within 10 s
Owners and admins of a Pro or Enterprise organization add endpoints in Settings, Developers, and choose which events each one receives. An organization can hold up to 10 endpoints.
- Endpoints must use HTTPS and cannot point at a private address such as
localhostor10.0.0.0/8. - Each endpoint gets its own secret, starting with
whsec_. It is shown once, when you create the endpoint.
Event types
| Event | Sent when |
|---|---|
counter.increment | A counter goes up. |
counter.decrement | A counter goes down. |
threshold.alert.fired | An Enterprise capacity alert fires on a counter or a group. |
threshold.alert.fired, but the event is only produced by an Enterprise threshold alert.Payloads
Every event has the same envelope: an id, the type, createdAt, and a data object. Count events carry the counter and the change. The id is built from the event itself, so one count always has the same id, even across retries.
{
"id": "counter.increment:cm1gatea0001:5812",
"type": "counter.increment",
"createdAt": "2026-09-26T18:42:10.000Z",
"data": {
"organizationId": "cm1org0001",
"counterId": "cm1gatea0001",
"counterName": "Gate A",
"previousCount": 186,
"count": 187,
"delta": 1,
"lastEventId": 5812
}
}counter.decrement has the same shape as an increment, with a negative delta. On a threshold alert, scope is counter or group, and kind is at_capacity, percent, or absolute.
Headers
POST /snapcount HTTP/1.1
Content-Type: application/json
User-Agent: SnapCount-Webhooks/1.0
X-SnapCount-Signature: t=1790448130,v1=5f8c...
X-SnapCount-Timestamp: 1790448130
X-SnapCount-Event: counter.increment
X-SnapCount-Delivery: cm1dlv0042| Header | Value |
|---|---|
X-SnapCount-Signature | t=<unix seconds>,v1=<hex>, an HMAC-SHA256 of ${timestamp}.${rawBody} keyed with the endpoint secret. |
X-SnapCount-Timestamp | The same unix timestamp, in seconds. |
X-SnapCount-Event | The event type. |
X-SnapCount-Delivery | A delivery id that stays the same across retries. Use it to ignore duplicates. |
Verify the signature
Check the signature before you trust a delivery. Compute the HMAC over the raw body exactly as it arrived, then compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto"
// secret: your endpoint's whsec_... value.
// rawBody: the body as received, before JSON parsing.
export function verifySnapCount(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((part) => part.trim().split("=")),
)
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex")
const received = Buffer.from(parts.v1 ?? "")
const wanted = Buffer.from(expected)
return received.length === wanted.length && timingSafeEqual(received, wanted)
}Reject old deliveries
The signed timestamp lets you refuse a replayed request. Compare t with your clock and drop anything older than a few minutes, allowing for the retry schedule below if you store and process deliveries later.
Retries and responses
Reply with any 2xx within 10 seconds to acknowledge a delivery. Any other status, or no answer in time, schedules a retry.
- First tryon the count
- Retry 1+30 s
- Retry 2+2 min
- Retry 3+10 min
- Retry 4+1 h
- Retry 5+6 h
| Your response | What happens |
|---|---|
2xx | Delivered. No more attempts. |
410 Gone | Stop. SnapCount does not retry this delivery. |
| Anything else, or a timeout | Retried after 30 s, 2 min, 10 min, 1 h, and 6 h, then marked failed. |
200, then process it, so a slow job never turns into a retry.Was this page helpful?
Ready to connect SnapCount?
Create an API key or a webhook endpoint in Settings. The public API and webhooks come with Pro and Enterprise.