> ## 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.

# Authentication

> Getting a token, and what a token can do.

Everything the panel does, it does through this API. There is no private
interface behind it.

## A session token

```bash theme={null}
curl -X POST https://your-host/api/v1/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","password":"..."}'
```

Returns a JWT valid for twelve hours. Send it as a bearer token:

```bash theme={null}
curl https://your-host/api/v1/campaigns \
  -H "authorization: Bearer $TOKEN"
```

## An API key

For anything that isn't a person: your CRM, a script, a data export. Create one
under **Settings → API keys**. The secret is shown once, and it goes in the same
header:

```bash theme={null}
curl https://your-host/api/v1/cdrs?limit=10 \
  -H "authorization: Bearer ft_…"
```

There are two kinds:

* **Integration key.** Belongs to the organisation, not to whoever made it, so
  it keeps working when staff change. You choose what it may do from a preset:
  *Read calls & CDRs*, *Click-to-call*, *Campaigns*, *Contacts & tickets sync*,
  *Webhooks admin* or *Full access*. Even with full access it can't create API
  keys or people.
* **Personal key.** Acts as you, within your role.

<Warning>
  **A key can never exceed its owner.** Its permissions are intersected with the
  role of the person who created it, at the time each request is made. You
  cannot grant a key something you do not have, and a key does not keep a
  permission its owner has since lost. Keys can't create or change other keys;
  a person signed in to the panel does that.
</Warning>

### Every key needs an IP allowlist

A key works only from the addresses you list: single addresses or ranges
(`203.0.113.0/24`), up to 20 entries.

* **Refused when creating a key:**
  * "everything" (`0.0.0.0/0`);
  * ranges wider than /16 (IPv4) or /32 (IPv6);
  * private or local addresses. Your integration calls from its public address.
* **A request from anywhere else** gets `403 ip_not_allowed`, naming the address
  it came from. The panel shows each key's last refused address.
* **Keys made before allowlists were compulsory** keep working for 14 days,
  with a warning in the panel. After that they get `403 ip_allowlist_required`
  until you add their addresses. The panel offers the addresses each key has
  been used from.

You can change a key's allowlist, name and rate at any time. You can't change
its permissions; make a new key for different access.

### Rate limits

Each key may make **600 requests a minute** unless you set a different rate,
and **at most 60 call placements a minute** whatever its rate. Every response
carries:

| Header                  | Meaning                             |
| ----------------------- | ----------------------------------- |
| `X-RateLimit-Limit`     | The key's requests per minute       |
| `X-RateLimit-Remaining` | What is left this minute            |
| `X-RateLimit-Reset`     | When the minute ends (Unix seconds) |

Over the limit: `429 rate_limited` with `Retry-After` in seconds. Panel sessions
aren't rate limited.

### Retrying safely: Idempotency-Key

`POST /calls`, `/contacts`, `/tickets` and `/campaigns/{id}/contacts` accept an
`Idempotency-Key` header. Use any string up to 255 characters, unique per
action.

```bash theme={null}
curl -X POST https://your-host/api/v1/calls \
  -H "authorization: Bearer ft_…" -H "Idempotency-Key: crm-task-8812" \
  -H 'content-type: application/json' -d '{"extension":"1001","destination":"+61412345678"}'
```

* **Retrying with the same key and body** (after a timeout, say) returns the
  first answer again, with `Idempotent-Replayed: true`, instead of placing a
  second call.
* **The same key with a different body** is `409 idempotency_mismatch`.
* **While the first request is still running**, a retry gets
  `409 idempotency_in_progress`.
* Keys are kept for 24 hours. A request that failed with a server error isn't
  kept, so retrying it runs it again.

## Who am I

```bash theme={null}
curl https://your-host/api/v1/auth/me -H "authorization: Bearer $TOKEN"
```

Returns the identity **and the resolved permission list**. That list is what the
panel gates its screens on; there is no second copy of the rules in the browser.

## Errors

```json theme={null}
{ "error": { "code": "invalid_request", "message": "schedule_days are 0=Sunday to 6=Saturday" } }
```

The message is written to be shown to a person. Where a request is refused for a
reason somebody can act on, the reason is in the message rather than implied by
the code.

<Note>
  Unknown JSON fields are **rejected**, not ignored. A typo'd field name is a
  bug, and silently discarding it means discovering months later that a setting
  never applied. If you get *malformed JSON body* on a valid document, look for
  a misspelt key.
</Note>
