Docs › Build
Webhooks
Everything that happens on the line arrives as an event: consistent envelope, HMAC-signed, ordered per resource, and retried with backoff until your endpoint returns a 2xx.
Register an endpoint
curl https://api.handset.dev/v1/webhook_endpoints \ -H "Authorization: Bearer $HANDSET_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://yourapp.com/handset/events"}' # The response contains the signing secret — shown exactly once.
Event catalog
| Event | When |
|---|---|
message.received | An inbound text arrived on a tenant number |
message.delivered | An outbound message reached the handset |
message.failed | Delivery failed, with a carrier-level reason |
call.started | An inbound call began ringing |
call.completed | A call ended, with duration and outcome |
call.missed | A call went unanswered with no voicemail |
voicemail.created | A voicemail landed — recording and transcript attached |
brand.status_changed | 10DLC brand vetting progressed |
campaign.status_changed | Campaign approval progressed |
The envelope
{
"id": "evt_01kzv9f2x8…",
"type": "message.received",
"created_at": "2026-08-12T18:04:11Z",
"tenant_id": "tnt_01kzv90afex…",
"data": { "object": "message", … }
}Verify signatures
Each delivery carries a Handset-Signature header:
t=<unix_ts>,v1=<hmac>. Recompute HMAC-SHA256 over
{timestamp}.{body} with your endpoint secret, compare in
constant time, and reject anything older than five minutes.
// Handset-Signature: t=1755012239,v1=5f3a… const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); const ok = timingSafeEqual(expected, v1); // ✓
Return 2xx quickly and process async — slow
endpoints get retried, and retries are delivered with the same event
id for dedup.