# API tokens

> Create an API token for scripts and CI, keep it working with renewal and zero-downtime rotation, and revoke it when it is no longer needed.

Source: https://docs.fortly.com/account/tokens

An API token lets a script, a CI job or your own terminal call the Fortly API as you. Every route of the API at `https://api.fortly.com` accepts one in the `Authorization` header, except the MCP endpoint used by AI agents:

```bash
curl https://api.fortly.com/projects \
  -H "Authorization: Bearer $FORTLY_TOKEN"
```

A token's secret starts with `fly_`. The prefix is there so secret scanners, log filters and people reading a paste can recognise a Fortly token and catch one that is about to be committed.

## What a token can do

A token can do anything your account can do: create and delete projects, deploy, read and change data, manage the team, make more tokens. There are no per-token scopes, so treat a token like your password and give each one to a single holder.

It also reaches the projects of any account you have joined as a teammate, with the role you hold there; see [Team and roles](https://docs.fortly.com/account/team).

> **Note:**
>
> API tokens are not used for AI agents. An MCP client signs in through its own flow and gets a grant limited to the projects and tools you approve; see [Connect an AI agent](https://docs.fortly.com/agents/mcp). For browser code, use a project's publishable key; see the [frontend API](https://docs.fortly.com/data/frontend-api).

## Create a token

**Console**

Open **Settings**, find **API tokens** and choose **Create token**. Name it after whatever will hold it (for example "Deploy script"), so you know what breaks if you revoke it. Choose how long it lasts: 30 days, 90 days or 1 year.

The secret is shown once, in a dialog. Copy it before you close the dialog; closing discards it.

**API**

```bash
curl -X POST https://api.fortly.com/tokens \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "GitHub Actions", "days": 90}'
```

```json
{
  "token": {
    "id": "…",
    "name": "GitHub Actions",
    "created_at": 1791763200,
    "expires_at": 1799539200,
    "last_used_at": null,
    "previous_until": null
  },
  "secret": "fly_…",
  "note": "This is the only time this secret is shown. Store it now."
}
```

- `name` (string, required) — What holds the token. 1 to 60 characters.
- `days` (integer, optional) — How long it lasts, from 1 to 365. Defaults to 90.

Fortly keeps only a digest of the secret, so no page or route can show it again. If you lose it, [rotate](#rotate-the-secret) the token to get a new secret rather than making a second token.

Times in token responses are Unix timestamps in seconds.

## List your tokens

```bash
curl https://api.fortly.com/tokens \
  -H "Authorization: Bearer $FORTLY_TOKEN"
```

The reply is `{"tokens": [...]}`, newest first, with the same fields as above and never a secret. `last_used_at` is when the token last authenticated a request. `previous_until` is set while an old secret is still accepted after a rotation.

In the console, a token within 7 days of expiring shows **Expiring soon**.

## Renew a token

Renewing moves the expiry date and keeps the same secret, so nothing that holds the token has to change.

```bash
curl -X POST https://api.fortly.com/tokens/TOKEN_ID/renew \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"days": 90}'
```

The new expiry is counted from now, not from the old expiry, and the reply is `{"expires_at": ...}`. `days` is optional, from 1 to 365, and defaults to 90. The console's **Renew for 90 days** does the same.

An expired token is not deleted. Renewing it makes the same secret work again.

## Rotate the secret

Rotating issues a new secret for the same token and keeps the old secret working for a grace period, so you can deploy the new one before the old one stops.

```bash
curl -X POST https://api.fortly.com/tokens/TOKEN_ID/rotate \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"grace_hours": 24}'
```

```json
{
  "secret": "fly_…",
  "note": "The previous secret keeps working until its grace runs out."
}
```

- `grace_hours` (integer, optional) — How long the old secret keeps working, from 0 to 720 (30 days). Defaults to 24. Values outside that range are clamped to it.

The console's **Rotate secret** uses a grace of 24 hours. Rotating keeps the token's name, id and expiry.

> **Warning:**
>
> If a secret has leaked, rotate with `"grace_hours": 0`. The old secret stops working at once and the new one takes its place. To stop the token entirely instead, revoke it.

## Revoke a token

Revoking deletes the token. Anything still using it fails from the next request, and there is no way to bring the same secret back.

```bash
curl -X DELETE https://api.fortly.com/tokens/TOKEN_ID \
  -H "Authorization: Bearer $FORTLY_TOKEN"
```

The reply is `204 No Content`. In the console, choose **Revoke** from the token's menu.

## Routes

- `GET /tokens` — Your tokens, newest first, without secrets
- `POST /tokens` — Create one. The secret is in this reply only
- `POST /tokens/{id}/renew` — Move the expiry, keeping the secret
- `POST /tokens/{id}/rotate` — Issue a new secret, keeping the old one for a grace period
- `DELETE /tokens/{id}` — Revoke it

## Errors

| Status | Message | Why |
|---|---|---|
| `401` | `sign in first` | No `Authorization: Bearer` header |
| `401` | `that token is not valid, or has expired` | The secret is wrong, revoked, expired, or an old secret past its grace |
| `400` | `give the token a name, so it can be told from the others` | `name` is empty |
| `400` | `that name is too long` | `name` is over 60 characters |
| `400` | `a token lasts between 1 and 365 days` | `days` is out of range |
| `400` / `404` | `no such token` | The id is not one of your tokens (`400` from renew and rotate, `404` from revoke) |

## Keep tokens safe

- Store a token in your CI provider's secret store or a password manager, never in a repository.
- Make one token per holder. Then revoking one breaks one thing, and `last_used_at` tells you whether a token is still in use.
- Pick the shortest life that works, and renew it before it lapses.
