Webhooks
Subscribe to platform events and SIP.IO POSTs them to your HTTPS endpoint as they happen, signed so you can verify they came from SIP.IO.
Subscribe
Section titled “Subscribe”Manage subscriptions via the /v1/webhooks endpoint (scope webhooks):
curl -X POST https://api.sip.io/v1/webhooks \ -H 'x-api-key: sk_…' -H 'content-type: application/json' \ -d '{ "url": "https://acme.com/hooks/sipio", "events": "call.started,call.ended", "secret": "optional" }'| Field | Purpose |
|---|---|
url | Your endpoint; must be https://. |
events | Comma-separated event names, or * for all (default). |
secret | HMAC signing secret. If omitted, one is generated and returned once at creation. |
GET /v1/webhooks lists your subscriptions (without secrets); DELETE /v1/webhooks with { id } removes one. POST /v1/webhooks/test fires a test event so you can verify your endpoint.
Events
Section titled “Events”| Event | Fires when | data |
|---|---|---|
call.started | A call is routed (call start). | callId, from, to, src_ip, ts |
call.ended | A call ends. | callId, direction, answered, billsec, hangup_cause, queue_id, agent_id, ts |
test | You call /v1/webhooks/test. |
Payload
Section titled “Payload”Every delivery is a JSON POST in this shape:
{ "event": "call.ended", "account_id": "acc_acme", "data": { "callId": "…", "direction": "inbound", "answered": 1, "billsec": 184, "hangup_cause": "NORMAL_CLEARING", "queue_id": "q_support", "agent_id": "us_dana", "ts": 1719600000000 }, "ts": 1719600000000}Each request also carries x-sipio-event: <event> and content-type: application/json.
Verifying the signature
Section titled “Verifying the signature”If the subscription has a secret, SIP.IO signs the raw request body with HMAC-SHA256 and sends it in the x-sipio-signature header as sha256=<hex>. Recompute it and compare:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret) { const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'); const a = Buffer.from(header || ''), b = Buffer.from(expected); return a.length === b.length && timingSafeEqual(a, b);}Delivery
Section titled “Delivery”- Each event is POSTed to every matching active subscription, with a 5-second timeout.
- Delivery is best-effort, single-attempt today. The last attempt’s HTTP status is recorded on the subscription; there’s no automatic retry yet. Make your handler idempotent and return
2xxquickly. - Endpoints must be
https://.