Webhooks
Webhooks
Register an endpoint and the events it should receive. Every delivery arrives in the same signed envelope, and a failed one is retried for about 24 hours.
Webhooks
Register a webhook endpoint with the events you want to receive. The signing_secret in the response is shown once, at creation.
const { data: webhook, error } = await honkio.webhooks.create({
url: 'https://example.com/webhooks/honkio',
events: ['message.delivered', 'message.failed', 'message.received'],
})
if (error) throw new Error(error.message)
console.log(webhook.id, webhook.signing_secret) // the secret is shown once: store it nowcurl -X POST https://api.honkio.ca/v1/webhooks \
-H "Authorization: Bearer mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/honkio",
"events": ["message.delivered", "message.failed", "message.received"]
}'
# Response 201: { "id": "...", "url": "...", "events": [...],
# "signing_secret": "64 hex characters, shown once", "active": true, "created_at": "..." }SMS, opt-out, phone number and account events and their payloads are listed in the event reference.
Email events (email.* and email_domain.*) and their payloads are listed in the event reference.
Every payload is signed with HMAC-SHA256, so verify the X-HonkIO-Signature header. A message.received event carries the sender, your number, the body, keyword_action (STOP/START handling), and the segment_count and cost_cents the message was billed at. A message.sent event carries carrier_message_id, the id the carrier assigned to the message, with the same two billing fields.
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"type": "message.delivered",
"created": "2026-09-19T12:00:00.000Z",
"account_id": "clxxxaccountxxxxxxxxxxxxx",
"livemode": true,
"data": {
"message_id": "clxxxmessagexxxxxxxxxxxxx",
"status": "delivered",
"message_status": "DELIVERED",
"to": "+1613XXXXXXX",
"from": "+1416XXXXXXX",
"errors": []
}
}
// Headers: X-HonkIO-Signature, X-HonkIO-Timestamp, X-HonkIO-EventA message id can emit message.failed (only when the failure was CARRIER_UNAVAILABLE; an INSUFFICIENT_BALANCE failure fires no webhook) and later message.queued again, then message.sent, when a retry with the same Idempotency-Key re-attempts a message that failed before it reached the carrier.
The envelope
Every event arrives as one JSON object with the same six top level fields. Only data changes from event to event.
| Field | Meaning |
|---|---|
| id | The event id, a UUID. It is the same on every attempt and every replay of this event, so store it and skip any id you have already processed. |
| type | The event name, e.g. message.received. The same value is sent in the X-HonkIO-Event header. |
| created | When the event happened, ISO 8601 in UTC with milliseconds. A retry or a replay keeps the original value. |
| account_id | The HonkIO account the event belongs to. |
| livemode | true when a live key's action caused the event, false for a test key's simulation. Account and phone number events are always true. |
| data | The event's own fields, listed under each event below. |
Every event also carries a top-level livemode field: true when a live key's action caused it, false for a test key's simulation. Account and phone number events are always live.
Retries, dead letters and disabling
Answer with a 2xx within 10 seconds, directly: deliveries never follow a redirect, so a 3xx counts as a failed attempt, as does any other status, a timeout or a connection error. A failed event is retried once about a second later (skipped when the endpoint was already failing), then about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 16 hours apart: about 24 hours in all, up to eight attempts. Delivery is at least once from the first attempt: once an attempt has failed, the event is stored and its retries survive our restarts and deploys (a crash during the very first attempt can lose that one event). Every attempt is signed afresh and carries the same event id, so deduplicate on it. An event that fails every attempt goes to your dead-letter queue. Your endpoint stays on through all of this: it is disabled only once every attempt to it has failed for at least 24 hours, with no successful delivery in between, and at least 5 different events have failed; any successful delivery starts that over, and so does a failure more than 17 hours after the last one (the longest gap between retries, plus an hour). We email you an hour into a failure streak and again if the endpoint is disabled, and while it is failing GET /v1/webhooks/:id shows failing_since, failed_events and last_failure_reason. Events that failed before an endpoint was disabled are kept as dead letters you can replay; events raised while it is disabled are not delivered to it and are not stored. A SUSPENDED or CLOSED account works the same way: nothing is delivered to its endpoint, whatever would have gone out goes straight to your dead-letter queue instead, and every one of those is replayable again once the account is reinstated. List dead letters with GET /v1/webhooks/:id/dead-letters, send one again with POST /v1/webhooks/dead-letters/:id/replay (409 DEAD_LETTER_ALREADY_REPLAYED if it already went or a replay is in progress, or 409 ACCOUNT_SUSPENDED while the account is suspended or closed) or drop it with DELETE /v1/webhooks/dead-letters/:id, and re-enable the endpoint with POST /v1/webhooks/:id/reactivate. Replayed dead letters are kept for 90 days; every dead letter goes at the message-retention cutoff.
With the Node.js SDK, turn a disabled endpoint back on and replay what it missed:
// Turn a disabled endpoint back on. Missed events are not resent by this: replay them.
const { error: reactivateError } = await honkio.webhooks.reactivate('WEBHOOK_ID')
if (reactivateError) throw new Error(reactivateError.message)
// Dead letters not yet replayed, newest first.
const { data, error } = await honkio.webhooks.deadLetters('WEBHOOK_ID')
if (error) throw new Error(error.message)
for (const deadLetter of data.data) {
const { error: replayError } = await honkio.webhooks.replay(deadLetter.id)
if (replayError) console.error(deadLetter.event_type, replayError.name, replayError.message)
}
HonkIO