title: Webhooks description: Receive signed, real-time notifications when events happen.

Webhooks

When a workspace enables an outbound webhook, Uniforms delivers a signed HTTP POST to the configured URL for each event (for example, a new submission). Each request is cryptographically signed so you can prove it came from us and reject replays.

Verify the signature

Every webhook includes:

  • X-Uniforms-Signature: t=<unix-seconds>,v1=<hex-hmac>.
  • A raw JSON body.

To verify:

  1. Split the header into t (timestamp) and v1 (signature).
  2. Reject if t is older than your replay window (e.g. 5 minutes).
  3. Compute HMAC-SHA256(per-workspace-secret, "${t}.${rawBody}") and compare it to v1 using a constant-time comparison.
import crypto from "node:crypto";

function verify(payload, header, secret, maxAgeSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((s) => s.split("=")));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > maxAgeSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${payload}`)
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Event payload

{
  "type": "submission.created",
  "workspaceId": "ws_...",
  "formId": "frm_...",
  "submissionId": "sub_...",
  "createdAt": "2026-06-22T12:00:00.000Z"
}

Webhooks carry references only — never secrets or full submission content. Fetch the full resource through the authenticated API if you need it.

Delivery guarantees

  • At-least-once: deliver through a queue with bounded retries and backoff.
  • Idempotent handling: a delivery may be retried; dedupe by event id.
  • Short timeout: deliveries time out quickly; respond 2xx fast and process asynchronously.

Responding

Return any 2xx status to acknowledge. Anything else triggers a retry. Always verify the signature before acting on the payload.

Destination safety

Destination URLs are validated for SSRF before sending: private, loopback, and link-local ranges and the cloud metadata IP are blocked, only https is allowed, and redirects to internal hosts are refused.