Docs
Webhooks & API

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

Go to Webhooks & API → Webhooks and select your workspace.
Click Add webhook.
Enter a Name (an internal label — it's not sent anywhere) and the Endpoint URL — the HTTPS destination on your server that will receive the POST.
Tick the events you want to subscribe to. Checkboxes are grouped by resource, each group has its own "select all," and a global All/None toggle sits at the top.
Click Create webhook.
Copy the Signing Secret shown next. The confirmation reads "This is the only time it will be shown in full. You can reveal or rotate it later from the edit sheet" — and that second part is true, so don't panic if you lose it, but copying it now saves a trip back later.

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..."
    }
  }
}
FieldDescription
idUnique event delivery identifier starting with evt_.
eventThe event key that triggered the delivery (e.g. form.submission.created).
createdISO-8601 timestamp representing when the event occurred.
apiVersionPlatform API version (currently 2026-05-01).
workspaceIdThe ID of the workspace scope.
siteIdThe ID of the site scope (can be null if workspace-scoped).
data.objectThe 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 string t.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:

HeaderDescription
X-TDC-EventThe event key — the same value as the payload's event field.
X-TDC-DeliveryA 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 test delivery 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):

AttemptTiming
1st attemptImmediate / 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 2xx immediately 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 in data to de-duplicate.
  • Keep the endpoint private. Treat the URL as a secret and accept only HTTPS.

  • 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.