welldun. offhook

Events and logs

Webhooks

Get a signed HTTP request for every call event instead of polling. Failed deliveries are retried with increasing delays, up to 8 attempts.

Register an endpoint

POST /v1/webhooks

Body parameters

FieldTypeDescription
urlrequired string A public HTTPS URL that receives the POSTs. Private and loopback addresses are rejected.
eventsoptional string[] The event types to send. Leave it empty to get every event.
curl -X POST http://localhost:8000/v1/webhooks \
  -H "Authorization: Bearer $OFFHOOK_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://you.example.com/hook", "events": [] }'

Event types

EventSent when
call.queued The call was accepted and is waiting to be dialed.
call.dialing The carrier is dialing the number.
call.in_progress The call connected and the conversation is live.
call.completed The call ended normally. Includes the outcome summary, duration and number of turns.
call.failed The call ended in an error. Includes an error_code.
call.received An inbound call arrived on one of your numbers.
carrier.hangup The carrier reported why the call ended (busy, no answer and so on).

Payload

Each delivery is a POST with a JSON body like this, and these headers:

{
  "id": "…",
  "event": "call.completed",
  "created": 1792065645,
  "data": {
    "call_id": "8f2c4d1e-6b7a-4c2e-9f1d-3a5b7c9e1f20",
    "status": "completed",
    "summary": "Confirmed for Tuesday at 3 PM",
    "duration_seconds": 38,
    "turns": 6,
    "error": null
  }
}

Your endpoint should reply with any 2xx status. Anything else counts as a failure and is retried.

Check the signature

X-Voice-Signature has the form t=<unix time>,v1=<signature>. The signature is an HMAC-SHA256 of {t}.{raw body}, keyed with your endpoint's secret. Compute it yourself, compare the two in constant time, and reject old timestamps so a captured request can't be replayed.

import hmac, hashlib, time

def verify(secret, body, header, tolerance=300):
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = int(parts["t"]), parts["v1"]
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.{body}".encode(),
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

List and delete

GET /v1/webhooks
DELETE /v1/webhooks/{id}