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. 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.
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.
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 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:
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.
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. Keys are stored hashed and never shown again, so revoke any key you cannot account for.
# 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.