Skip to main content
DRIFTSTACK

Webhooks

Driftstack delivers event notifications to your HTTPS endpoint as they happen. Use webhooks to react to session lifecycle, egress, challenge, profile-save, payment, and key-revocation events without polling.

Event types

EventWhen it fires
session.completedFires once per logical destroy of a non-terminal session — whether you called DELETE /v1/sessions/:id, the free-tier duration cap expired it, or your account was suspended and its sessions reclaimed. Payload carries { session_id, duration_ms }, plus { auto_destroyed: true, reason } on the two automatic cases. Branch on auto_destroyed if you attribute completions.
session.failedFires when a session ends in an errored state.
api_key.revokedFires when an API key is revoked from the dashboard.
session.egress_capability_changedFires each time the control plane ingests an egress capability report from the session harness — on every report, not only on a change (there is no change detection, so identical consecutive reports each emit an event). Compare against your last stored state rather than treating it as a transition. `warnings` also carries streaming faults, so it can fire when only the video stream degraded.
crypto.order.paidFires 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.failedFires 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_detectedFires 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: <uuid>
X-Driftstack-Event-Type: session.completed
X-Driftstack-Signature: t=1747051200,v1=<hex hmac>

{
  "id": "<uuid>",
  "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. There is no X-Driftstack-Emitted-At header: the delivery path sends exactly three X-Driftstack-* headers — Event-Id, Event-Type and Signature.

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": "<uuid>",
  "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? [email protected].