handset docs

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
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

EventWhen
message.receivedAn inbound text arrived on a tenant number
message.deliveredAn outbound message reached the handset
message.failedDelivery failed, with a carrier-level reason
call.startedAn inbound call began ringing
call.completedA call ended, with duration and outcome
call.missedA call went unanswered with no voicemail
voicemail.createdA voicemail landed — recording and transcript attached
brand.status_changed10DLC brand vetting progressed
campaign.status_changedCampaign approval progressed

The envelope

event.json
{
  "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.

verify.ts
// 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.