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

# Integrations

> Look callers up in your own system during a call, and receive call events as they happen.

Two ways to connect FireTone to your own systems. Both are in **Settings →
Integrations**:

* **HTTP connections**: your IVR asks your system about a caller mid-call,
  using the [Fetch data](/tenant/design/ivr-flows#fetch-data) step.
* **Webhooks**: FireTone tells your system when a call starts, is answered
  and ends.

Both only call **public `https://` addresses**. FireTone refuses private,
loopback and cloud-internal addresses, even when a hostname resolves to one
or a redirect points there.

## HTTP connections

A connection is a **base URL** and the **headers** your system needs, usually
`Authorization: Bearer …` or an API-key header. A Fetch-data step picks a
connection and adds its own path, so the key lives in one place, not in every
flow.

* Header values are stored encrypted and **never shown again**. When you edit
  a connection, leave a saved header blank to keep it.
* Turn on **signing** to have every request carry `X-FireTone-Signature`, so
  your system can check it really came from FireTone (see
  [below](#checking-a-signature)).
* Every request also carries `X-FireTone-Call: <call id>`.
* **Test** runs a request and shows the answer.

## Webhooks

A webhook sends the events you pick to your URL, as a JSON `POST`:

| Event                                               | When                                                                                                                                |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `call.started`                                      | A call reached FireTone: inbound, outbound, or placed by a campaign or the API                                                      |
| `call.ringing`                                      | An agent's phone is ringing                                                                                                         |
| `call.answered`                                     | Someone, or the IVR, picked up                                                                                                      |
| `call.ended`                                        | The call is over. Includes the duration, the hangup cause, whether it was answered and recorded, its cost, and the flow's variables |
| `call.missed`                                       | An inbound call was offered to an extension or queue, nobody answered it, and no voicemail was left                                 |
| `voicemail.received`                                | A caller left a voicemail. Includes `audio_url`                                                                                     |
| `recording.ready`                                   | A call's recording is saved. Includes `recording_url`                                                                               |
| `conversation.completed`                            | An AI agent's call ended. Includes the summary, turn count and review flags                                                         |
| `callback.requested`                                | An AI agent arranged a callback: the ticket, the number, the question and who will call                                             |
| `ticket.created` / `ticket.updated`                 | A ticket was raised, or changed (`change`: `fields` or `update_added`)                                                              |
| `contact.created`                                   | A new contact came from a call, an import or the API (`source`)                                                                     |
| `campaign.contact.completed` / `campaign.completed` | A campaign finished with one contact, or finished altogether                                                                        |
| `csat.submitted`                                    | A caller answered the satisfaction survey                                                                                           |

```json theme={null}
{
  "id": "5b0c…",
  "event": "call.ended",
  "occurred_at": "2026-09-14T13:50:10Z",
  "organisation_id": "f1be…",
  "data": {
    "call_uuid": "b0ad…", "caller": "+919216217231", "destination": "+918600600903",
    "direction": "inbound", "answered": true, "billsec": 42, "hangup_cause": "NORMAL_CLEARING",
    "variables": { "acct": "4711", "tier": "vip" }
  }
}
```

* **Nothing is lost.** Every event is saved before it is sent, so a restart,
  or your system being down, doesn't drop it.
* **Delivery:** a `2xx` answer counts as delivered. Anything else is retried
  after 30 seconds, 5 minutes, 30 minutes, 2 hours and 12 hours (six tries in
  about 15 hours), then marked failed.
* **Deliveries** lists every attempt for 14 days: its status, the answer, and
  the exact body that was sent.
* **Send again** re-queues a delivery, for example after you've fixed your
  system. It sends the same body with the same `id`.
* **Switched off after three days.** A webhook whose deliveries have all failed
  for three days is switched off. It shows why, and your organisation's admins
  are emailed. Switch it back on once your system is fixed.
* **Duplicates:** a retry carries the same `id` (also sent as
  `X-FireTone-Delivery`), so your system can ignore a repeat.
* **Order:** events are sent in the order they happened, but a retry can arrive
  after later events. Use `occurred_at` if order matters.
* **Secret:** each webhook has its own signing secret, shown **once** when it
  is created or rotated.
* **Test:** **Send test** posts a `webhook.test` event, signed like a real
  one.

## Checking a signature

The full event reference, with every payload and PHP as well, is in the
[API documentation](/api/webhooks).

Every signed request carries:

```
X-FireTone-Signature: t=1789390078,v1=7937d6d8…
```

`v1` is the hex HMAC-SHA256 of `"<t>.<raw body>"`, keyed with your secret.
Check it against the raw bytes of the body, before parsing, and reject a `t`
more than a few minutes old.

<CodeGroup>
  ```js Node theme={null}
  const crypto = require("crypto");

  function verified(secret, header, rawBody) {
    const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
    const mac = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
    const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
    return fresh && typeof v1 === "string" && v1.length === mac.length &&
      crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
  }
  ```

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

  def verified(secret: bytes, header: str, raw_body: bytes) -> bool:
      parts = dict(p.split("=", 1) for p in header.split(","))
      mac = hmac.new(secret, f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
      fresh = abs(time.time() - int(parts["t"])) < 300
      return fresh and hmac.compare_digest(mac, parts["v1"])
  ```
</CodeGroup>
