Webhooks
Receive real-time HTTP notifications when events happen in your workspace — setup, signature verification, retries, and security.
Webhooks push real-time JSON payloads to your server the moment an event occurs in your workspace — a form submission, a new blog post, a CMS update, and more. Use them to trigger automations, sync to third-party systems, or run background jobs without polling the API.
Configure webhooks under Webhooks & API → Webhooks in the dashboard.
Webhooks require a plan that includes the Webhooks & API feature. See Feature Access. For the complete list of events you can subscribe to — and which ones actually fire today — see Event Types.
Creating a Webhook
POST.Your endpoint must be served over HTTPS and respond with a 2xx status quickly. Non-2xx responses and timeouts are treated as failures and retried (see Retry Behavior).
Each workspace can hold up to 10 webhooks. The underlying record also supports an optional formId to scope a webhook to a single form instead of a whole site or workspace, though the dashboard's creation sheet doesn't expose a way to set that today.
Payload Structure
Every event is delivered as a JSON POST with a consistent envelope. The fields are nested inside data.object, varying by event — see Event Types for per-event shapes.
{
"id": "evt_12345678-abcd-1234-abcd-1234567890ab",
"event": "form.submission.created",
"created": "2026-06-06T12:00:00.000Z",
"apiVersion": "2026-05-01",
"workspaceId": "ws_abc123",
"siteId": "site_xyz456",
"data": {
"object": {
"...event-specific fields..."
}
}
}| Field | Description |
|---|---|
id | Unique event delivery identifier starting with evt_. |
event | The event key that triggered the delivery (e.g. form.submission.created). |
created | ISO-8601 timestamp representing when the event occurred. |
apiVersion | Platform API version (currently 2026-05-01). |
workspaceId | The ID of the workspace scope. |
siteId | The ID of the site scope (can be null if workspace-scoped). |
data.object | The event-specific payload object. |
Verifying Signatures
Every delivery includes an X-TDC-Signature header so you can confirm it genuinely came from Blox and was not tampered with or replayed.
X-TDC-Signature: t=1716801000,v1=sha256=<hex_hmac>t— the Unix timestamp (seconds) when delivery was attempted.v1— an HMAC-SHA256 signature of the stringt.raw_request_body, keyed with your webhook's Signing Secret.
To verify: recompute the HMAC over ${t}.${rawBody} using your secret, compare it to v1 with a constant-time comparison, and reject anything older than ~5 minutes to prevent replay.
import crypto from 'crypto';
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const t = signatureHeader.match(/t=(\d+)/)?.[1];
const receivedSig = signatureHeader.match(/v1=sha256=([a-f0-9]+)/)?.[1];
if (!t || !receivedSig) return false;
// Reject payloads older than 5 minutes (replay protection)
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
// Constant-time comparison (prevents timing attacks)
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(receivedSig, 'hex'),
);
}Verify against the raw, unparsed request body. If your framework parses JSON before you can read the raw bytes, the recomputed signature won't match. Capture the raw body first.
Two more headers travel with every delivery, useful for routing or de-duplication without touching the body:
| Header | Description |
|---|---|
X-TDC-Event | The event key — the same value as the payload's event field. |
X-TDC-Delivery | A unique ID for this specific delivery attempt. |
Every request also identifies itself with User-Agent: TDC-Webhooks/1.0.
Managing Secrets
Under Webhooks & API → Webhooks, click Edit next to a webhook:
- Reveal secret — shows the current Signing Secret in full, any time.
- Rotate secret — generates a new signing key and invalidates the old one immediately. Update your server's stored secret the moment you rotate.
Unlike your personal API key (which is hashed), a webhook's Signing Secret is stored so it can be revealed on demand later. Rotate a webhook's secret immediately if you suspect its endpoint or stored value has been exposed.
Deliveries Log
Click Deliveries next to a webhook to inspect its history:
- Each entry shows the status, HTTP response code (e.g.
200,500), duration (ms), and the request/response payloads. - Replay — re-send the exact stored payload of a past delivery (useful after fixing server downtime).
- Test — an icon-only send button next to the webhook (tooltip: "Send test payload") that fires a real, synthetic
testdelivery at your endpoint — not one of the events in the catalog — and logs it here like any other delivery, so you can confirm your endpoint and verification logic work.
Retry Behavior
If your endpoint returns a non-2xx status or times out, delivery is retried automatically up to 3 total attempts (1 original attempt + up to 2 retries):
| Attempt | Timing |
|---|---|
| 1st attempt | Immediate / Initial try |
| 2nd attempt (1st retry) | 1 minute after initial try |
| 3rd attempt (2nd retry) | 5 minutes after 1st retry |
Retries are scheduled with an in-process timer, not a durable queue. If the server restarts between a delivery's attempts (for example, during a deploy), a pending retry is silently dropped — the log shows whichever attempts already ran, but no further retry follows.
If a webhook reaches 5 consecutive failures, it's marked "unhealthy" in the dashboard as an early warning. At 10 consecutive failures, the system automatically disables it (isActive set to false) to prevent unnecessary load on your server. Re-enabling a webhook from the dashboard resets its failure count to zero.
Security Recommendations
- Always verify the signature before trusting a payload — never act on an unverified request.
- Enforce the 5-minute timestamp window to block replayed deliveries.
- Respond fast, process async. Acknowledge with
2xximmediately and do heavy work in a background job, so you don't trigger timeout retries. - Make handlers idempotent. Retries (and manual replays) mean the same event may arrive more than once — key on
event+ a stable ID indatato de-duplicate. - Keep the endpoint private. Treat the URL as a secret and accept only HTTPS.
Related
- Event Types — Every event you can subscribe to, which ones actually fire, and their payloads.
- API Reference — Manage webhooks programmatically via REST.
- Authentication — How your Secret Key authenticates every request.
