Tone docs
Flows

Webhook delivery and rotation

What we send, how often we retry, and the one thing that breaks verification during a rotation.

See Webhooks and events for the event catalog.

A delivery

Header
x-tone-eventThe event type
x-tone-event-idStable across retries — dedupe on this
x-tone-timestampUnix seconds, covered by the signature
x-tone-signatureOne or more space-delimited v1=<hex> signatures
x-tone-delivery-attempt1 on the first send

Verify HMAC-SHA256 of the literal {timestamp}.{raw body} — the raw body, before any JSON parsing. Re-serialising first changes the bytes and the signature will never match.

The URL path is deliberately not signed, so a proxy or a path rewrite in front of your endpoint does not break verification.

Answer within 10 seconds

Do the minimum: verify, enqueue, return 2xx. A slow handler does not merely time out — it manufactures duplicates, because a retry is indistinguishable from a first delivery to a handler that already did the work.

Your responseWe
2xxConsider it delivered
4xx, except 408 and 429Give up — you said it was malformed
5xx, 408, 429, timeoutRetry

The retry ladder

1, 2, 4, 8, 16, 32, 64, 128 minutes, then every 4 hours — 14 attempts over roughly a day, each with ±20% jitter.

⚠️ 20 consecutive failures across all deliveries disables the endpoint, with an email to owners and admins. Re-enable it with a PATCH once you have fixed the receiver.

Rotating the secret

curl "$TONE_API/v1/integrations/webhooks/$ID/roll-secret" -X POST \
  -H "Authorization: Bearer $TONE_KEY"

🔴 During the 24-hour overlap, x-tone-signature carries SEVERAL space-delimited signatures. A verifier that treats the header as one string starts rejecting everything the moment you rotate. Split on whitespace and accept the delivery if any signature matches:

const ok = header.split(' ').some((sig) =>
  timingSafeEqual(Buffer.from(sig), Buffer.from(`v1=${expected}`)));

Write it that way before you ever rotate, not during.

When you miss one

Deliveries are at-least-once and unordered. If your receiver was down past the retry window:

curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/events?type=call.completed"
curl -H "Authorization: Bearer $TONE_KEY" "$TONE_API/v1/integrations/webhooks/deliveries"

30-day retention. POST /deliveries/{id}/retry re-sends immediately with the same event id.

⚠️ GET /v1/events/{id} re-renders the payload from current state rather than replaying the signed bytes. A transcript that landed after delivery is present here and was absent from what your endpoint received. Use it to understand an event, not to re-verify a signature.

Was this page helpful?

On this page