Webhooks
Subscribe your own systems to Flash events. Register an HTTPS endpoint for the events you care about, and Flash delivers a signed, thin payload the moment each one fires — no polling, no nightly export. Signatures follow the Standard Webhooks spec, deliveries retry with backoff, and every attempt is logged.
Register an endpoint
Create a webhook endpoint with a Bearer fl_live_ key that holds the webhooks:manage scope. Post an HTTPS url and the list of eventTypes you want delivered there. The signing secret (whsec_…) is returned once in the response and never again — store it securely; you need it to verify every delivery.
curl -X POST https://flash.socialhub.ai/api/v2/webhooks \
-H "Authorization: Bearer fl_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.your-app.com/flash",
"description": "prod ingest",
"eventTypes": ["member.created", "loyalty.tier_upgraded"]
}'
# 201 Created
# {
# "data": {
# "id": "whe_...",
# "url": "https://hooks.your-app.com/flash",
# "eventTypes": ["member.created", "loyalty.tier_upgraded"],
# "status": "active",
# "secret": "whsec_..." <-- shown ONCE, store it now
# }
# }Manage endpoints later with GET /api/v2/webhooks (list, no secrets), PATCH /api/v2/webhooks/{id} (change url / events / status, or send { "action": "rotate_secret" } to roll the secret), and DELETE /api/v2/webhooks/{id}. You can also register, test-ping and replay deliveries from Settings → Webhooks.
The event catalog
Ask the API which events you can subscribe to. The catalog is derived from the live automation-trigger source of truth, so it lists only events that actually fire today — grouped by lifecycle stage (acquisition, engagement, loyalty, retention, commerce, advocacy) — each with its event key, a description, and an example payload. The response also documents the shared payload envelope and the signature headers.
curl https://flash.socialhub.ai/api/v2/events/catalog \
-H "Authorization: Bearer fl_live_..."
# { "data": { "events": [
# { "event": "member.created", "label": "Member created",
# "stage": "acquisition", "examplePayload": { ... } },
# { "event": "loyalty.tier_upgraded", "label": "Tier upgraded",
# "stage": "loyalty", "examplePayload": { ... } },
# ...
# ],
# "payload": { "schema": { ... }, "headers": { ... } }
# } }Put an event's event key straight into the eventTypes array when you subscribe. Requires the webhooks:manage scope.
A thin, non-PII payload
Every delivery carries the same small envelope: a stable id, the event type, a created_at timestamp, and a compact data block. The payload never contains personal data— no email, name, phone or address. When you need the member's details, fetch them back over the authenticated API using member_ref. This keeps PII on the member record and off your webhook logs.
POST https://hooks.your-app.com/flash
webhook-id: evt_a1b2c3... # stable across retries — dedupe on this
webhook-timestamp: 1767225600 # unix seconds
webhook-signature: v1,K5x0mF...== # base64 HMAC-SHA256 (may be space-separated during rotation)
Content-Type: application/json
{
"id": "evt_a1b2c3...",
"type": "loyalty.tier_upgraded",
"created_at": "2026-01-01T00:00:00.000Z",
"data": {
"resource_id": "mbr_...",
"member_ref": "mbr_...", // opaque member id — fetch PII via the API
"tier": "Gold", // present on loyalty / tier events
"amount_band": "100-500" // coarse band on commerce events — never the exact amount
}
}Verify every signature
Flash signs each delivery with your endpoint's whsec_ secret using Standard Webhooks (HMAC-SHA256). Recompute the signature over {webhook-id}.{webhook-timestamp}.{body} and compare it, in constant time, against the webhook-signatureheader. Reject anything that doesn't match, and reject a webhook-timestampthat's too old to defend against replays. The signing key is the base64-decoded bytes of the secret after the whsec_ prefix, per the spec. During a secret rotation, the header carries multiple space-separated v1,… values — accept the delivery if any match.
import crypto from "node:crypto";
// req.body must be the RAW, unparsed request body string.
function verifyFlashWebhook(headers, rawBody, whsec) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sigHeader = headers["webhook-signature"]; // "v1,<b64> v1,<b64> ..."
// reject stale deliveries (5 min tolerance)
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(whsec.replace(/^whsec_/, ""), "base64");
const expected = crypto
.createHmac("sha256", key)
.update(`${id}.${ts}.${rawBody}`)
.digest("base64");
return sigHeader
.split(" ")
.map((p) => p.split(",")[1])
.some((sig) =>
sig &&
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
);
}Retries, idempotency & the delivery log
Return 2xx quickly
Acknowledge with any 2xx and do your work asynchronously. A 429, a 5xx, a timeout or a connection error is treated as retryable.
Automatic retry with backoff
Retryable failures are retried with exponential, full-jitter backoff. A 3xx or a non-429 4xx is treated as a permanent failure and not retried.
Dedupe by webhook-id
The id is identical across retries, so at-least-once delivery is safe: treat a repeated webhook-id as already handled.
Delivery log & replay
Every attempt is recorded. If retries are exhausted the delivery is marked dead; you can inspect and replay deliveries from Settings → Webhooks.