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 the payload body |
X-Webhook-Event | The event type (e.g. contact.created) |
X-Webhook-Delivery | Unique delivery ID |
Signature Verification
Verify the X-Webhook-Signature header using your endpoint’s signing secret:
const crypto = require("crypto");
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return 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 (All 26 Types)
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 |
contact.merged | Two contacts merged (anonymous to known) | primaryContactId, secondaryContactId, mergedContact |
Segment Events
| Event | Description | Payload Fields |
|---|---|---|
segment.entered | Contact enters a segment | segmentId, contactId |
segment.exited | Contact leaves a segment | segmentId, contactId |
Campaign Events
| Event | Description | Payload Fields |
|---|---|---|
campaign.started | Campaign activated | campaignId, name, status |
campaign.completed | Campaign finished for all contacts | campaignId, name, totalEnrolled |
campaign.contact.enrolled | Contact enters a campaign | campaignId, contactId |
campaign.contact.completed | Contact finishes a campaign flow | campaignId, contactId, completedAt |
Message Events
| Event | Description | Payload Fields |
|---|---|---|
message.sent | Message dispatched to a channel | contactId, channel, messageId, status, campaignId |
message.delivered | Delivery confirmed by provider | messageId, contactId, channel |
message.opened | Email or push opened | messageId, contactId, channel |
message.clicked | Tracked link clicked | messageId, contactId, channel, url |
message.bounced | Email bounced (hard or soft) | messageId, contactId, bounceType |
message.complained | Recipient marked as spam | messageId, contactId |
message.failed | Message failed to send | messageId, contactId, error |
Subscription Events
| Event | Description | Payload Fields |
|---|---|---|
unsubscribe.email | Contact opts out of email | contactId, channel, messageId |
unsubscribe.push | Push subscription removed | contactId, channel |
unsubscribe.telegram | Contact blocks Telegram bot | contactId, channel |
unsubscribe.all | Global unsubscribe from all channels | contactId |
Form Events
| Event | Description | Payload Fields |
|---|---|---|
form.submitted | Form submission received | formId, submissionId, fields |
Push Events
| Event | Description | Payload Fields |
|---|---|---|
push.subscribed | Browser subscribes to push | contactId, endpoint |
push.clicked | Push notification clicked | contactId, messageId |
Custom Events
| Event | Description | Payload Fields |
|---|---|---|
event.tracked | Custom tracking event ingested | contactId, eventType, metadata |