Webhooks
Webhooks push events to a URL you control instead of you polling the API. Every event is HMAC-signed with a per-endpoint secret so you can verify it actually came from Stawka before acting on it.
Webhooks are available on Hobby and Pro plans. On Free plans the create button is disabled; existing endpoints from a prior paid plan are retained but paused — upgrade and they resume automatically.
- Go to Webhooks in the dashboard (under your organization).
- Create webhook → enter the receiver URL and pick at least one event.
- Copy the signing secret that’s revealed once — Stawka never shows it again. Store it like any other secret.
- The dashboard’s Send test event button fires a synthetic delivery so you can verify your receiver before real events flow.
You can edit the event set anytime; the secret can be rotated from the detail page. After rotation, the previous secret stops verifying signatures immediately — update your receiver before any new event fires.
Event taxonomy
Section titled “Event taxonomy”| Event | When it fires | What’s in data |
|---|---|---|
key.created | A new API key is minted in the dashboard. | { keyId, prefix, name, createdBy } |
key.revoked | A key is revoked from the dashboard. | { keyId, prefix, name, revokedBy } |
key.rotated | A key is rotated (old key revoked, new key minted as a pair). | { oldKeyId, newKeyId, prefix } |
quota.threshold | First time in the month the org crosses 80% of its monthly request quota. Fires once per month per org. | { planId, limit, used, percent } |
quota.exceeded | A request is denied because the org is over its monthly quota. Fires every denied request — treat as a “stop hammering” signal, not a one-shot alert. | { planId, limit, used, retryAfterSeconds } |
subscription.upgraded | Plan moves to a higher tier (Free → Hobby/Pro, Hobby → Pro). Free → paid emits upgraded. | { fromPlan, toPlan } |
subscription.downgraded | Plan moves to a lower tier or is cancelled (anything → Free). | { fromPlan, toPlan } |
rates.changed | Daily TEDB cron detected at least one VAT rate move vs. yesterday’s snapshot. | { effectiveDate, changes[] } — same shape as the /changes endpoint. |
Stay narrow on what to subscribe to. quota.exceeded for a hot key
can fire thousands of times per minute under abuse. Subscribe only
to events your receiver actually needs.
Payload envelope
Section titled “Payload envelope”Every delivery — regardless of event — has the same outer shape:
{ "id": "whd_2bX5tQ9...", // delivery id; same across retries "event": "rates.changed", // one of the event names above "orgId": "org_abc123", // your organization id "createdAt": "2026-05-30T07:00:23.000Z", "livemode": true, // always true in prod "data": { /* event-specific */ }}The id is the delivery row id, stable across retries of the same
delivery. A successful receiver should dedupe on this — if you’ve
already processed whd_2bX5tQ9... and a retry arrives, ack it without
re-acting.
Signature verification
Section titled “Signature verification”Stawka sends a Stawka-Signature header on every request:
Stawka-Signature: t=1748583623,v1=8b2c91a7e9d4f5c1...t— UNIX seconds at the time we signed.v1— lowercase hex SHA-256 HMAC of${t}.${rawBody}, keyed by your endpoint’s signing secret. The${t}.prefix is the replay-defence: an attacker who captures one signed body can’t resend it indefinitely.
The format mirrors Stripe’s webhook signing — receivers familiar with
that pattern have one less new thing to learn. v1 is a version
tag; if we ever need to migrate the algorithm we’ll emit both v1
and v2 during a deprecation window.
Verification steps
Section titled “Verification steps”- Read the raw request body before parsing JSON. Whitespace, key ordering, and number formatting all affect the HMAC. Don’t re-serialize from a parsed object — sign the exact bytes you received.
- Reject anything more than ±5 minutes off. Compare
tto your server clock; outside that window, return 400. - HMAC-SHA256 of
${t}.${rawBody}with your endpoint secret. Compare in constant time to thev1hex.
Node.js example
Section titled “Node.js example”import crypto from "node:crypto";
function verifyStawkaSignature(rawBody, header, secret) { const parts = Object.fromEntries( header.split(",").map((kv) => kv.split("=")), ); const t = Number.parseInt(parts.t, 10); if (!Number.isFinite(t)) return false; if (Math.abs(Date.now() / 1000 - t) > 5 * 60) return false;
const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex");
return crypto.timingSafeEqual( Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"), );}The crypto.timingSafeEqual call matters — === on the hex strings
leaks timing information that lets an attacker recover the secret
byte-by-byte. Same lesson applies in any language: compare in
constant time.
Retry policy
Section titled “Retry policy”We send POST and expect 2xx to ack. Any other response — or
no response within 10 seconds — counts as a failure and we re-queue.
The schedule, indexed by failed-attempt number:
| Attempt | Delay before next |
|---|---|
| 1 | 30 s |
| 2 | 1 min |
| 3 | 2 min |
| 4 | 4 min |
| 5 | 8 min |
| 6 | 16 min |
| 7 | 32 min |
| 8 | 1 h |
| 9 | 2 h |
| 10 | 4 h |
| 11 | — (marked dead, no more retries) |
Each delay is jittered ±10% to spread retries across receivers that all fail at once (e.g. when a popular cloud webhook host has an outage). Total budget is 11 attempts.
Auto-disable
Section titled “Auto-disable”After 5 consecutive failures an endpoint is automatically paused. Pending deliveries finish their retry budget; new events won’t enqueue until you fix the receiver and re-enable the endpoint from the dashboard. A single successful delivery resets the consecutive-failure counter.
Delivery semantics
Section titled “Delivery semantics”- At-least-once. A network blip mid-retry can produce a duplicate
delivery of the same
id. Always dedupe. - Order is best-effort. Within a single endpoint, deliveries
generally arrive in emit order, but retries can shuffle that.
Don’t depend on
key.createdarriving before itskey.revokedfor the same key — read state from the API if order matters. - Failures are observable. Each delivery attempt is logged with its status, http response code, and any network error. The dashboard’s webhook detail page shows recent deliveries; failing rows surface inline so you can debug without grepping logs.
What gates delivery
Section titled “What gates delivery”A rates.changed event reaching your receiver requires all three:
- Your org is on Hobby or Pro (re-checked at fire time, so a downgrade made one second before dispatch prevents delivery without touching the endpoint row).
- The endpoint is enabled in the dashboard.
rates.changedis in the endpoint’s subscribed events set.
The same gating applies to every event. The plan re-check is the “retain but pause” contract — your endpoints survive a downgrade and resume the moment you upgrade back; no re-setup needed.
Pointers
Section titled “Pointers”- Event payload reference — the per-event
datashape is documented per-endpoint where the event is most relevant. Forrates.changed, see/changes— samechangesarray. - Setup walk-through and edit UI — in the dashboard at Webhooks.
- Signing secret rotation — dashboard’s webhook detail page.