Skip to content
DocsStart free

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.

Manage subscriptions via the /v1/webhooks endpoint (scope webhooks):

Terminal window
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" }'
FieldPurpose
urlYour endpoint; must be https://.
eventsComma-separated event names, or * for all (default).
secretHMAC 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.

EventFires whendata
call.startedA call is routed (call start).callId, from, to, src_ip, ts
call.endedA call ends.callId, direction, answered, billsec, hangup_cause, queue_id, agent_id, ts
testYou call /v1/webhooks/test.

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.

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);
}
  • 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 2xx quickly.
  • Endpoints must be https://.