Which secret signs what
Both use the same signature scheme and the
same
X-Mirai-Signature header. Only the secret differs.
Every signed delivery also carries X-Mirai-Signed-With, which names the secret
that signed it:
If your server receives webhooks from both, read this header first and pick the
secret it names.
One URL, one secret
A URL receives each event once, signed with one secret. If a call’swebhook_url is the same URL as one of your enabled destinations,
the destination takes over that URL:
- When a subscription on that destination already covers the event for this call (the whole workspace, or the call’s agent), the destination delivers it. We do not send the per-call copy, so the event arrives once.
- When no subscription covers the event, we still send the per-call copy, but sign it with the Destination secret, so that URL only ever sees one secret.
webhook_url, including the ones set on inbound
phone numbers and WhatsApp calls. Only enabled destinations count: a disabled
destination never takes over a URL. A per-call webhook_url that matches no
destination is signed the same way as before.
Our recommendation
Use destinations. You set the URL once, choose events for one agent or the whole workspace, and see every delivery and retry in the console. Keepwebhook_url for simple one-off integrations, such as a script
that places a few calls and wants their results.
If you already pass webhook_url on every call and have now created a
destination for the same URL, you can remove webhook_url from your call
requests.
A webhook failed its signature check
Your server answered401 or 403, or your logs show a signature mismatch.
Work through these in order.
- Check
X-Mirai-Signed-With. It names the secret that signed this delivery. A server that holds only the Destination secret will reject deliveries signed with the Call webhook secret, and the other way round. - Verify with that secret. Use the full
whsec_…value exactly as it was shown, with no spaces or newlines. - Sign the raw body. Compute
HMAC-SHA256(secret, t + "." + raw_body)over the exact bytes you received, before any JSON parsing, and compare the lowercase hex withv1fromX-Mirai-Signature: t=…,v1=…. A body that was parsed and serialised again will not match. See raw body. - Check the time window. Reject only when
tis more than 300 seconds from your server’s clock. If good deliveries fail this check, sync your clock with NTP. - Run the test vector. Check your code against the test vector.
- The Call webhook_url panel lists each
webhook_urlyour calls used in the last 7 days, with its recent attempts, status codes and which secret signed them. - Deliveries lists every delivery to your destinations, with each attempt.
Read per-call deliveries with the API
GET /v2/webhooks/call-deliveries lists recent attempts to deliver to the
webhook_url passed on calls, newest first. Deliveries to destinations are under
GET /v2/webhooks/deliveries.
Response
When
has_more is true, the response also has next_cursor.
We keep the newest 500 records per workspace, for up to 30 days.
GET /v2/webhooks/settings
also returns call_webhook_urls: each distinct webhook_url your calls used in
the last 7 days.
last_status is the outcome of the latest delivery: delivered, failed or
superseded. last_http_status and last_hint appear when there is a value.
failures_24h counts failed deliveries in the last 24 hours. endpoint_id is the
enabled destination with the same URL, or null when there is none. When it is set, that destination now handles the URL as described in
one URL, one secret.