# 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).
