Webhooks
CampaignOS supports both outbound and inbound webhooks for integrating with external systems.
- Outbound webhooks send data from CampaignOS to your systems when events occur.
- Inbound webhooks receive data from your systems into CampaignOS.
Outbound Webhooks
How It Works
- Create a webhook endpoint with the events you want to subscribe to.
- CampaignOS sends a signed
POSTrequest to your URL whenever a matching event occurs. - Your server verifies the signature and processes the payload.
Payload Format
All outbound webhook deliveries follow this format:
{
"event": "contact.created",
"timestamp": "2026-01-15T10:00:00.000Z",
"data": {
"contactId": "ct_abc123",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe"
}
}Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC-SHA256 hex digest of "<timestamp>.<body>" |
X-Webhook-Timestamp | Unix timestamp (seconds) used in the signature; reject requests older than 5 minutes |
X-Webhook-Event | The event type (e.g. contact.created) |
X-Webhook-Delivery | Unique delivery ID |
X-Webhook-Attempt | Delivery attempt number (retries use backoff of 2s and 8s) |
Signature Verification
Verify the X-Webhook-Signature header using your endpoint’s signing secret. The signature covers the timestamp and the raw body joined by a dot:
const crypto = require("crypto");
function verifyWebhook(rawBody, headers, secret) {
const timestamp = headers["x-webhook-timestamp"];
const signature = headers["x-webhook-signature"] ?? "";
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return (
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}Inbound Webhooks
How It Works
- Create an inbound webhook endpoint in CampaignOS.
- Copy the generated URL and signing secret.
- Send data from your external systems to this URL.
- CampaignOS processes the payload, creates events, and optionally auto-creates contacts.
Endpoint URL
POST /api/webhooks/inbound/{endpointId}Payload Format
{
"type": "user.signup",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"phone": "+1234567890",
"metadata": {
"source": "external-crm",
"plan": "pro"
}
}| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Event type to record (e.g. user.signup, purchase.completed) |
email | string | No | Contact email — used to find or auto-create a contact |
contactId | string | No | CampaignOS contact ID (if known) |
firstName | string | No | Contact first name (used when auto-creating) |
lastName | string | No | Contact last name (used when auto-creating) |
phone | string | No | Contact phone (used when auto-creating) |
metadata | object | No | Additional data stored with the event |
Auto-Create Contacts
When you include an email in the payload and no matching contact exists in your workspace, CampaignOS automatically creates a new contact with the provided details (email, firstName, lastName, phone). This means you can send events from external systems without pre-creating contacts.
Signing Inbound Requests
If you configure a signing secret, include a signature header:
X-Webhook-Signature: <hmac_hex_digest>Computed as HMAC-SHA256 of the raw request body using your signing secret.
cURL Example
curl -X POST https://your-domain.com/api/webhooks/inbound/ENDPOINT_ID \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: YOUR_SIGNATURE" \
-d '{
"type": "user.signup",
"email": "jane@example.com",
"firstName": "Jane",
"metadata": { "source": "crm", "plan": "pro" }
}'Response
{
"received": true,
"deliveryId": "del_abc123"
}API Reference
List Webhook Endpoints
GET /api/workspaces/{workspaceId}/webhooks
Lists all configured webhook endpoints with delivery counts.
Auth: Session required
Response:
[
{
"id": "wh_abc123",
"name": "CRM Sync",
"url": "https://hooks.example.com/campaignos",
"events": ["contact.created", "message.sent"],
"direction": "OUTBOUND",
"active": true,
"_count": { "deliveries": 1500 },
"createdAt": "2026-01-15T10:00:00.000Z"
}
]Create Webhook Endpoint
POST /api/workspaces/{workspaceId}/webhooks
Auth: Session + ADMIN role required
Request Body:
{
"name": "CRM Sync",
"url": "https://hooks.example.com/campaignos",
"events": ["contact.created", "contact.updated", "message.sent"],
"direction": "OUTBOUND"
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Human-readable label (max 100 chars) |
url | string | Outbound only | Webhook destination URL |
events | string[] | No | Event types to subscribe to |
direction | string | No | INBOUND or OUTBOUND (default: OUTBOUND) |
Response (201): Webhook endpoint with secret (shown only once).
Update Webhook Endpoint
PATCH /api/workspaces/{workspaceId}/webhooks/{webhookId}
Auth: Session + ADMIN role required
Request Body:
{
"name": "Updated Name",
"url": "https://new-url.example.com/hook",
"events": ["contact.created"],
"active": false
}Delete Webhook Endpoint
DELETE /api/workspaces/{workspaceId}/webhooks/{webhookId}
Auth: Session + ADMIN role required. Cascades to all delivery records.
List Deliveries
GET /api/workspaces/{workspaceId}/webhooks/deliveries
Lists webhook delivery logs with pagination and filtering.
Query Parameters:
| Param | Type | Description |
|---|---|---|
page | number | Page number (default: 1) |
limit | number | Items per page (default: 50, max: 100) |
endpointId | string | Filter by endpoint |
success | boolean | Filter by success status |
event | string | Filter by event type |
Retry Delivery
POST /api/workspaces/{workspaceId}/webhooks/deliveries/{deliveryId}/retry
Auth: Session + ADMIN role required. Only outbound deliveries can be retried.
Webhook Events
These are the event types currently emitted by CampaignOS.
Contact Events
| Event | Description | Payload Fields |
|---|---|---|
contact.created | New contact created | contactId, email, firstName, lastName |
contact.updated | Contact profile updated | contactId, email, firstName, lastName |
contact.deleted | Contact permanently removed | contactId, email |
Campaign Events
| Event | Description | Payload Fields |
|---|---|---|
campaign.contact.enrolled | Contact enters a campaign | campaignId, contactId |
Message Events
| Event | Description | Payload Fields |
|---|---|---|
message.sent | Message dispatched to a channel | contactId, channel, messageId, status, campaignId |
Subscription Events
| Event | Description | Payload Fields |
|---|---|---|
unsubscribe.email | Contact opts out of email | contactId, email, externalId, channel, source, messageId (when from a link) |
unsubscribe.push | Contact opts out of push | contactId, email, externalId, channel, source |
unsubscribe.telegram | Contact opts out of Telegram | contactId, email, externalId, channel, source |
unsubscribe.all | Global unsubscribe from all channels | contactId, email, externalId, channel, source |
An opt-out fires these whatever its origin: an unsubscribe link, the preference centre, the ingest endpoint’s emailOptOut, the consent API or an automation step. source says which. Use them to keep other systems (another mailing tool, a CRM) in step.
Form Events
| Event | Description | Payload Fields |
|---|---|---|
form.submitted | Form submission received | formId, submissionId, fields |