# Authentication

> Authenticate to the Sendai SMS API with an API key sent as a bearer token — how to create one, keep it secret, rotate it, and revoke it.

Every request to the Sendai SMS API is authenticated with an **API key**, sent as a bearer
token in the `Authorization` header:

```http
Authorization: Bearer sk_live_4c8e…
```

There is nothing else to configure — no signing, no session to keep alive, no expiry to
handle. A key is valid from the moment you create it until you revoke it.

## Create a key

Create keys from the **Settings → Developer** tab in the dashboard, or over the API. Give
each key a name that identifies the service using it — you will be glad of it when you come
to rotate.

```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"
  }
}
```

`data.id` is what you pass to the revoke endpoint — keep it alongside the key. `account_id`
tells you which account the key authenticates as.

> **`token` is returned exactly once.** It appears in this create response and nowhere else —
> Sendai stores only a hash. Put it straight into your secret manager. If you lose it, you
> cannot recover it; revoke the key and create another.

## Use it

Send the key as a bearer credential on every call:

```bash [cURL]
curl -X POST https://api.sendai.co.zw/api/v1/sms \
  -H "Authorization: Bearer $SENDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "263771000000", "from": "YOUR_SENDER_ID", "message": "Your code is 4821" }'
```

```js [Node.js]
const res = await fetch('https://api.sendai.co.zw/api/v1/sms', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.SENDAI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: '263771000000',
    from: 'YOUR_SENDER_ID',
    message: 'Your code is 4821',
  }),
})
```

```python [Python]
import os
import requests

res = requests.post(
    "https://api.sendai.co.zw/api/v1/sms",
    headers={"Authorization": f"Bearer {os.environ['SENDAI_API_KEY']}"},
    json={
        "to": "263771000000",
        "from": "YOUR_SENDER_ID",
        "message": "Your code is 4821",
    },
)
```

A missing, malformed, or revoked key returns `401`:

```json [401 Unauthorized]
{
  "status": "error",
  "message": "missing authentication",
  "error": { "code": "UNAUTHORIZED", "description": "missing authentication" }
}
```

## List your keys

Listing lets you audit which keys exist and what each is for:

```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",
      "Name": "orders-service",
      "Active": true,
      "LastUsedAt": "2026-08-06T11:04:19Z"
    }
  ]
}
```

> **Field names on this endpoint are PascalCase**, not snake_case like the rest of the API —
> it serialises the internal record directly. Read `ID`, `Name`, `Active`, `CreatedAt` and
> `LastUsedAt`; the response carries a few storage columns beyond these, which you should
> treat as unstable and not build against.

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

## Revoke a key

Revocation takes effect immediately — the next request using that key gets a `401`. Pass the
`ID` from the create or list response:

```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" }
```

A malformed id returns `400 invalid token id`. 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.

## Rotating without downtime

Because a key is valid until revoked, you can overlap the old and the new and never drop a
message:

1. **Create** the replacement key.
2. **Deploy** it to the service, alongside or in place of the old one.
3. **Confirm** traffic has moved — new sends succeed with the new key.
4. **Revoke** the old key.

Revoking before step 3 is what causes an outage. Keep the overlap short, but do keep it.

## Handling keys safely

- **One key per service.** Separate keys for your orders service, your marketing job, and
  your staging environment mean you can revoke one without touching the others.
- **Never commit a key.** Keep them in environment variables or a secret manager, not in
  source control, client-side JavaScript, or a mobile app — anything shipped to a user's
  device can be read by that user.
- **Never log the `Authorization` header.** Redact it in request logs and error reporters.
- **Rotate on a cadence**, and immediately if a key is exposed.

## Next steps

- Send your first message → [Quickstart](/guides/quickstart)
- Send to many recipients at once → [Bulk SMS](/guides/bulk-sms)
- The token endpoints in full → [API tokens](/api/api-tokens)
- Hand a key to an AI agent → [AI agents](/guides/mcp)
