# Keys

The two kinds of API key, and how to rotate one.

Every request carries an API key as a bearer token. There are two kinds, and an operation takes only one of them.

## Two kinds of key

|             | Server key                                                                                                                                | Account key                                                                                                                      |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Starts with | `sk_`, or `sk_test_` on a [test server](https://sendora.se/docs/test-servers)                                                                               | `ak_`                                                                                                                            |
| Belongs to  | One server                                                                                                                                | The account                                                                                                                      |
| Does        | Sends, reads the message log, and manages the server's streams, suppressions, webhooks and its own keys. An SMTP client signs in with it. | Creates, renames and deletes servers, manages every server's keys, and manages the sending domains.                              |
| Never       | Manages the account's servers or domains                                                                                                  | Sends or reads mail                                                                                                              |
| Made        | With its server, or by the server's own key or the account key                                                                            | By an administrator in the dashboard, under [Account › API keys](https://app.sendora.se/account/tokens). The API cannot make one. |

Your application holds a server key. Keys have no narrower scopes: every key of a server can do all that a server key does. A key of the wrong kind fails with `403 wrong_token_kind`, and the message names the kind the operation needs.

With the TypeScript SDK:

The SDK has a client for each: `Sendora` takes a server key, and `SendoraAccount` an account key. Keep both in the environment on the server, as `SENDORA_API_TOKEN` and `SENDORA_ACCOUNT_TOKEN`. Never ship either to a browser.

With the Python SDK:

The SDK has a client for each: `Sendora` takes a server key, and `SendoraAccount` an account key. Keep both in the environment on the server, as `SENDORA_API_TOKEN` and `SENDORA_ACCOUNT_TOKEN`. Each client reads its own variable when you give it no key.

## A server comes with its first key

A new server comes with a default transactional stream and its first key, whether you create it in the dashboard or with the account key. The response shows the key's value, and only this once:

cURL:

```bash
curl -X POST https://api.sendora.se/v1/servers \
  -H "Authorization: Bearer $SENDORA_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Invoicing"
}'
```

TypeScript:

```ts
import { SendoraAccount } from '@sendora/sdk';

const account = new SendoraAccount({
  token: process.env.SENDORA_ACCOUNT_TOKEN,
});

const server = await account.servers.create({
  name: 'Invoicing',
});

console.log(server.serverId);
console.log(server.token.token);
```

Python:

```python
import os

from sendora import SendoraAccount

account = SendoraAccount(
    os.environ["SENDORA_ACCOUNT_TOKEN"]
)

server = account.servers.create(name="Invoicing")

print(server.server_id)
print(server.token.token)
```

Response:

```json
{
  "serverId": "7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70",
  "name": "Invoicing",
  "mode": "live",
  "createdAt": "2026-09-20T12:00:00.000Z",
  "token": {
    "tokenId": "3b0c9e2f-6d4a-4e8b-8a1c-9f2d7e6c5b40",
    "name": "default",
    "prefix": "sk_a1b2c3d4e5",
    "createdAt": "2026-09-20T12:00:00.000Z",
    "revokedAt": null,
    "token": "sk_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v"
  }
}
```

Store `token.token` in your secret store. It is never shown again.

## Create and list keys

A server holds at most two live keys: the one in use and the one being rotated in. The account holds at most two live account keys the same way. A third fails with `409 token_limit`. A key's name only helps people tell the keys apart.

Create a key:

cURL:

```bash
curl -X POST https://api.sendora.se/v1/servers/{serverId}/tokens \
  -H "Authorization: Bearer $SENDORA_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Invoicing system"
}'
```

TypeScript:

```ts
import { SendoraAccount } from '@sendora/sdk';

const account = new SendoraAccount({
  token: process.env.SENDORA_ACCOUNT_TOKEN,
});

const key = await account.servers.tokens.create(
  '7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70',
  { name: 'Invoicing system' },
);

console.log(key.token);
```

Python:

```python
import os

from sendora import SendoraAccount

account = SendoraAccount(
    os.environ["SENDORA_ACCOUNT_TOKEN"]
)

key = account.servers.tokens.create(
    "7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70",
    name="Invoicing system",
)

print(key.token)
```

Response:

```json
{
  "tokenId": "9f2d7e6c-5b40-4e8b-8a1c-3b0c9e2f6d4a",
  "name": "Invoicing system",
  "prefix": "sk_c3d4e5f6g7",
  "createdAt": "2026-09-20T12:05:00.000Z",
  "revokedAt": null,
  "token": "sk_c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x"
}
```

List a server's keys:

cURL:

```bash
curl https://api.sendora.se/v1/servers/{serverId}/tokens \
  -H "Authorization: Bearer $SENDORA_ACCOUNT_TOKEN"
```

TypeScript:

```ts
import { SendoraAccount } from '@sendora/sdk';

const account = new SendoraAccount({
  token: process.env.SENDORA_ACCOUNT_TOKEN,
});

const { tokens } = await account.servers.tokens.list(
  '7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70',
);

for (const key of tokens) {
  console.log(
    key.name,
    key.prefix,
    key.revokedAt ? 'revoked' : 'live',
  );
}
```

Python:

```python
import os

from sendora import SendoraAccount

account = SendoraAccount(
    os.environ["SENDORA_ACCOUNT_TOKEN"]
)

listed = account.servers.tokens.list(
    "7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70"
)

for key in listed.tokens:
    print(
        key.name,
        key.prefix,
        "revoked" if key.revoked_at else "live",
    )
```

Response:

```json
{
  "tokens": [
    {
      "tokenId": "3b0c9e2f-6d4a-4e8b-8a1c-9f2d7e6c5b40",
      "name": "default",
      "prefix": "sk_a1b2c3d4e5",
      "createdAt": "2026-09-20T12:00:00.000Z",
      "revokedAt": null
    },
    {
      "tokenId": "9f2d7e6c-5b40-4e8b-8a1c-3b0c9e2f6d4a",
      "name": "Invoicing system",
      "prefix": "sk_c3d4e5f6g7",
      "createdAt": "2026-09-20T12:05:00.000Z",
      "revokedAt": null
    }
  ]
}
```

These samples use the account key. A server key manages its own server's keys the same way:

| Key         | Manages the keys of       | Path                            |
| ----------- | ------------------------- | ------------------------------- |
| Server key  | Its own server            | `/v1/tokens`                    |
| Account key | Any server of the account | `/v1/servers/{serverId}/tokens` |

With the TypeScript SDK:

With a server key, `sendora.tokens.create({ name })`, `sendora.tokens.list()` and `sendora.tokens.revoke(tokenId)` manage the server's own keys.

With the Python SDK:

With a server key, `sendora.tokens.create(name=...)`, `sendora.tokens.list()` and `sendora.tokens.revoke(token_id)` manage the server's own keys.

## Rotation is create, switch, revoke

1. Create the new key.
2. Move your systems to it; both keys work meanwhile.
3. Revoke the old one. It stops working at once.

cURL:

```bash
curl -X DELETE https://api.sendora.se/v1/servers/{serverId}/tokens/{tokenId} \
  -H "Authorization: Bearer $SENDORA_ACCOUNT_TOKEN"
```

TypeScript:

```ts
import { SendoraAccount } from '@sendora/sdk';

const account = new SendoraAccount({
  token: process.env.SENDORA_ACCOUNT_TOKEN,
});

await account.servers.tokens.revoke(
  '7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70',
  '3b0c9e2f-6d4a-4e8b-8a1c-9f2d7e6c5b40',
);
```

Python:

```python
import os

from sendora import SendoraAccount

account = SendoraAccount(
    os.environ["SENDORA_ACCOUNT_TOKEN"]
)

account.servers.tokens.revoke(
    "7c1e4d2a-0b9f-4a3e-8d6c-5e2f1a9b8c70",
    "3b0c9e2f-6d4a-4e8b-8a1c-9f2d7e6c5b40",
)
```

The last live key of a server or of the account cannot be revoked (`409 last_token`), so nothing is ever locked out by mistake. A revoked key stays in the list with its `revokedAt`, so the history is visible.

If a key leaks, rotate it at once. You cannot revoke a server's only key, so create the new key first. For an account key, an administrator does the same under [Account › API keys](https://app.sendora.se/account/tokens). Keys are stored hashed and never shown again, so revoke any key you cannot account for.
