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
Creating a Subscription
POST/api/public/v1/webhooks — scope webhooks:create, returns 201
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:- Workspace — the event’s workspace must equal the subscription’s. This is not configurable.
event_types— the event type must be listed. An empty array delivers nothing, not everything.campaign_id— if set, events with a different campaign ID (or none at all) are dropped.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.
Payload Format
Every delivery is aPOST with a JSON body:
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.- Python
- Node.js
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.
Respond fast, process later
Deliveries are synchronous HTTP calls with a 10 second budget. Do your verification, persist the payload, return2xx, and process asynchronously. A handler that blocks on a downstream API will time out and be retried, which duplicates work.
Idempotency
Retries reuse the sameX-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:
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:
Statistics
GET/api/public/v1/webhooks/{webhook_id}/stats — scope webhooks:read
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.

