Skip to main content
DRIFTSTACK

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

EventWhen it fires
session.completedFires when a session is destroyed cleanly (non-error end of life). Payload carries { session_id, duration_ms }.
session.failedFires when a session ends in an errored state.
quota.warning_80pctFires when an account hits 80% of a tier-metered quota.
quota.exceededFires when an account exceeds a tier-metered quota.
api_key.revokedFires when an API key is revoked from the dashboard.
session.egress_capability_changedArc 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.paidV-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.failedV-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_detectedW393 — 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_failedFires 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:

Retry policy

Driftstack retries non-2xx responses (and timeouts) with backoff. The schedule is fixed:

AttemptDelay before retry
1 (initial)
21 minute after attempt 1
35 minutes after attempt 2
415 minutes after attempt 3
530 minutes after attempt 4
6 (final)60 minutes after attempt 5

Your endpoint should:

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:

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

Support

Questions or trouble integrating? developers@driftstack.dev.