StrataDocs

Webhooks

Strata delivers signed HTTPS callbacks when key-scoped events occur. You subscribe per API key, receive a JSON POST at your URL, and verify the HMAC signature before trusting the payload.

Subscribing

Open Admin → API Keys, click a key to open its drill-down, and under Webhooks click Add webhook. Each subscription has one event, one delivery URL, and its own signing secret.

The signing secret (prefixed whsec_) is shown once on creation — store it immediately. You verify the Strata-Signature header against it. If you lose it, delete the subscription and add a new one.

A single key can have multiple subscriptions. Subscriptions are isolated: revoking a key disables its webhooks, but other keys keep delivering.

Warning

Delivery URLs must be public HTTPS endpoints. Strata blocks private, loopback, link-local, and internal-only targets at both subscribe time and delivery time, and never follows redirects.

Event vocabulary

Only events that have a real server-side emitter are offered when you subscribe. The vocabulary is intentionally small and grows as the platform surface grows.

EventWhen it fires
key.revokedThe API key is revoked by an admin or by automated rotation.
Note

New events are additive and will not change the delivery envelope. Watch the changelog before depending on a wider set.

Delivery shape

Deliveries are POST application/json to your subscription URL with a uniform envelope:

{
  "event_id": "9b6a1c2e-4b1d-4f3e-9a77-9b3a0e0d8e7b",
  "event_type": "key.revoked",
  "timestamp": 1717592528,
  "data": {
    "key_id": "8f4e1d72-2c8e-4a90-b06b-1e9c2a4dd2f1",
    "prefix": "sk_live_ab12cd",
    "name": "Reporting pipeline",
    "revoked_at": "2026-06-24T14:22:08.000Z",
    "reason": "rotated",
    "grace_period_ends_at": "2026-06-25T14:22:08.000Z"
  }
}
FieldTypeDescription
event_idstring (uuid)Unique delivery ID. Use it for idempotent processing — retries reuse the same event_id.
event_typestringEvent name from the vocabulary above.
timestampnumberUnix epoch seconds when the event was signed. Also sent as the Strata-Timestamp header.
dataobjectEvent-specific payload. Schema depends on event_type.

key.revoked data fields

FieldTypeDescription
key_idstring (uuid)ID of the revoked key.
prefixstringPublic prefix of the token (for logs and audit trails).
namestringThe key's display name.
revoked_atstring (ISO 8601, UTC)Server-side revocation timestamp.
reasonstring (nullable)Free-text reason when supplied by the admin or rotation job; null otherwise.
grace_period_ends_atstring (ISO 8601, UTC, nullable)When the revoked key stops verifying. During rotation both old and new keys verify until this time, so you can swap credentials without downtime.

Headers

Every delivery carries these headers:

HeaderValue
Strata-EventThe event type, e.g. key.revoked.
Strata-TimestampUnix epoch seconds the request was signed at. Matches the body's timestamp.
Strata-Event-IdThe delivery's event_id (also in the body).
Strata-Signaturesha256=<hex> — the HMAC of the signed payload (see below).
User-AgentStrata-Webhooks/1.0 (retries append (retry)).

The signature is computed as:

Strata-Signature: sha256=<hex>
where <hex> = HMAC-SHA256(secret, `${Strata-Timestamp}.${rawBody}`)

Always compute the HMAC over the raw request body bytes — not a re-serialized JSON object — and compare using a constant-time comparison.

Alert

If a delivery arrives with a Strata-Signature-Missing header instead of Strata-Signature, Strata could not load the signing secret for that subscription (a legacy row that predates secret storage). Treat it as unverifiable and reject it — delete and re-create the subscription to restore signing.

Replay protection

Reject any delivery where Strata-Timestamp is more than five minutes from your server's clock. Strata signs over that same timestamp, so a fresh delivery always falls inside the window. This shuts down captured-payload replay attacks even if an attacker learns a valid signature.

Alert

Verify BOTH the signature AND the timestamp. A valid signature on a stale timestamp is still a replay attempt. Reject and log it.

Retry contract

ResponseBehavior
2xxDelivery succeeds. No retry.
Non-2xx or timeout (>10s)Retried with backoffs of 1 min, 5 min, 30 min, 2 hr, 12 hr (up to 6 attempts total, including the first).

Retries replay the identical request — same event_id, same Strata-Timestamp, same body and signature — so dedupe on event_id.

After repeated consecutive failures the subscription is auto-disabled and surfaced in the Admin UI with its last delivery error, so you can fix the endpoint and re-create the subscription.

Return 200 as soon as you have persisted the event_id. Do the slow work asynchronously — long handlers cause timeouts, which cost you retries and surface as duplicate deliveries.

Verifying in Node.js

const crypto = require('crypto');

function verifyStrataSignature(rawBody, timestampHeader, signatureHeader, secret) {
  const ts = Number(timestampHeader);
  if (!ts) return false;

  // Reject if the timestamp drifts beyond 5 minutes.
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;

  // Header looks like "sha256=<hex>".
  const [version, sig] = String(signatureHeader || '').split('=');
  if (version !== 'sha256' || !sig) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(sig, 'hex');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

Wire it into Express with a raw body parser so the bytes you sign over are the bytes you received:

app.post(
  '/webhooks/strata',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ok = verifyStrataSignature(
      req.body.toString('utf8'),
      req.header('Strata-Timestamp') || '',
      req.header('Strata-Signature') || '',
      process.env.STRATA_WEBHOOK_SECRET
    );
    if (!ok) return res.status(401).end();
    // Persist req.body keyed by Strata-Event-Id, return 200, process async.
    res.status(200).end();
  }
);

Verifying in Python

import hmac
import hashlib
import time

def verify_strata_signature(raw_body: bytes, timestamp_header: str,
                            signature_header: str, secret: str) -> bool:
    try:
        ts = int(timestamp_header)
    except (TypeError, ValueError):
        return False

    if abs(time.time() - ts) > 300:
        return False

    version, _, sig = (signature_header or "").partition("=")
    if version != "sha256" or not sig:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{ts}.".encode("utf-8") + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, sig)

In Flask, read request.get_data() to obtain the raw bytes before any JSON parsing.

Related