Hoppa till innehållet

Sending

Sending

Send messages one at a time or in a batch. Both take the same message and return the same result for each message. The reference lists every field, and Limits gives every size and count.

The message

  • from must be an address on a verified sending domain of your account.
  • to, cc and bcc take plain addresses or objects with email and a display name. The recipients of all three count together.
  • subject is required, and so is at least one of text and html. Send both when you can, since each receiver picks what it shows.
  • streamId sets the stream the message goes on. Without it, the message goes on the server’s default transactional stream.

Attachments, headers, tags and metadata

POST /v1/email
curl -X POST https://api.sendora.se/v1/email \
  -H "Authorization: Bearer $SENDORA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "from": {
    "email": "billing@example.se",
    "name": "Example AB"
  },
  "to": [
    {
      "email": "anna@example.com",
      "name": "Anna Andersson"
    }
  ],
  "cc": [
    "ekonomi@example.se"
  ],
  "subject": "Invoice 2026-0912",
  "text": "Hi Anna, invoice 2026-0912 is attached. It is due on 2026-10-15.",
  "html": "<p>Hi Anna,</p><p>Invoice <strong>2026-0912</strong> is attached. It is due on 2026-10-15.</p>",
  "attachments": [
    {
      "name": "invoice-2026-0912.pdf",
      "content": "JVBERi0xLjcKJcOkw7zDtsOfCg==",
      "contentType": "application/pdf"
    }
  ],
  "headers": {
    "Reply-To": "ekonomi@example.se",
    "X-Invoice-Id": "2026-0912"
  },
  "tag": "invoice",
  "metadata": {
    "invoiceId": "2026-0912",
    "customerId": "4711"
  }
}'
import { readFile } from 'node:fs/promises';
import { Sendora } from '@sendora/sdk';

const sendora = new Sendora({
  token: process.env.SENDORA_API_TOKEN,
});

const invoice = await readFile(
  new URL('./invoice-2026-0912.pdf', import.meta.url),
);

const { messageId } = await sendora.email.send({
  from: {
    email: 'billing@example.se',
    name: 'Example AB',
  },
  to: [
    { email: 'anna@example.com', name: 'Anna Andersson' },
  ],
  cc: ['ekonomi@example.se'],
  subject: 'Invoice 2026-0912',
  text: 'Hi Anna, invoice 2026-0912 is attached. It is due on 2026-10-15.',
  html: '<p>Hi Anna,</p><p>Invoice <strong>2026-0912</strong> is attached. It is due on 2026-10-15.</p>',
  attachments: [
    {
      name: 'invoice-2026-0912.pdf',
      content: invoice,
      contentType: 'application/pdf',
    },
  ],
  headers: {
    'Reply-To': 'ekonomi@example.se',
    'X-Invoice-Id': '2026-0912',
  },
  tag: 'invoice',
  metadata: {
    invoiceId: '2026-0912',
    customerId: '4711',
  },
});

console.log(messageId);
import os
from pathlib import Path

from sendora import Sendora

sendora = Sendora(os.environ["SENDORA_API_TOKEN"])

invoice = (
    Path(__file__)
    .with_name("invoice-2026-0912.pdf")
    .read_bytes()
)

accepted = sendora.email.send(
    from_={
        "email": "billing@example.se",
        "name": "Example AB",
    },
    to=[
        {
            "email": "anna@example.com",
            "name": "Anna Andersson",
        }
    ],
    cc=["ekonomi@example.se"],
    subject="Invoice 2026-0912",
    text=(
        "Hi Anna, invoice 2026-0912 is attached. "
        "It is due on 2026-10-15."
    ),
    html=(
        "<p>Hi Anna,</p>"
        "<p>Invoice <strong>2026-0912</strong> "
        "is attached. It is due on 2026-10-15.</p>"
    ),
    attachments=[
        {
            "name": "invoice-2026-0912.pdf",
            "content": invoice,
            "content_type": "application/pdf",
        }
    ],
    headers={
        "Reply-To": "ekonomi@example.se",
        "X-Invoice-Id": "2026-0912",
    },
    tag="invoice",
    metadata={
        "invoiceId": "2026-0912",
        "customerId": "4711",
    },
)

print(accepted.message_id)

Response

{
  "messageId": "4e1c3f2a-9b6d-4e5c-9a0f-3d4e5f6a7b82",
  "status": "accepted",
  "submittedAt": "2026-09-15T12:00:00.000Z",
  "test": false
}
  • attachments: send the content base64-encoded, without line breaks. contentType defaults to application/octet-stream. Set contentId to use the attachment as an inline image, with <img src="cid:…"> in the HTML. Sendora deletes the content once its mail server has the message, and keeps the name, type and size.
  • headers: your own headers, such as Reply-To, In-Reply-To or X- headers. Sendora writes the address, date, ID and trace headers itself, such as From, To, Subject, Date, Message-ID, Return-Path and DKIM-Signature. Setting one of them fails with invalid_request, and so does any header that starts with Content-, Resent- or ARC-. The headers field in the reference lists every one.
  • tag: one label per message, such as invoice or welcome. It comes back with the message, in the log’s search and in every webhook about the message.
  • metadata: your own key-value pairs. Keys are letters, digits, _, . and -. They come back with the message and in every webhook, so an order id here saves you a lookup when an event arrives.

The SDK takes attachment content as bytes, a Uint8Array or a Node Buffer, and encodes it. It passes a base64 string through as it is.

The SDK takes attachment content as bytes and encodes it. It passes a base64 string through as it is. The content id is content_id.

Idempotency

A network can fail after Sendora has accepted a message but before your code gets the answer. To retry safely, send an idempotency key: any printable text that identifies the request on your side. A retry with the same key returns the first answer instead of sending again. Without a key of your own, a retry of a request that went through sends the message twice. Limits gives the key’s length and how long Sendora remembers it.

POST /v1/email
curl -X POST https://api.sendora.se/v1/email \
  -H "Authorization: Bearer $SENDORA_API_TOKEN" \
  -H "Idempotency-Key: order-4711-confirmation" \
  -H "Content-Type: application/json" \
  -d '{
  "from": {
    "email": "order@example.se",
    "name": "Example AB"
  },
  "to": [
    "anna@example.com"
  ],
  "subject": "Order 4711 confirmed",
  "text": "Hi Anna, we have received order 4711 and will ship it tomorrow."
}'
import { Sendora } from '@sendora/sdk';

const sendora = new Sendora({
  token: process.env.SENDORA_API_TOKEN,
});

const { messageId } = await sendora.email.send(
  {
    from: {
      email: 'order@example.se',
      name: 'Example AB',
    },
    to: ['anna@example.com'],
    subject: 'Order 4711 confirmed',
    text: 'Hi Anna, we have received order 4711 and will ship it tomorrow.',
  },
  { idempotencyKey: 'order-4711-confirmation' },
);

console.log(messageId);
import os

from sendora import Sendora

sendora = Sendora(os.environ["SENDORA_API_TOKEN"])

accepted = sendora.email.send(
    from_={
        "email": "order@example.se",
        "name": "Example AB",
    },
    to=["anna@example.com"],
    subject="Order 4711 confirmed",
    text=(
        "Hi Anna, we have received order 4711 "
        "and will ship it tomorrow."
    ),
    idempotency_key="order-4711-confirmation",
)

print(accepted.message_id)

Response

{
  "messageId": "5f2d4a3b-0c7e-4f6d-8b1a-4e5f6a7b8c93",
  "status": "accepted",
  "submittedAt": "2026-09-15T12:00:00.000Z",
  "test": false
}

The SDK sends a key with every request: yours if you set idempotencyKey, or else a random one. So its own retries after a lost connection never send twice. Set your own key when a retry may come from another process, such as a job runner.

The SDK sends a key with every request: yours if you set idempotency_key, or else a random one. So its own retries after a lost connection never send twice. Set your own key when a retry may come from another process, such as a job runner.

Send the key in the Idempotency-Key header. A replayed answer has the header Idempotent-Replayed: true.

Keys are per server. The same key with a different message fails with idempotency_key_mismatch, since a changed request is a new request.

Batches

A batch sends a list of messages in one request, up to the number on Limits. It needs an idempotency key. Over HTTP, a batch without one fails with idempotency_key_required. The SDKs send a random key when you give none.

POST /v1/email/batch
curl -X POST https://api.sendora.se/v1/email/batch \
  -H "Authorization: Bearer $SENDORA_API_TOKEN" \
  -H "Idempotency-Key: invoices-2026-09-15-run-1" \
  -H "Content-Type: application/json" \
  -d '[
  {
    "from": {
      "email": "billing@example.se",
      "name": "Example AB"
    },
    "to": [
      "anna@example.com"
    ],
    "subject": "Invoice 2026-0912",
    "text": "Hi Anna, invoice 2026-0912 is due on 2026-10-15.",
    "tag": "invoice",
    "metadata": {
      "invoiceId": "2026-0912"
    }
  },
  {
    "from": {
      "email": "billing@example.se",
      "name": "Example AB"
    },
    "to": [
      "bo@example.com"
    ],
    "subject": "Invoice 2026-0913",
    "text": "Hi Bo, invoice 2026-0913 is due on 2026-10-15.",
    "tag": "invoice",
    "metadata": {
      "invoiceId": "2026-0913"
    }
  }
]'
import { Sendora } from '@sendora/sdk';

const sendora = new Sendora({
  token: process.env.SENDORA_API_TOKEN,
});

const { results } = await sendora.email.sendBatch(
  [
    {
      from: {
        email: 'billing@example.se',
        name: 'Example AB',
      },
      to: ['anna@example.com'],
      subject: 'Invoice 2026-0912',
      text: 'Hi Anna, invoice 2026-0912 is due on 2026-10-15.',
      tag: 'invoice',
      metadata: { invoiceId: '2026-0912' },
    },
    {
      from: {
        email: 'billing@example.se',
        name: 'Example AB',
      },
      to: ['bo@example.com'],
      subject: 'Invoice 2026-0913',
      text: 'Hi Bo, invoice 2026-0913 is due on 2026-10-15.',
      tag: 'invoice',
      metadata: { invoiceId: '2026-0913' },
    },
  ],
  { idempotencyKey: 'invoices-2026-09-15-run-1' },
);

for (const result of results) {
  if (result.status === 'accepted') {
    console.log(
      result.index,
      result.messageId,
      result.replayed ? 'sent earlier' : 'sent',
    );
  } else {
    console.log(
      result.index,
      result.error,
      result.message,
    );
  }
}
import os

from sendora import Sendora

sendora = Sendora(os.environ["SENDORA_API_TOKEN"])

batch = sendora.email.send_batch(
    [
        {
            "from_": {
                "email": "billing@example.se",
                "name": "Example AB",
            },
            "to": ["anna@example.com"],
            "subject": "Invoice 2026-0912",
            "text": (
                "Hi Anna, invoice 2026-0912 "
                "is due on 2026-10-15."
            ),
            "tag": "invoice",
            "metadata": {"invoiceId": "2026-0912"},
        },
        {
            "from_": {
                "email": "billing@example.se",
                "name": "Example AB",
            },
            "to": ["bo@example.com"],
            "subject": "Invoice 2026-0913",
            "text": (
                "Hi Bo, invoice 2026-0913 "
                "is due on 2026-10-15."
            ),
            "tag": "invoice",
            "metadata": {"invoiceId": "2026-0913"},
        },
    ],
    idempotency_key="invoices-2026-09-15-run-1",
)

for result in batch.results:
    if result.status == "accepted":
        print(
            result.index,
            result.message_id,
            "sent earlier" if result.replayed else "sent",
        )
    else:
        print(result.index, result.code, result.message)
Response (20 lines)
{
  "results": [
    {
      "index": 0,
      "messageId": "2c9a1d0e-7f4b-4c3a-9e8d-1b2c3d4e5f60",
      "status": "accepted",
      "submittedAt": "2026-09-15T12:00:00.000Z",
      "test": false,
      "replayed": false
    },
    {
      "index": 1,
      "messageId": "3d0b2e1f-8a5c-4d4b-8f9e-2c3d4e5f6a71",
      "status": "accepted",
      "submittedAt": "2026-09-15T12:00:00.000Z",
      "test": false,
      "replayed": false
    }
  ]
}

Sendora accepts or refuses each message on its own, so one invalid message never fails the batch. The batch returns 200 with one result per message, in the order you sent them. Each has the status accepted, with the message’s id, or error, with the code in error and the message that a single send would have returned.

  • A retry with the same key sends nothing twice. Messages accepted before come back with replayed: true, and only the messages that failed are tried again. You may correct a failed message in the retry, since only accepted messages are remembered. Changing an accepted one fails that message with idempotency_key_mismatch.
  • A refusal that concerns the whole account fails the batch before any message is checked. That covers a paused account, one without active payment, and sending stopped by Sendora.
  • The per-minute limit and the monthly cap count message by message, in order. The messages up to the limit are accepted, and the rest get rate_limited or monthly_cap_reached in their results.

What happens next

Sendora stores the message encrypted and hands it to its mail server within seconds. Its recipients[].status then changes as receivers answer, and its timeline fills with events, as Messages and events shows. To be told instead of polling, register a webhook.