Skip to content

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.

  1. You register an endpoint URL and subscribe it to event types.
  2. When a subscribed event occurs, SIPSTACK POSTs the event JSON to your URL.
  3. Your server responds with a 2xx status within 10 seconds to acknowledge.
  4. Failed deliveries are retried with backoff; endpoints that keep failing are disabled automatically.

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):

ActionEndpoint
List endpointsGET /api/webhooks
Create an endpointPOST /api/webhooks with { "url": "https://...", "events": ["message.delivered"] }
Delete an endpointDELETE /api/webhooks/{id}
View delivery logGET /api/webhooks/{id}/deliveries
Send a test eventPOST /api/webhooks/{id}/test
  • The events array selects which events the endpoint receives; ["*"] subscribes to everything (including event types added later). Discover the valid names programmatically with GET /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:

ActionEndpoint
List endpointsGET /api/v2/webhooks
Create an endpointPOST /api/v2/webhooks with { "url": "https://...", "events": ["message.delivered"] }
Delete an endpointDELETE /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.

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:

HeaderValue
Content-Typeapplication/json
X-Webhook-EventThe event name
X-Webhook-Signaturesha256=<HMAC-SHA256 hex of the raw body>
X-Idempotency-KeyThe body’s idempotency_key, on events that carry one — the same value on every retry
User-AgentSIPSTACK-Webhook/1.0

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.

Fired as the carrier reports status changes on outbound SMS, or when an inbound SMS arrives:

EventWhenPayload fields
message.sentMessage accepted by the carriermessageId, status, to, from, campaignId
message.deliveredCarrier confirmed deliverymessageId, status, to, from, campaignId
message.failedDelivery failed or was undeliverablemessageId, status, to, from, campaignId
message.receivedAn inbound SMS arrived on one of your numbersmessageId, from, to, body

campaignId is null for conversational (non-campaign) messages.

Fired when a call’s CDR is finalized:

EventWhenPayload fields
call.completedA call was answered and endedcallId, from, to, duration, billsec, disposition, direction
call.missedA call ended without being answeredcallId, from, to, duration, disposition, direction

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.

EventWhenExtra payload fields
call.inboundA call is arriving on one of your numbers (before it’s answered)—
call.ringingA call started ringing—
call.answeredA call was answered—
call.hangupA call ended — the live terminal signal, fired for every directionanswered, 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.

Fired by Aura agent calls (distinct from the flow-defined webhook steps below):

EventWhenPayload fields
aura.call.startedThe agent answered and began handling the callcallId, agentId, agentName, from, to, direction, startedAt
aura.call.completedThe agent call endedcallId, agentId, agentName, from, to, direction, billsec, outcome, transferTarget, summary, sentiment, extractedFields

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:

EventWhenExtra payload fields
contact.opted_outA 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_inA number was removed from your opt-out list — the recipient texted START, or it was opted back in in the portal or via the APIprevious_opt_out_methods
contact.consent_changedA 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:

FieldDescription
eventThe event name
event_idUnique ID (UUID) for this change — your de-duplication key
phone_numberThe number, in E.164 format
contact_idsIDs (UUIDs) of your contacts with this number — can be empty or list several
methodHow the change was made: sms, web, admin, api, system, …
actorWho made it: { "type": "recipient" | "user" | "api_key" | "system" | "integration" } (an integration actor also names its provider), or null when unknown
occurred_atWhen 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.

EventWhenPayload fields
campaign.completedA campaign finishes sendingcampaignId, name, messagesSent

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.

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.

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');
});
  • 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.

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).