Your account is approved, and payment is active. Activate payment under Account › Billing once you are approved. Until then a live server’s sends fail, with the codes on Errors.
The domain you send from is verified, as Domains explains. The Domains page shows its two DNS records: a CNAME for the return path and a TXT record for the DKIM key.
To try this without sending real mail, use a test server and its key. A test server works before your account is approved and before payment.
Sign in to the dashboard and open your server under Servers. Signup made a server named default, without a key. Create a key on its API keys tab. The key is shown once, so put it in your secret store. Keys explains the two kinds of key.
Install the SDK. It has no dependencies, is ESM, and runs on Node 20.19 or newer, Bun, Deno and Cloudflare Workers:
npm install @sendora/sdk
Keep the key in the environment on the server, as SENDORA_API_TOKEN below. Never ship it to a browser. The client throws when you create it without a key.
Install the SDK. It runs on Python 3.11 or newer and has one dependency, httpx2:
pip install sendora
Pin the major version you test with, such as sendora~=1.0 in your requirements. Only a new major version may break your code.
Keep the key in the environment on the server, as SENDORA_API_TOKEN below. Sendora() reads it from there when you give it no key, and raises ValueError when there is none.
curl -X POST https://api.sendora.se/v1/email \
-H "Authorization: Bearer $SENDORA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": {
"email": "no-reply@example.se",
"name": "Example AB"
},
"to": [
"anna@example.com"
],
"subject": "Your account is ready",
"text": "Hi Anna, your account at Example is ready. Sign in at https://example.se/login.",
"tag": "welcome"
}'
import { Sendora } from'@sendora/sdk';
const sendora = newSendora({
token: process.env.SENDORA_API_TOKEN,
});
const { messageId } = await sendora.email.send({
from: {
email: 'no-reply@example.se',
name: 'Example AB',
},
to: ['anna@example.com'],
subject: 'Your account is ready',
text: 'Hi Anna, your account at Example is ready. Sign in at https://example.se/login.',
tag: 'welcome',
});
console.log(messageId);
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
accepted = sendora.email.send(
from_={
"email": "no-reply@example.se",
"name": "Example AB",
},
to=["anna@example.com"],
subject="Your account is ready",
text=(
"Hi Anna, your account at Example is ready. ""Sign in at https://example.se/login."
),
tag="welcome",
)
print(accepted.message_id)
The from address must be on a verified domain. to, cc and bcc take plain addresses or objects with a display name. A message needs a subject and at least one of text and html.
The answer comes at once. accepted means Sendora has stored the message and will hand it to its mail server. The log, the events and the webhooks all refer to the message by its messageId. Delivery, deferral, bounce and complaint arrive later, as events.
If your code may retry a send, give it an idempotency key. A retry with the same key returns the first answer instead of sending twice. Sending explains keys and batches.
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
message = sendora.messages.get(
"5f432ffd-c005-499b-aa3a-5b8a088ea20e"
)
for recipient in message.recipients:
print(recipient.address, recipient.status)
for event in message.events:
print(event.at, event.type, event.code, event.details)
Response (38 lines)
{"messageId":"5f432ffd-c005-499b-aa3a-5b8a088ea20e","streamId":"3d1a2b4c-5e6f-4a7b-8c9d-0e1f2a3b4c5d","broadcastId":null,"status":"injected","from":"no-reply@example.se","subject":"Your account is ready","tag":"welcome","metadata":{},"submittedAt":"2026-09-15T12:00:00.000Z","test":false,"recipients":[{"address":"anna@example.com","kind":"to","status":"delivered"}],"attachments":[],"events":[{"recipient":"anna@example.com","type":"Reception","at":"2026-09-15T12:00:00.000Z","code":250,"details":null,"classification":null},{"recipient":"anna@example.com","type":"Delivery","at":"2026-09-15T12:00:02.000Z","code":250,"details":"2.0.0 OK","classification":null}]}
The message’s status turns injected once Sendora’s mail server has it. Each recipient’s status moves from queued to delivered, deferred or bounced as the receiving server answers. The events list is the timeline: Reception when Sendora’s mail server took the message, Delivery when the receiver accepted it, and Bounce when it refused it.
The server’s Messages tab in the dashboard shows the same log, searchable by recipient, tag, status and time.
Register a webhook to get delivered, bounced, deferred and spam_complaint events as they happen.
Send from a system that only speaks SMTP, as SMTP submission shows.
Look up any route, with its fields and error codes, in the API reference.
# Quickstart
Send your first message and read it in the log.
Three steps take you from an approved account to a delivered message.
## Before you start
- Your account is approved, and payment is active. Activate payment under [Account › Billing](https://app.sendora.se/account/billing) once you are approved. Until then a live server's sends fail, with the codes on [Errors](https://sendora.se/docs/errors#when-an-account-cannot-send).
- The domain you send from is verified, as [Domains](https://sendora.se/docs/domains) explains. The [Domains](https://app.sendora.se/domains) page shows its two DNS records: a CNAME for the return path and a TXT record for the DKIM key.
- To try this without sending real mail, use a [test server](https://sendora.se/docs/test-servers) and its key. A test server works before your account is approved and before payment.
## 1. Get a server key
Sign in to the dashboard and open your server under [Servers](https://app.sendora.se/servers). Signup made a server named default, without a key. Create a key on its **API keys** tab. The key is shown once, so put it in your secret store. [Keys](https://sendora.se/docs/keys) explains the two kinds of key.
Over HTTP:
Send the key as a bearer token on every request:
```bash
Authorization: Bearer sk_…
```
With the TypeScript SDK:
Install the SDK. It has no dependencies, is ESM, and runs on Node 20.19 or newer, Bun, Deno and Cloudflare Workers:
```sh
npm install @sendora/sdk
```
Keep the key in the environment on the server, as `SENDORA_API_TOKEN` below. Never ship it to a browser. The client throws when you create it without a key.
With the Python SDK:
Install the SDK. It runs on Python 3.11 or newer and has one dependency, httpx2:
```sh
pip install sendora
```
Pin the major version you test with, such as `sendora~=1.0` in your requirements. Only a new major version may break your code.
Keep the key in the environment on the server, as `SENDORA_API_TOKEN` below. `Sendora()` reads it from there when you give it no key, and raises `ValueError` when there is none.
## 2. Send a message
cURL:
```bash
curl -X POST https://api.sendora.se/v1/email \
-H "Authorization: Bearer $SENDORA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"from": {
"email": "no-reply@example.se",
"name": "Example AB"
},
"to": [
"anna@example.com"
],
"subject": "Your account is ready",
"text": "Hi Anna, your account at Example is ready. Sign in at https://example.se/login.",
"tag": "welcome"
}'
```
TypeScript:
```ts
import { Sendora } from '@sendora/sdk';
const sendora = new Sendora({
token: process.env.SENDORA_API_TOKEN,
});
const { messageId } = await sendora.email.send({
from: {
email: 'no-reply@example.se',
name: 'Example AB',
},
to: ['anna@example.com'],
subject: 'Your account is ready',
text: 'Hi Anna, your account at Example is ready. Sign in at https://example.se/login.',
tag: 'welcome',
});
console.log(messageId);
```
Python:
```python
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
accepted = sendora.email.send(
from_={
"email": "no-reply@example.se",
"name": "Example AB",
},
to=["anna@example.com"],
subject="Your account is ready",
text=(
"Hi Anna, your account at Example is ready. "
"Sign in at https://example.se/login."
),
tag="welcome",
)
print(accepted.message_id)
```
Response:
```json
{
"messageId": "5f432ffd-c005-499b-aa3a-5b8a088ea20e",
"status": "accepted",
"submittedAt": "2026-09-15T12:00:00.000Z",
"test": false
}
```
The `from` address must be on a verified domain. `to`, `cc` and `bcc` take plain addresses or objects with a display name. A message needs a `subject` and at least one of `text` and `html`.
The answer comes at once. `accepted` means Sendora has stored the message and will hand it to its mail server. The log, the events and the webhooks all refer to the message by its `messageId`. Delivery, deferral, bounce and complaint arrive later, as events.
If your code may retry a send, give it an idempotency key. A retry with the same key returns the first answer instead of sending twice. [Sending](https://sendora.se/docs/sending#idempotency) explains keys and batches.
## 3. Read the message
The message log keeps every message with its recipients and its timeline, for the time on [Limits](https://sendora.se/docs/limits#retention). Read one by its id:
cURL:
```bash
curl https://api.sendora.se/v1/messages/{messageId} \
-H "Authorization: Bearer $SENDORA_API_TOKEN"
```
TypeScript:
```ts
import { Sendora } from '@sendora/sdk';
const sendora = new Sendora({
token: process.env.SENDORA_API_TOKEN,
});
const message = await sendora.messages.get(
'5f432ffd-c005-499b-aa3a-5b8a088ea20e',
);
for (const recipient of message.recipients) {
console.log(recipient.address, recipient.status);
}
for (const event of message.events) {
console.log(
event.at,
event.type,
event.code,
event.details,
);
}
```
Python:
```python
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
message = sendora.messages.get(
"5f432ffd-c005-499b-aa3a-5b8a088ea20e"
)
for recipient in message.recipients:
print(recipient.address, recipient.status)
for event in message.events:
print(event.at, event.type, event.code, event.details)
```
Response:
```json
{
"messageId": "5f432ffd-c005-499b-aa3a-5b8a088ea20e",
"streamId": "3d1a2b4c-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"broadcastId": null,
"status": "injected",
"from": "no-reply@example.se",
"subject": "Your account is ready",
"tag": "welcome",
"metadata": {},
"submittedAt": "2026-09-15T12:00:00.000Z",
"test": false,
"recipients": [
{
"address": "anna@example.com",
"kind": "to",
"status": "delivered"
}
],
"attachments": [],
"events": [
{
"recipient": "anna@example.com",
"type": "Reception",
"at": "2026-09-15T12:00:00.000Z",
"code": 250,
"details": null,
"classification": null
},
{
"recipient": "anna@example.com",
"type": "Delivery",
"at": "2026-09-15T12:00:02.000Z",
"code": 250,
"details": "2.0.0 OK",
"classification": null
}
]
}
```
The message's `status` turns `injected` once Sendora's mail server has it. Each recipient's `status` moves from `queued` to `delivered`, `deferred` or `bounced` as the receiving server answers. The `events` list is the timeline: `Reception` when Sendora's mail server took the message, `Delivery` when the receiver accepted it, and `Bounce` when it refused it.
The server's **Messages** tab in the dashboard shows the same log, searchable by recipient, tag, status and time.
## What next
- Register a [webhook](https://sendora.se/docs/webhooks) to get `delivered`, `bounced`, `deferred` and `spam_complaint` events as they happen.
- Send from a system that only speaks SMTP, as [SMTP submission](https://sendora.se/docs/smtp) shows.
- Look up any route, with its fields and error codes, in the [API reference](https://sendora.se/docs/api).