# API tokens

> Create, list, and revoke the API keys that authenticate every request. The key secret is returned once, at creation, and never again.

API keys are the only credential on the API. Create your first key in the dashboard under
**Settings → Developer**; once you hold one, you can manage keys over the API itself. For
rotation runbooks and safe handling, see [Authentication](/guides/authentication).

There is no single canonical token object: the **create** response is a snake_case object
carrying the secret once, while the **list** response serialises the internal record with
PascalCase field names. Each shape is documented with its operation below rather than
pretended into one.

## Create an API key

`POST /api/v1/api-tokens`

Creates a key. **`data.token` is the secret and is returned only here** — Sendai stores a
hash, so a lost key cannot be recovered; revoke it and create another.

### Request body

- **`name`** (`string`, required)
  Identifies the key. Name it for the service that will use it — you will be glad of it when
  you come to rotate.

### Response

`data.id` is what you pass to the revoke endpoint — keep it alongside the key. `account_id`
is the account the key authenticates as; the account-scoped
[Balances](/api/balances) and [Sender IDs](/api/sender-ids) endpoints take it in their path.

### Errors

| Status | When |
| --- | --- |
| `400` | `name` is missing — listed in `error.validation_errors`. |
| `401` | Missing, malformed, or revoked API key. |

```bash [cURL]
curl -X POST https://api.sendai.co.zw/api/v1/api-tokens \
  -H "Authorization: Bearer $SENDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "orders-service" }'
```

```js [Node.js]
const res = await fetch('https://api.sendai.co.zw/api/v1/api-tokens', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.SENDAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name: 'orders-service' }),
})

const { data } = await res.json()
// data.token is shown only in this response — store it now
```

```python [Python]
import os
import requests

res = requests.post(
    "https://api.sendai.co.zw/api/v1/api-tokens",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
    json={"name": "orders-service"},
)

data = res.json()["data"]
# data["token"] is shown only in this response — store it now
```

```json [201 Created]
{
  "status": "success",
  "message": "api token created — save this token, it will not be shown again",
  "data": {
    "id": "9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b",
    "name": "orders-service",
    "token": "sk_live_4c8e6b21d0f34a97b5e8c1d2f6a3b904",
    "account_id": "2d6f4b8e-1a90-4e21-8b7d-3f1a5c2e9a0c",
    "created_at": "2026-07-06T09:12:03Z"
  }
}
```

## List API keys

`GET /api/v1/api-tokens`

Lists the keys on your account. **This response is not normalised**: it serialises the
internal record directly, so field names are **PascalCase** rather than snake_case and
include storage columns.

Read `ID`, `Name`, `Active`, `CreatedAt` and `LastUsedAt`; treat everything else (such as
`TokenHash`) as unstable and do not build against it. Key material is never returned.

`LastUsedAt` is the useful one for cleanup: `null` means never used, and a key untouched for
months is usually one you can revoke.

### Errors

| Status | When |
| --- | --- |
| `401` | Missing, malformed, or revoked API key. |

```bash [cURL]
curl https://api.sendai.co.zw/api/v1/api-tokens \
  -H "Authorization: Bearer $SENDAI_API_KEY"
```

```js [Node.js]
const res = await fetch('https://api.sendai.co.zw/api/v1/api-tokens', {
  headers: { 'Authorization': `Bearer ${process.env.SENDAI_API_KEY}` },
})

const { data: keys } = await res.json() // PascalCase fields — ID, Name, Active…
```

```python [Python]
import os
import requests

res = requests.get(
    "https://api.sendai.co.zw/api/v1/api-tokens",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
)

keys = res.json()["data"]  # PascalCase fields — ID, Name, Active…
```

```json [200 OK]
{
  "status": "success",
  "message": "",
  "data": [
    {
      "ID": "9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b",
      "CreatedAt": "2026-07-06T09:12:03Z",
      "UpdatedAt": null,
      "DeletedAt": null,
      "Name": "orders-service",
      "TokenHash": "9b74c9897bac770ffc029102a200c5de0d3d2a2f4b2b6c9a2f0b4b2c1d8e3f7a",
      "AccountId": "2d6f4b8e-1a90-4e21-8b7d-3f1a5c2e9a0c",
      "Active": true,
      "LastUsedAt": "2026-08-06T11:04:19Z"
    }
  ]
}
```

## Revoke an API key

`DELETE /api/v1/api-tokens/{id}`

Revokes immediately — the next request using that key gets a `401`.

### Path parameters

- **`id`** (`string (uuid)`, required)
  The key's `ID`, from the create or list response.

### Response

Note that revoking an id that does not exist does **not** return `404` — treat a `200` as
"this key is now not valid" rather than as proof the key existed.

### Errors

| Status | When |
| --- | --- |
| `400` | `id` is not a valid UUID. |
| `401` | Missing, malformed, or revoked API key. |

```bash [cURL]
curl -X DELETE https://api.sendai.co.zw/api/v1/api-tokens/9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b \
  -H "Authorization: Bearer $SENDAI_API_KEY"
```

```js [Node.js]
const id = '9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b'

await fetch(`https://api.sendai.co.zw/api/v1/api-tokens/${id}`, {
  method: 'DELETE',
  headers: { 'Authorization': `Bearer ${process.env.SENDAI_API_KEY}` },
})
```

```python [Python]
import os
import requests

key_id = "9f2a4c8e-6b21-4d0f-a97b-5e8c1d2f6a3b"

requests.delete(
    f"https://api.sendai.co.zw/api/v1/api-tokens/{key_id}",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
)
```

```json [200 OK]
{ "status": "success", "message": "api token revoked" }
```
