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
| Header | Description |
|---|---|
| X-CTCT-Signature | HMAC-SHA256 signature in the form sha256=<hex>, calculated over the exact raw request body. |
| X-CTCT-Delivery-Id | Unique webhook delivery attempt lineage. Also sent as Idempotency-Key. |
| X-CTCT-Subscription-Id | Subscription that matched the event. |
| X-CTCT-Event-Id | Event identifier for the delivered event. |
| X-CTCT-Attempt | Attempt 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}:redeliverto request a retry for a failed delivery. - Use
POST /v1/webhook-endpoints/{webhook_endpoint_id}:rotate-secretto 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.