API webhooks
Webhooks push events to you as they happen, so you don't have to poll. Add an endpoint under
Admin → Integrations → API → Webhooks: give it a name and an https:// URL, and choose its events. Its
signing secret (whsec_…) is shown once. Store it with your endpoint code.
Events
Each event carries the same object the REST API returns, so one parser serves both.
| Event | When | data |
|---|---|---|
call.ended |
Every finished call. Filter on status for missed calls. |
Call, with its segments (reference) |
sms.received |
A text arrives. An MMS sends one event per item. | Message |
sms.status |
An outbound text moves on: sent, delivered or failed. |
Message |
voicemail.received |
A message is left in a mailbox. | Voicemail. transcript is null here; fetch it later from GET /voicemails. |
queue.updated |
A queue's waiting calls, agents or today's totals change, at most every 5 seconds per queue. | QueueState |
Every POST looks like this:
{ "id": "evt_01928c6e5a2b7c40a1e3f0d2b4c6a8e0", "type": "call.ended",
"createdAt": "2026-10-20T14:04:13Z", "data": { … the Call … } }
and carries these headers:
| Header | |
|---|---|
Exovo-Signature |
t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<body>" with your secret> |
Exovo-Event |
The event type. |
Exovo-Delivery |
The event id, the same as id in the body. |
Verifying the signature
Recompute the HMAC over the raw body exactly as received, before you parse it. Reject the request if the
signature doesn't match, or if t is more than 5 minutes old (a replay):
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
Delivery
- Answer with any 2xx within 10 seconds. Do the real work after you reply, for example from a queue.
- Anything else (an error status, a timeout, a refused connection or a redirect) is retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes and 1 hour, then every 2 hours, for 24 hours. After that the delivery is given up.
- Delivery is at-least-once and unordered, per object as well as overall. Retries and parallel sending
mean an older event can arrive after a newer one, and the same event can arrive twice. So:
- de-duplicate on
id; datais the object's complete state when the event was created, so for any one object keep the event with the latestcreatedAtand ignore older ones (for example, ansms.status"sent" arriving after "delivered");- when in doubt,
GETthe object. The REST API is always the source of truth.
- de-duplicate on
- A disabled endpoint keeps its pending events and catches up if you enable it again within 24 hours.
Managing endpoints
The Webhooks list shows each endpoint's health: when it last succeeded or failed, the last error, and how many events are waiting.
- Send test queues a
pingevent to that endpoint. - Deliveries shows the past week's attempts and their results.
- New secret replaces the signing secret. The old one stops working at once, so update your endpoint straight away.
- Delete stops events immediately and drops undelivered ones.
A system can have up to 10 endpoints. Every change is written to the audit log.