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.
WarningDelivery 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.
| Event | When it fires |
|---|---|
key.revoked | The API key is revoked by an admin or by automated rotation. |
NoteNew 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"
}
}
| Field | Type | Description |
|---|---|---|
event_id | string (uuid) | Unique delivery ID. Use it for idempotent processing — retries reuse the same event_id. |
event_type | string | Event name from the vocabulary above. |
timestamp | number | Unix epoch seconds when the event was signed. Also sent as the Strata-Timestamp header. |
data | object | Event-specific payload. Schema depends on event_type. |
key.revoked data fields
| Field | Type | Description |
|---|---|---|
key_id | string (uuid) | ID of the revoked key. |
prefix | string | Public prefix of the token (for logs and audit trails). |
name | string | The key's display name. |
revoked_at | string (ISO 8601, UTC) | Server-side revocation timestamp. |
reason | string (nullable) | Free-text reason when supplied by the admin or rotation job; null otherwise. |
grace_period_ends_at | string (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:
| Header | Value |
|---|---|
Strata-Event | The event type, e.g. key.revoked. |
Strata-Timestamp | Unix epoch seconds the request was signed at. Matches the body's timestamp. |
Strata-Event-Id | The delivery's event_id (also in the body). |
Strata-Signature | sha256=<hex> — the HMAC of the signed payload (see below). |
User-Agent | Strata-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.
AlertIf a delivery arrives with a
Strata-Signature-Missingheader instead ofStrata-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.
AlertVerify BOTH the signature AND the timestamp. A valid signature on a stale timestamp is still a replay attempt. Reject and log it.
Retry contract
| Response | Behavior |
|---|---|
2xx | Delivery 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.