Guides › Receive webhooks

Guide

Receive webhooks

Webhook endpoints receive signed POST requests for subscribed event types. Use HTTPS endpoints, verify the signature over the raw request body, and make handlers idempotent.

List event types

curl "https://api.ctct.dev/v1/webhook-event-types" \
  -H "Authorization: Bearer <API_KEY>"

Create an endpoint

POST /v1/webhook-endpoints requires a URL and at least one event type. The create response returns the cleartext signing secret once. Store it securely.

curl -X POST "https://api.ctct.dev/v1/webhook-endpoints" \
  -H "Authorization: Bearer <API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/ctct",
    "event_types": ["email.sent", "email.delivered", "domain.updated"]
  }'
{
  "webhook_endpoint": {
    "webhook_endpoint_id": "we_...",
    "url": "https://example.com/webhooks/ctct",
    "status": "active",
    "event_types": ["email.sent", "email.delivered", "domain.updated"]
  },
  "secret": "whsec_..."
}

Delivery headers

HeaderDescription
X-CTCT-SignatureHMAC-SHA256 signature in the form sha256=<hex>, calculated over the exact raw request body.
X-CTCT-Delivery-IdUnique webhook delivery attempt lineage. Also sent as Idempotency-Key.
X-CTCT-Subscription-IdSubscription that matched the event.
X-CTCT-Event-IdEvent identifier for the delivered event.
X-CTCT-AttemptAttempt number for this delivery.

Verify signatures

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyCtctWebhook(rawBody, signatureHeader, secret) {
  if (!signatureHeader?.startsWith("sha256=")) return false;
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  const received = Buffer.from(signatureHeader);
  const calculated = Buffer.from(expected);
  return received.length === calculated.length && timingSafeEqual(received, calculated);
}

Manage deliveries

  • Use GET /v1/webhook-events?webhook_endpoint_id=<WEBHOOK_ENDPOINT_ID> to list endpoint-scoped deliveries.
  • Use POST /v1/webhook-events/{event_id}:redeliver to request a retry for a failed delivery.
  • Use POST /v1/webhook-endpoints/{webhook_endpoint_id}:rotate-secret to rotate the signing secret. The rotated secret is returned once.
  • Use DELETE /v1/webhook-endpoints/{webhook_endpoint_id} to archive an endpoint.
Security: use public HTTPS callback URLs. Do not use private IP addresses or laptop/VPN-only addresses for production webhook endpoints.