> ## Documentation Index
> Fetch the complete documentation index at: https://docs.miraiminds.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Which webhook secret do I use?

> Per-call webhook_url and webhook destinations are signed with different secrets. See which one signed a delivery, why an event never arrives twice at one URL, and how to fix a failed signature check.

There are two ways to receive webhooks, and each has its own secret.

## Which secret signs what

| How you receive webhooks | Signed with | Where you find the secret |
| :- | :- | :- |
| A `webhook_url` you pass when you [create a call](https://docs.miraiminds.co/v2/calls.md#create-a-call) (or on a campaign or phone number) | The **Call webhook secret** of your API key | Shown when you create a key, under **Developers → API keys**. The secret is the same for every key in the workspace. |
| A [destination](https://docs.miraiminds.co/v2/webhook-subscriptions.md) you set up under **Webhooks**, with subscriptions | That destination's **Destination secret** | Shown once when you create the destination, as `signing_secret` |

Both use the same [signature scheme](https://docs.miraiminds.co/v2/webhooks.md#signature-verification) 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:

```http theme={"system"}
X-Mirai-Signed-With: whe_3f9c2a71d4b84e0f9a6b1c2d3e4f5a6b
```

| Value | Verify with |
| :- | :- |
| `whe_…` | The Destination secret of that destination |
| `api_key` | Your API key's Call webhook secret. Calls, campaigns, phone number events and the `webhook` post-call tool use it. |
| A phone connection id | The signing secret of the phone connection behind that inbound number |
| `whatsapp_integration` | Your WhatsApp integration's signing secret |

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's `webhook_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.

URLs match when they are the same after we lowercase the scheme and host, drop a
default port and ignore a trailing slash. The query string must match exactly.

This applies to every per-call `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](https://docs.miraiminds.co/v2/webhook-subscriptions.md). You set the URL once, choose
events for one agent or the whole workspace, and see every delivery and retry in
the console. Keep `webhook_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 answered `401` or `403`, or your logs show a signature mismatch.
Work through these in order.

1. **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.
2. **Verify with that secret.** Use the full `whsec_…` value exactly as it was
   shown, with no spaces or newlines.
3. **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 with `v1` from `X-Mirai-Signature: t=…,v1=…`. A body that was parsed and
   serialised again will not match. See [raw body](https://docs.miraiminds.co/v2/webhooks.md#raw-body).
4. **Check the time window.** Reject only when `t` is more than 300 seconds from
   your server's clock. If good deliveries fail this check, sync your clock with
   NTP.
5. **Run the test vector.** Check your code against the
   [test vector](https://docs.miraiminds.co/v2/webhooks.md#test-vector).

To see failed deliveries in the console, open **Webhooks**:

* The **Call webhook\_url** panel lists each `webhook_url` your 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.

A call's detail page shows the webhook deliveries for that call, of both kinds.

## Read per-call deliveries with the API

[`GET /v2/webhooks/call-deliveries`](https://docs.miraiminds.co/api-reference/webhooks/list-call-webhook_url-deliveries) lists recent attempts to deliver to the
`webhook_url` passed on calls, newest first. Deliveries to destinations are under
[`GET /v2/webhooks/deliveries`](https://docs.miraiminds.co/v2/webhook-subscriptions#deliveries-and-retries).

```bash theme={"system"}
curl "https://sandbox.voice.miraiminds.co/v2/webhooks/call-deliveries?outcome=failed&limit=20" \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY"
```

```json Response theme={"system"}
{
  "data": [
    {
      "id": "cwd_01M5Z2Q8R4T6V8X0Z2B4D6F8H0",
      "object": "call_webhook_delivery",
      "call_id": "call_01M5Z2N6P8R0T2V4X6Z8B0D2F4",
      "event_id": "evt_01M5Z2P7Q9S1U3W5Y7A9C1E3G5",
      "event_type": "call.completed",
      "url": "https://example.com/mirai/webhook",
      "signed_with": "api_key",
      "outcome": "failed",
      "http_status": 401,
      "hint": "signature_rejected",
      "error": "http_401",
      "attempt": 3,
      "at": "2026-10-27T09:16:40Z"
    }
  ],
  "has_more": false
}
```

| Query | Meaning |
| :- | :- |
| `limit` | 1 to 100. Defaults to 50. |
| `cursor` | The `next_cursor` of the previous page, to read the next one. |
| `call_id` | Only this call's deliveries. |
| `outcome` | `delivered`, `failed` or `superseded`. |

| Field | Meaning |
| :- | :- |
| `outcome` | `delivered` (your server answered `2xx`), `failed`, or `superseded` (a destination for the same URL delivered this event instead). |
| `signed_with` | The secret that signed it, the same value as `X-Mirai-Signed-With`. On a `superseded` record, the destination that delivered it. |
| `endpoint_id` | The destination with the same URL, when there is one. |
| `http_status` | Your server's status code, when it answered. |
| `hint` | `signature_rejected` when your server answered `401` or `403`. Follow [the steps above](#a-webhook-failed-its-signature-check). |
| `error` | `http_<status>`, `timeout` or `network_error`. |
| `attempt` | Which attempt this was, starting at 1. |

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`](https://docs.miraiminds.co/v2/webhook-subscriptions#low-balance-alert-level)
also returns `call_webhook_urls`: each distinct `webhook_url` your calls used in
the last 7 days.

```json theme={"system"}
{
  "call_webhook_urls": [
    {
      "url": "https://example.com/mirai/webhook",
      "last_used_at": "2026-10-27T09:16:38Z",
      "endpoint_id": "whe_3f9c2a71d4b84e0f9a6b1c2d3e4f5a6b",
      "last_status": "delivered",
      "last_http_status": 200,
      "failures_24h": 0
    }
  ]
}
```

`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](#one-url-one-secret).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.