Webhooks
Webhooks
Section titled “Webhooks”Webhooks push real-time HTTP POST notifications to your server when events happen on your SIPSTACK account — message delivery updates, live call lifecycle (ringing / answered / hangup, for CRM screen-pop), Aura AI voice-agent call outcomes, SMS opt-out and consent changes, campaign completion, and custom events fired by your AI voice-agent flows. Instead of polling the API, your endpoint is called the moment the event occurs.
How Webhooks Work
Section titled “How Webhooks Work”- You register an endpoint URL and subscribe it to event types.
- When a subscribed event occurs, SIPSTACK POSTs the event JSON to your URL.
- Your server responds with a 2xx status within 10 seconds to acknowledge.
- Failed deliveries are retried with backoff; endpoints that keep failing are disabled automatically.
Configuring Webhooks
Section titled “Configuring Webhooks”Manage webhook endpoints in Switchboard at Account → Integrations → Webhooks — register an endpoint, choose the events it receives, view the delivery log, and send a test event, all from the UI. Webhooks are a plan-gated feature.
Account owners can always manage webhooks. Other team members can too once an owner grants them the Manage API Keys & Webhooks permission — that one permission covers the Switchboard page and the API alike.
You can also manage them through the API, authenticated with a portal session that carries the Manage API Keys & Webhooks permission (flare.api_keys.manage) — account owners always have it (see Authentication):
| Action | Endpoint |
|---|---|
| List endpoints | GET /api/webhooks |
| Create an endpoint | POST /api/webhooks with { "url": "https://...", "events": ["message.delivered"] } |
| Delete an endpoint | DELETE /api/webhooks/{id} |
| View delivery log | GET /api/webhooks/{id}/deliveries |
| Send a test event | POST /api/webhooks/{id}/test |
- The
eventsarray selects which events the endpoint receives;["*"]subscribes to everything (including event types added later). Discover the valid names programmatically withGET /api/v2/events. - The create response includes the endpoint’s signing secret — shown only once. Store it securely; you need it to verify deliveries.
You can also manage endpoints with an API key (x-api-key) instead of a portal session, using the /api/v2/webhooks alias — handy for server-to-server setup:
| Action | Endpoint |
|---|---|
| List endpoints | GET /api/v2/webhooks |
| Create an endpoint | POST /api/v2/webhooks with { "url": "https://...", "events": ["message.delivered"] } |
| Delete an endpoint | DELETE /api/v2/webhooks/{id} |
The API-key create response returns the signing secret once (same as the portal endpoint) and echoes the signature header/format so you know exactly what to verify. See the API Reference for full shapes.
Payload Structure
Section titled “Payload Structure”The body is the event name plus the event’s fields at the top level:
{ "event": "message.delivered", "messageId": 12345, "status": "delivered", "to": "+14165550100", "from": "+16135550200", "campaignId": 42}Request headers on every delivery:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Event | The event name |
X-Webhook-Signature | sha256=<HMAC-SHA256 hex of the raw body> |
X-Idempotency-Key | The body’s idempotency_key, on events that carry one — the same value on every retry |
User-Agent | SIPSTACK-Webhook/1.0 |
Events Reference
Section titled “Events Reference”The platform exposes sixteen subscribable event types today (plus the synthetic test ping). Subscribe an endpoint only to events on this list (or ["*"] for all of them); an unknown event name is rejected at create time with a 400 that lists the valid names. You can always fetch the live catalog from GET /api/v2/events.
Messaging events
Section titled “Messaging events”Fired as the carrier reports status changes on outbound SMS, or when an inbound SMS arrives:
| Event | When | Payload fields |
|---|---|---|
message.sent | Message accepted by the carrier | messageId, status, to, from, campaignId |
message.delivered | Carrier confirmed delivery | messageId, status, to, from, campaignId |
message.failed | Delivery failed or was undeliverable | messageId, status, to, from, campaignId |
message.received | An inbound SMS arrived on one of your numbers | messageId, from, to, body |
campaignId is null for conversational (non-campaign) messages.
Call events (finalized CDR)
Section titled “Call events (finalized CDR)”Fired when a call’s CDR is finalized:
| Event | When | Payload fields |
|---|---|---|
call.completed | A call was answered and ended | callId, from, to, duration, billsec, disposition, direction |
call.missed | A call ended without being answered | callId, from, to, duration, disposition, direction |
Real-time call events (Nova)
Section titled “Real-time call events (Nova)”Fired live — milliseconds after the PBX sees them, before the CDR is finalized — so a CRM can screen-pop on an incoming call. Every payload carries callId, from, to, and direction (inbound / outbound / internal), plus extension and channel when known.
| Event | When | Extra payload fields |
|---|---|---|
call.inbound | A call is arriving on one of your numbers (before it’s answered) | — |
call.ringing | A call started ringing | — |
call.answered | A call was answered | — |
call.hangup | A call ended — the live terminal signal, fired for every direction | answered, duration, billsec, disposition |
An inbound call.hangup is followed by the matching finalized call.completed or call.missed above — de-duplicate terminal handling on callId if you subscribe to both the real-time and finalized sets.
Aura AI voice-agent call events
Section titled “Aura AI voice-agent call events”Fired by Aura agent calls (distinct from the flow-defined webhook steps below):
| Event | When | Payload fields |
|---|---|---|
aura.call.started | The agent answered and began handling the call | callId, agentId, agentName, from, to, direction, startedAt |
aura.call.completed | The agent call ended | callId, agentId, agentName, from, to, direction, billsec, outcome, transferTarget, summary, sentiment, extractedFields |
Contact consent events
Section titled “Contact consent events”Fired when a phone number’s SMS opt-out state or a contact’s consent changes, so a CRM or your own suppression list stays in step with SIPSTACK:
| Event | When | Extra payload fields |
|---|---|---|
contact.opted_out | A number was added to your SMS opt-out list — the recipient texted STOP, it was opted out in the portal or via the API, or a message hard-bounced | — |
contact.opted_in | A number was removed from your opt-out list — the recipient texted START, or it was opted back in in the portal or via the API | previous_opt_out_methods |
contact.consent_changed | A contact’s consent status or basis changed (implied / express / none) | consent_status, consent_method, consent_obtained_at, implied_consent_basis, implied_consent_basis_date, channel |
Every consent event carries the same base fields:
| Field | Description |
|---|---|
event | The event name |
event_id | Unique ID (UUID) for this change — your de-duplication key |
phone_number | The number, in E.164 format |
contact_ids | IDs (UUIDs) of your contacts with this number — can be empty or list several |
method | How the change was made: sms, web, admin, api, system, … |
actor | Who made it: { "type": "recipient" | "user" | "api_key" | "system" | "integration" } (an integration actor also names its provider), or null when unknown |
occurred_at | When the change happened (ISO-8601) |
idempotency_key | <event>:<event_id> — also sent as the X-Idempotency-Key header |
Opt-out events are per number, not per contact: one number shared by three contacts sends one contact.opted_out listing all three IDs in contact_ids. Opting out a number that is already on the list sends nothing. Payloads never include message text (including the STOP message), opt-out reasons, or notes.
contact.opted_out
{ "event": "contact.opted_out", "event_id": "3f6c1a2e-8b4d-4c1e-9f0a-5d2b7e9c1a44", "phone_number": "+14165550100", "contact_ids": ["2489e9ad-2ee2-8e00-8ec9-32d5f69181c0"], "method": "sms", "actor": { "type": "recipient" }, "occurred_at": "2026-10-08T14:03:12.418Z", "idempotency_key": "contact.opted_out:3f6c1a2e-8b4d-4c1e-9f0a-5d2b7e9c1a44"}contact.opted_in — previous_opt_out_methods lists how the number had been opted out:
{ "event": "contact.opted_in", "event_id": "9b2d4f61-0c3a-4e8b-a1d7-6f5e2c8b3a90", "phone_number": "+14165550100", "contact_ids": ["2489e9ad-2ee2-8e00-8ec9-32d5f69181c0"], "method": "sms", "actor": { "type": "recipient" }, "previous_opt_out_methods": ["sms"], "occurred_at": "2026-10-09T09:41:57.006Z", "idempotency_key": "contact.opted_in:9b2d4f61-0c3a-4e8b-a1d7-6f5e2c8b3a90"}contact.consent_changed — consent_status gives the old and new status. implied_consent_basis and implied_consent_basis_date appear only when the change involves implied consent:
{ "event": "contact.consent_changed", "event_id": "c7a85e13-4d2f-4b6a-8e19-0f3d5a7b2c68", "phone_number": "+14165550100", "contact_ids": ["2489e9ad-2ee2-8e00-8ec9-32d5f69181c0"], "method": "api", "actor": { "type": "api_key" }, "consent_status": { "from": "implied", "to": "express" }, "consent_method": "web_form", "consent_obtained_at": "2026-10-08T13:55:00.000Z", "channel": "api_contact_update", "occurred_at": "2026-10-08T13:55:02.731Z", "idempotency_key": "contact.consent_changed:c7a85e13-4d2f-4b6a-8e19-0f3d5a7b2c68"}Consent events are recorded in the same transaction as the change and delivered within about a minute. Delivery is at-least-once: de-duplicate on event_id (or the X-Idempotency-Key header). Events for different numbers can arrive out of order — compare occurred_at if order matters.
To backfill or reconcile instead of (or as well as) listening, poll GET /api/v2/opt-outs?since= for the current opt-out list and GET /api/v2/contacts?updated_since= for changed contacts (including is_opted_out). A number that is opted back in leaves the opt-out list, so the contact.opted_in event or is_opted_out on the contact is how you see removals. See the API Reference.
Campaign events
Section titled “Campaign events”| Event | When | Payload fields |
|---|---|---|
campaign.completed | A campaign finishes sending | campaignId, name, messagesSent |
Voice-agent flow events
Section titled “Voice-agent flow events”AI voice-agent flows can include webhook steps. When a call reaches that step, the event name and payload you defined in the flow are dispatched to your subscribed endpoints. Test calls from the agent builder include "test": true in the payload so you can filter them.
Test event
Section titled “Test event”POST /api/webhooks/{id}/test delivers a test event ({ "event": "test", "message": "This is a test webhook delivery" }) to a single endpoint on demand, so you can verify connectivity and signature handling.
Verifying Signatures
Section titled “Verifying Signatures”Always verify X-Webhook-Signature before trusting a payload — it proves the request came from SIPSTACK and wasn’t tampered with.
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, secret) { const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex'); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));}
// Express handler — note express.raw(): verify against the RAW body,// not a re-serialized parse of it.app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; if (!signature || !verifyWebhookSignature(req.body, signature, process.env.SIPSTACK_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); }
const event = JSON.parse(req.body); // hand off to a queue and respond fast res.status(200).send('OK');});Delivery, Retries & Auto-Disable
Section titled “Delivery, Retries & Auto-Disable”- Your endpoint must respond 2xx within 10 seconds — respond immediately and process asynchronously. A non-2xx response, a timeout, or a connection error all count as a failed attempt.
- A failed delivery is retried up to 2 more times: attempt 2 about 1 minute after the failure, attempt 3 about 5 minutes after that — at most 3 attempts per event.
- Every attempt is logged — event type, HTTP status, response time, attempt number — and queryable via
GET /api/webhooks/{id}/deliveries. - If you delete or disable an endpoint while retries are pending, those retries are skipped — nothing is delivered to a removed endpoint.
- When an event exhausts all 3 attempts, the endpoint’s consecutive-failure count increments; after 10 consecutive fully-failed events the endpoint is automatically disabled to protect the queue. Any successful delivery resets the count to zero. Recreate the endpoint (or contact support) once your server is healthy, and use the delivery log to backfill anything you missed.
Idempotency
Section titled “Idempotency”Retries mean your endpoint can receive the same event more than once. Delivery is at-least-once, never exactly-once. When an event carries an idempotency_key (also sent as the X-Idempotency-Key header), de-duplicate on it — or on event_id for contact consent events. Otherwise, use a natural key (messageId + event, or your flow’s own correlation ID).