Webhooks
Be told when something happens, instead of asking.
A webhook endpoint is an organization-wide subscription to a list of events. Deliveries are signed with HMAC-SHA256 over <timestamp>.<body> and sent as X-Vocalis-Signature: t=…,v1=…; verify it before trusting a payload, and reject a timestamp that is not recent. Each delivery carries X-Vocalis-Event-Id, equal to the body's id and the same on every retry, so use it as an idempotency key. It is not the id the same event has on a stream: each surface numbers its own. Failures are retried with exponential backoff; a 4xx other than 408 or 429 is treated as a refusal and not retried, and an endpoint that fails continuously is switched off rather than being sent events into a void.
The signing secret is returned exactly once, when the endpoint is created and again when it is rotated. There is no endpoint that reads it back. While a rotation overlaps, a delivery carries one v1= signature per live secret (t=…,v1=<new>,v1=<old>); accept it when any of them matches a secret you hold.
List endpoints#
Every webhook endpoint in the organization.
/api/v1/webhook-endpointsResponses
The endpoints.
curl -X GET "https://voice.evryhealth.com/api/v1/webhook-endpoints" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"endpoints": [{"id": "c41a…","name": "Ops receiver","url": "https://hooks.example.com/vocalis","events": ["call.ended","call.analysis.completed"],"secretPrefix": "whsec_9Qa3Kd","isEnabled": true,"consecutiveFailures": 0,"lastSuccessAt": "2026-09-15T13:58:02.000Z"}]},"meta": {"requestId": "req_…"}}
Create an endpoint#
Subscribe a URL to a list of events. The URL is checked at creation: it must be public http(s), and one resolving to a private or link-local address is refused.
/api/v1/webhook-endpointsBody
application/jsonname- string
- required
url- string
- required
events- string[]
- required
Responses
Created. The secret appears here and nowhere else.
curl -X POST "https://voice.evryhealth.com/api/v1/webhook-endpoints" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"name": "Ops receiver","url": "https://hooks.example.com/vocalis","events": ["call.ended","intelligence.speaker.identified"]}'
{"data": {"id": "c41a…","name": "Ops receiver","url": "https://hooks.example.com/vocalis","events": ["call.ended","intelligence.speaker.identified"],"secret": "whsec_9Qa3Kd…","secretPrefix": "whsec_9Qa3Kd"},"meta": {"requestId": "req_…"}}
Get an endpoint#
One endpoint. The signing secret is never returned here.
/api/v1/webhook-endpoints/{endpointId}Path parameters
endpointIdstringrequired- Endpoint id.
Responses
The endpoint.
curl -X GET "https://voice.evryhealth.com/api/v1/webhook-endpoints/{endpointId}" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"id": "c41a…","name": "Ops receiver","url": "https://hooks.example.com/vocalis","events": ["call.ended"],"secretPrefix": "whsec_9Qa3Kd","isEnabled": true},"meta": {"requestId": "req_…"}}
Update an endpoint#
Change the URL, the subscription, or pause it. Re-enabling clears the failure count, so an endpoint switched off after an outage starts clean.
/api/v1/webhook-endpoints/{endpointId}Path parameters
endpointIdstringrequired- Endpoint id.
Body
application/jsonname- string
url- string
events- string[]
isEnabled- boolean
Responses
The updated endpoint.
curl -X PATCH "https://voice.evryhealth.com/api/v1/webhook-endpoints/{endpointId}" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"events": ["call.ended"],"isEnabled": true}'
{"data": {"id": "c41a…","events": ["call.ended"]},"meta": {"requestId": "req_…"}}
Rotate the secret#
Issue a new signing secret. The previous one keeps signing deliveries alongside it for `expiresInSeconds` (24 hours by default, up to 7 days), so every delivery in that window, including retries of events queued before the rotation, carries a signature from each and your receiver can switch over when it is ready. `previousSecretExpiresAt` says when the old one stops. Send `expiresInSeconds: 0` to end the old secret at once, which is what you want after a leak: an overlap is a period in which the leaked secret still signs valid-looking traffic. Rotating again during an overlap retires the oldest secret immediately.
/api/v1/webhook-endpoints/{endpointId}/rotate-secretPath parameters
endpointIdstringrequired- Endpoint id.
Body
application/jsonexpiresInSeconds- integer, 0 to 604800
Responses
The new secret, returned once.
curl -X POST "https://voice.evryhealth.com/api/v1/webhook-endpoints/{endpointId}/rotate-secret" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"expiresInSeconds": 3600}'
{"data": {"id": "c41a…","secret": "whsec_2Pb7Xm…","secretPrefix": "whsec_2Pb7Xm","previousSecretExpiresAt": "2026-09-16T14:07:44.000Z"},"meta": {"requestId": "req_…"}}
Delivery log#
What we tried to send, how many times, and what the receiver said. Payloads are not returned. They were sent to you, and they are encrypted at rest because they carry PHI.
/api/v1/webhook-endpoints/{endpointId}/deliveriesPath parameters
endpointIdstringrequired- Endpoint id.
Query parameters
statusstring- pending, in_flight, succeeded, failed or dead_lettered.
limitnumber- Up to 200.
- Default
50 cursorstring- From meta.pagination.nextCursor.
Responses
A page of deliveries.
curl -X GET "https://voice.evryhealth.com/api/v1/webhook-endpoints/{endpointId}/deliveries" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"deliveries": [{"id": "7f21…","eventType": "call.ended","eventId": "1789525082142-0","status": "succeeded","attemptCount": 1,"responseStatus": 200,"createdAt": "2026-09-15T14:07:44.400Z"}]},"meta": {"requestId": "req_…","pagination": {"nextCursor": null,"limit": 50}}}
Delete an endpoint#
Stop delivering. Queued deliveries for it are abandoned rather than chased.
/api/v1/webhook-endpoints/{endpointId}Path parameters
endpointIdstringrequired- Endpoint id.
Responses
Deleted.
curl -X DELETE "https://voice.evryhealth.com/api/v1/webhook-endpoints/{endpointId}" \-H "Authorization: Bearer YOUR_API_KEY"
{"data": {"deleted": true},"meta": {"requestId": "req_…"}}