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

# Webhooks

> Get told when calls happen, instead of polling.

Register a URL and Qall posts to it when something happens on a call.

## Setting one up

Go to **Organisation → Webhooks**, add your URL, and tick the events you want.

You get a **signing secret** when you create the endpoint. It's shown once — copy
it then. After that you only see a hint, and you can generate a new one if it's
lost.

## Events

<CardGroup cols={2}>
  <Card title="call.completed" href="/api-reference/webhooks/call-completed">
    The conversation ended. Carries the summary, transcript and outcomes.
  </Card>

  <Card title="recording.ready" href="/api-reference/webhooks/recording-ready">
    The audio file is ready to download.
  </Card>

  <Card title="call.status_updated" href="/api-reference/webhooks/call-status-updated">
    A call changed state while it was running.
  </Card>

  <Card title="call.transferred" href="/api-reference/webhooks/call-transferred">
    A caller was handed to a person.
  </Card>
</CardGroup>

Most integrations want `call.completed`, plus `recording.ready` if you store the
audio.

## What every delivery looks like

```json theme={null}
{
  "id": "4f3c1e8a9b2d4c7e8f1a2b3c4d5e6f70",
  "type": "call.completed",
  "created_at": "2026-10-02T09:14:21.481000+00:00",
  "message": { }
}
```

`message` differs per event — see the pages above.

| Header | |
| - | - |
| `Qall-Event-Id` | The event id. Same on a retry, so use it to avoid handling one twice. |
| `Qall-Event-Type` | The event type, so you can route before parsing. |
| `Qall-Signature` | `t=<timestamp>,v1=<signature>` |

## Checking the signature

The signature is an HMAC-SHA256 of `"{timestamp}.{body}"` using your signing
secret.

<Warning>
  Check it against the **raw request body**, exactly as received. Parsing the JSON
  and re-encoding it changes the byte order and the signature won't match. This is
  the one thing that catches everyone out.
</Warning>

Reject anything with a timestamp more than five minutes old.

```python theme={null}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    timestamp, signature = parts.get("t", ""), parts.get("v1", "")
    if not timestamp or not signature:
        return False
    if abs(time.time() - int(timestamp)) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

In FastAPI that's `await request.body()`. In Express, mount
`express.raw({ type: "application/json" })` on the route.

## Retries

Anything that isn't a 2xx, and anything that times out, is retried — quickly at
first, then with longer gaps, over about a day and a half. After that the delivery
is given up on.

Because retries reuse the event id, **your handler needs to cope with receiving
the same event twice.**

An endpoint that keeps failing is switched off, so a receiver that's been down for
a long time doesn't get hammered when it comes back. Turn it on again once you've
fixed it.

Answer quickly — acknowledge with a 2xx and do your work afterwards. A slow
handler gets treated as a failure.

## Testing

**Send test event** delivers a signed `test.ping` to your endpoint and records it
like any other delivery.

**Deliveries** shows recent attempts with the status code, the response and the
timing — the first place to look when something hasn't arrived.


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