Webhooks
Driftstack delivers event notifications to your HTTPS endpoint as they happen. Use webhooks to react to session completion, quota events, and key revocations without polling.
Event types
| Event | When it fires |
|---|---|
session.completed | Fires when a session is destroyed cleanly (non-error end of life). Payload carries { session_id, duration_ms }. |
session.failed | Fires when a session ends in an errored state. |
quota.warning_80pct | Fires when an account hits 80% of a tier-metered quota. |
quota.exceeded | Fires when an account exceeds a tier-metered quota. |
api_key.revoked | Fires when an API key is revoked from the dashboard. |
session.egress_capability_changed | Arc 5 EGRESS eg.7.e — fires when a session's egress capability state changes (e.g. proxy connectivity verified or lost). Lets subscribers react to capability transitions without polling. |
crypto.order.paid | V-666 — fires when a crypto order transitions to the terminal paid state (NowPayments IPN settled). Subscribe to grant entitlements server-side without polling /v1/billing/crypto-orders. |
crypto.order.failed | V-666 — fires when a crypto order transitions to the terminal failed state (address-window expired, NowPayments failed/refunded). Subscribe to drive support workflows on settlement-failure. |
session.challenge_detected | W393 — fires when the in-session harness flags a bot-check (DataDome / Arkose / PerimeterX / AWS-WAF / GeeTest / …). The session auto-pauses; resolve the challenge and it resumes. Subscribe to route challenge alerts into your own ops surface. |
session.profile_save_failed | Fires when a profile-backed session could not persist its updated profile store at teardown (the session itself succeeded). Terminal — the next restore of that profile will be stale. Subscribe if you rely on persisted profile state. |
test.ping is an additional event type that exists
only for the POST /v1/webhooks/<id>/test endpoint;
it bypasses subscriptions, so you don't list it under
events when creating an endpoint.
Each account can have up to 10 active endpoints. Mint as many narrow-purpose endpoints as you need rather than one mega-endpoint that fans out — easier to retire individual integrations later.
Creating an endpoint
POST /v1/webhooks
Authorization: Bearer ds_live_…
Content-Type: application/json
{
"url": "https://yourapp.com/driftstack-webhook",
"events": ["session.completed", "session.failed"],
"description": "Production listener"
}
→ 201 Created
{
"id": "whk_…",
"secret": "whsec_…", ← shown ONCE; copy it now
"secret_prefix": "whsec_xxxx",
"url": "https://yourapp.com/driftstack-webhook",
"events": ["session.completed", "session.failed"],
"description": "Production listener",
"active": true,
"consecutive_failures": 0,
"delivery_counts": { "pending": 0, "delivered": 0, "failed": 0, "dlq": 0 },
"created_at": "2026-05-12T12:00:00.000Z",
…
} Important: the secret is shown
once in the create response and never again. Store it in your
config / secret manager. If you lose it, rotate via
POST /v1/webhooks/<id>/rotate-secret.
Subsequent GET /v1/webhooks/<id> responses
include only secret_prefix (first 12 plaintext
chars, non-sensitive) for display.
Endpoint URLs MUST be HTTPS. http:// is rejected
at registration time.
Delivery payload
Every event arrives as a JSON POST to your endpoint:
POST /driftstack-webhook
Content-Type: application/json
X-Driftstack-Event-Id: evt_…
X-Driftstack-Event-Type: session.completed
X-Driftstack-Emitted-At: 1747051200
X-Driftstack-Signature: t=1747051200,v1=<hex hmac>
{
"id": "evt_…",
"type": "session.completed",
"created_at": "2026-05-12T12:00:00.000Z",
"data": { /* event-specific payload */ }
}
During a secret-rotation grace window the single
X-Driftstack-Signature header carries both the new
and old HMACs as two comma-separated v1= entries
(t=…,v1=<new>,v1=<old>) — see
Secret rotation below.
Signature verification
Always verify the signature before trusting the payload — otherwise anyone who guesses your endpoint URL can spoof events.
The header is Stripe-style:
X-Driftstack-Signature: t=<unix-seconds>,v1=<hex hmac>.
The HMAC is computed as:
hmac = HMAC-SHA256(
secret,
<unix-seconds> + "." + <raw request body>
)
The timestamp lives inside the header (the t=
component), not in a separate header. The
X-Driftstack-Emitted-At header is informational
and must NOT be used as the signed-payload timestamp.
Server-side verification in Node.js looks like:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(req, secret) {
const header = req.headers['x-driftstack-signature']; // 't=…,v1=…'
const body = req.rawBody; // RAW bytes — NOT parsed JSON
if (typeof header !== 'string' || !body) return false;
const parts = Object.fromEntries(
header.split(',').map((p) => {
const i = p.indexOf('=');
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}),
);
const ts = Number(parts.t);
const sig = parts.v1;
if (!ts || !sig) return false;
// Reject replays — Driftstack signs with the current timestamp;
// anything older than 5 minutes is almost certainly a replay.
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${ts}.${body}`)
.digest('hex');
const a = Buffer.from(sig, 'hex');
const b = Buffer.from(expected, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
} Or use the SDK helpers — they handle the parsing, replay window, and previous-secret grace path for you:
- TypeScript / JS:
import { verifyWebhookSignature } from '@driftstack/sdk' - Python:
from driftstack import verify_webhook_signature - Go:
driftstack.VerifyWebhookSignature(...)
Retry policy
Driftstack retries non-2xx responses (and timeouts) with backoff. The schedule is fixed:
| Attempt | Delay before retry |
|---|---|
| 1 (initial) | — |
| 2 | 1 minute after attempt 1 |
| 3 | 5 minutes after attempt 2 |
| 4 | 15 minutes after attempt 3 |
| 5 | 30 minutes after attempt 4 |
| 6 (final) | 60 minutes after attempt 5 |
- After 6 attempts (the initial delivery plus 5 retries), the delivery moves to a dead-letter queue (DLQ). You can requeue from the dashboard or via the admin API.
- After 50 consecutive failures across all deliveries for one endpoint, the endpoint is auto-disabled. You'll see an email + a banner in the dashboard. Re-enable from the dashboard once the receiving service is healthy.
- Request timeout is 10 seconds — slower than that, we consider the delivery failed and back off.
Your endpoint should:
- Return 2xx within 10 seconds.
- Be idempotent — we may retry the same
X-Driftstack-Event-Idif our delivery infra retried before getting your response. - Acknowledge fast + process asynchronously. A 200 response means "I have the payload"; process it on your side after.
Secret rotation
When you suspect a leaked secret (or just for routine hygiene), rotate it:
POST /v1/webhooks/<id>/rotate-secret
Authorization: Bearer ds_live_…
→ 200 OK
{
"id": "whk_…",
"secret": "whsec_NEW…", ← shown ONCE; copy it now
"secret_prefix": "whsec_yyyy",
"prev_secret_prefix": "whsec_xxxx",
"grace_expires_at": "2026-05-13T12:00:00.000Z"
}
During the 24-hour grace window, Driftstack signs every
outbound delivery with both secrets inside the single
X-Driftstack-Signature header, as two
comma-separated v1= entries:
v1=<new>— signed with the new secret.v1=<old>— signed with the old secret.
X-Driftstack-Signature: t=…,v1=<new>,v1=<old>
Your verifier should accept the delivery if either HMAC
matches. The SDK helpers do this automatically — they iterate
every v1= entry in the one header, so no extra
argument is needed. Roll the new secret across your infra at
your own pace during the grace window — when it expires, the
old v1= entry drops and only the new-secret
signature is sent.
Testing
Use the test endpoint to fire a synthetic
test.ping event at any active endpoint, bypassing
subscription:
POST /v1/webhooks/<id>/test
Authorization: Bearer ds_live_…
→ 202 Accepted
{
"delivery_id": "wdl_…",
"event_id": "evt_…",
"event_type": "test.ping"
} The test event arrives at your endpoint with the same headers + signature as a real event. Your endpoint's response is captured so you can debug from the dashboard.
Inspecting deliveries
Per-endpoint delivery log + counts available from the dashboard
under Webhooks → <endpoint> → Deliveries,
or from GET /v1/webhooks/<id>/deliveries.
Failed deliveries surface the response status + first 200 bytes
of your endpoint's response body so you can debug.
What to do when something goes wrong
- Signatures don't verify — confirm you're
hashing the raw request body, not the parsed JSON.
Most frameworks parse the body by default; you need a raw-body
middleware ahead of the JSON parser. Also confirm you're
reading the timestamp from inside
X-Driftstack-Signature(thet=component), not from a separate header. - Endpoint auto-disabled — fix the receiving service, then re-enable via the dashboard. Past deliveries in DLQ can be requeued.
- Duplicate events — expected. Dedupe on
X-Driftstack-Event-Id. - Missing events — check the endpoint registration includes the event type you're expecting; events only fire to endpoints subscribed to that type.
Support
Questions or trouble integrating? developers@driftstack.dev.