Skip to main content

Webhooks

Webhooks notify external systems when submissions are created or updated.

Webhook Configuration

CREATE TABLE webhook_configs (
  id UUID PRIMARY KEY,
  form_id UUID REFERENCES forms(id),
  name TEXT NOT NULL,
  url TEXT NOT NULL,
  events TEXT[] DEFAULT ARRAY['submission.created'],
  secret TEXT NOT NULL,  -- HMAC signing key
  headers JSONB DEFAULT '{}',
  is_active BOOLEAN DEFAULT true
);

Events

EventTrigger
submission.createdNew submission
submission.updatedSubmission modified
status.changedStatus updated

HMAC Signing

Every webhook is signed with HMAC-SHA256:
function signPayload(payload: string, secret: string): string {
  return crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
}

// Headers sent:
// X-Webhook-Signature: sha256=abc123...
// X-Webhook-Timestamp: 1704000000

Verification (Receiver)

function verifyWebhook(
  payload: string,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  // Check timestamp isn't too old (prevent replay)
  const age = Date.now() / 1000 - parseInt(timestamp);
  if (age > 300) return false; // 5 minute window

  // Verify signature
  const expected = signPayload(`${timestamp}.${payload}`, secret);
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

Retry Strategy

Webhooks use the async queue with 5 retries and exponential backoff.