Skip to content

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.

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:

Terminal window
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.

server.ts
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.

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.

{
"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.