Skip to main content
Webhooks push events to an HTTPS endpoint you control, so you can react to a reply the moment it lands instead of polling for it.
Webhook subscriptions are workspace-scoped, unlike the contact directory which is team-scoped. A subscription only receives events from the workspace of the API key that created it.

Quick Start

The signing secret appears only in the create response. It is not returned by GET /webhooks, GET /webhooks/{id} or PATCH, and there is no rotation endpoint. Store it in your secret manager immediately — the only way to get a new one is to delete the subscription and create another.

Creating a Subscription

POST /api/public/v1/webhooks — scope webhooks:create, returns 201
That is the complete list of event types that currently deliver. The API accepts six more, and none of them fire — see the status table in Event Types.
Not every accepted event type is delivered. The subscription validator is more permissive than the pipeline behind it: campaign_completed and campaign_started are never emitted, and email_queued, email_send_failed, inbox_paused and domain_limit_reached are emitted internally but dropped before delivery. A subscription naming them is created with a 201 and then stays silent. In particular, event_categories: ["system"] matches only non-deliverable types, so it receives nothing at all.
An HTTP target_url is rejected with a 422, as is an unknown event type or category. Changes take effect immediately — the API drops the dispatcher’s subscription cache on every create, update and delete.

How filters combine

The three filters are applied in order, and all must pass:
  1. Workspace — the event’s workspace must equal the subscription’s. This is not configurable.
  2. event_types — the event type must be listed. An empty array delivers nothing, not everything.
  3. campaign_id — if set, events with a different campaign ID (or none at all) are dropped.
  4. event_categories — if set, the event type must belong to at least one listed category.
event_types and event_categories are an AND, not an OR. Listing event_types: ["reply_received"] alongside event_categories: ["delivery"] delivers nothing, because reply_received is not in the delivery category. Use one or the other.
PATCH with {"event_types": []} narrows the subscription to zero events — it does not clear the filter. event_types is non-nullable, so there is no “all types” setting: name the types you want.

Payload Format

Every delivery is a POST with a JSON body:
The five envelope fields are the same for every event; only data varies. Field-by-field shapes are in Event Types. campaign_id is null for events that are not about a campaign.

Headers

Verifying Signatures

Verify the signature on every delivery. Compare against the raw request body — parsing and re-serialising the JSON will not match.
Use a constant-time comparison — hmac.compare_digest or crypto.timingSafeEqual. A plain == leaks the signature byte by byte through timing.

Delivery Semantics

Each attempt has a 10 second timeout. Retryable failures are re-attempted up to 3 times, at 1 minute, 5 minutes and 30 minutes — 4 attempts in total. After that the delivery is marked dead and nothing further happens automatically.
A 404 from your endpoint — a wrong path, a service not yet deployed — is a 4xx and is treated as permanent. It will never be retried. Test with POST /webhooks/{id}/test before relying on a subscription.

Respond fast, process later

Deliveries are synchronous HTTP calls with a 10 second budget. Do your verification, persist the payload, return 2xx, and process asynchronously. A handler that blocks on a downstream API will time out and be retried, which duplicates work.

Idempotency

Retries reuse the same X-ColdSend-Delivery-Id and the same event_id. Deduplicate on either — but note that a redelivery (below) is a genuinely new attempt at the same event, so keying on event_id alone is the safer choice if you must process exactly once.

Testing a Subscription

POST /api/public/v1/webhooks/{webhook_id}/test — scope webhooks:update Sends a ping event straight to your endpoint, synchronously, and reports what came back:
The ping payload carries event_type: "ping" and data: {"message": "Webhook test from ColdSend"}. It is signed exactly like a real delivery, so it exercises your verification path too.
The test endpoint returns 200 even when the ping fails — success: false with response_code: null on a timeout or connection error. Check the body, not the status code. Test deliveries are not written to the delivery log.

Delivery Log

GET /api/public/v1/webhooks/{webhook_id}/deliveries — scope webhooks:read
page_size is 1–100, default 50. Each entry carries event_type, attempt_number, status, response_code, error, attempted_at and delivered_at.

Redelivering

POST /api/public/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver — scope webhooks:update Re-queues a delivery for an immediate attempt, bypassing the backoff schedule:
Only deliveries in status failed or dead can be redelivered. Anything else returns 400 naming the current status — including a delivery that succeeded, which you cannot force through again this way.

Statistics

GET /api/public/v1/webhooks/{webhook_id}/stats — scope webhooks:read
Watch dead — those events are gone unless you redeliver them by hand.

Managing Subscriptions

PATCH accepts any subset of name, target_url, event_types, campaign_id, event_categories and active. Setting active: false pauses delivery without losing the subscription or its secret — the right move while you redeploy an endpoint. A subscription in another workspace, or one that does not exist, is a 404.

Next Steps

Event Types

Every event and its exact data shape.

Subsequences

What triggers subsequence_entered.

API Scopes

The webhooks:* family.

Error Codes

Status codes across the whole API.