Webhooks
Webhooks tell your systems what happened in a workspace: a message arrived, a call was missed, an order was paid.
Each delivery is an HTTPS POST of a JSON event, signed so you can check it came from Nexafon.
1. Add an endpoint
Section titled “1. Add an endpoint”In the dashboard open Developers → Webhooks, add your URL and pick the events it should
receive. Or create it through the API with a secret key that has developers:manage:
curl -X POST "https://api.nexafone.so/api/v1/workspaces/$WORKSPACE_ID/webhook-endpoints" \ -H "Authorization: Bearer $NEXAFON_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/nexafon", "events": ["message.received", "call.missed"]}'The answer carries the endpoint’s signing secret (whsec_…) once. Store it with your other secrets.
The URL must be https and reach the public internet: Nexafon resolves the host and refuses private, link-local and
cloud metadata addresses, connects to the address it checked, and never follows redirects. A workspace can have up to
20 endpoints, each subscribed to up to 64 event types.
2. Receive and verify
Section titled “2. Receive and verify”if (req.method === 'POST' && req.url === '/hooks/nexafon') { try { // The raw body, exactly as received: parsing it first changes the bytes the signature covers. const event = verifyWebhook( req.headers, await rawBody(req), process.env.NEXAFON_WEBHOOK_SECRET ?? '', ) // Deliveries can repeat: skip an event.id you have already handled. console.log(event.type, event.id) res.writeHead(204).end() } catch (err) { if (!(err instanceof WebhookVerificationError)) throw err res.writeHead(400).end() } return}Signatures has the receiver in PHP, Python and Go, and the check without a helper.
3. Answer fast
Section titled “3. Answer fast”Answer any 2xx within 10 seconds, then do the work: put the event on your own queue first if handling it takes
longer. Anything else (a timeout, 3xx, 4xx, 5xx) counts as a failure and is
retried for up to 3 days.
The event
Section titled “The event”{ "id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c", "type": "message.received", "createdAt": "2026-09-21T14:13:18Z", "workspaceId": "aaaaaaaa-0000-0000-0000-000000000000", "data": { "message": { "id": "…", "conversationId": "…", "direction": "inbound", "type": "text", "text": "Hello" }, "conversation": { "id": "…", "status": "open", "contact": { "phone": "252611234567" } } }}| Field | |
|---|---|
id |
Unique per event. Retries, replays and redeliveries send the same id: dedupe on it |
type |
The event type |
createdAt |
When it happened (RFC 3339, UTC) |
workspaceId |
The workspace (store) it happened in |
partner |
On a partner’s stores only: { "storeExternalRef": "…" } (partner webhooks) |
data |
The event’s objects, in the same shape as the API returns them |
Every delivery also carries these headers:
| Header | |
|---|---|
Nexafon-Signature |
t=<unix seconds>,v1=<hex HMAC-SHA256> (signatures) |
Nexafon-Event-Id |
The event’s id |
Nexafon-Event-Type |
The event’s type |
Nexafon-Delivery-Attempt |
1 for the first attempt, then 2, 3… |
User-Agent |
Nexafon-Webhooks/1.0 |
Ordering is best effort per endpoint: use createdAt and the objects’ own versions where order matters.