Docs navigation

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;
    • data is the object's complete state when the event was created, so for any one object keep the event with the latest createdAt and ignore older ones (for example, an sms.status "sent" arriving after "delivered");
    • when in doubt, GET the object. The REST API is always the source of truth.
  • 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 ping event 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.