Hoppa till innehållet

Sending

Streams

Every message goes on a stream of the server that sends it. A server is one part of your account, with its own keys, streams and message log. A stream keeps one kind of mail apart from the rest. Each sending stream has its own suppression list, so a recipient who bounced or unsubscribed on one stream still gets mail on the others.

Every server starts with a default transactional stream. A message that names no stream goes there, so you can send without creating one.

Three kinds

  • transactional: mail one person expects because of something they did or have with you, such as a receipt, an invoice, a sign-in link or a case notice.
  • broadcast: the same message to many people who asked for it, such as a newsletter or service information to every customer. Every message on it follows the rules of a broadcast stream, the unsubscribe link among them.
  • inbound: mail the server receives, as Inbound explains. It sends nothing, and it has no suppression list.

Any account can create transactional and broadcast streams. An inbound stream needs an approved account.

Add a sending stream for each kind of mail that a recipient might stop on its own. Put a newsletter on a broadcast stream of its own, so that an unsubscribe from it never stops your receipts. Don’t add a stream per customer or per message. A municipality might keep Notices for case notices and Receipts for payment receipts, each with its own list of addresses that bounced.

Create a stream

POST /v1/streams
curl -X POST https://api.sendora.se/v1/streams \
  -H "Authorization: Bearer $SENDORA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "kind": "transactional",
  "name": "Notices"
}'
import { Sendora } from '@sendora/sdk';

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

const stream = await sendora.streams.create({
  kind: 'transactional',
  name: 'Notices',
});

console.log(stream.streamId);
import os

from sendora import Sendora

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

stream = sendora.streams.create(
    kind="transactional", name="Notices"
)

print(stream.stream_id)

Response

{
  "streamId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "kind": "transactional",
  "name": "Notices",
  "isDefault": false,
  "createdAt": "2026-09-16T12:00:00.000Z",
  "archivedAt": null,
  "pausedAt": null,
  "inboundAddress": null,
  "contentRetentionDays": null
}

The name is yours, and it must be unique within the server, archived streams included. A taken name fails with stream_exists, which gives the id of the stream that has it. The streamId never changes. A new sending stream’s suppression list is empty, and you can limit a webhook to the stream.

Send on a stream

Set streamId on the message. Without it, the message goes on the default stream. Over SMTP, set the header X-Sendora-Stream to the id instead.

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": "no-reply@example.se",
    "name": "Example AB"
  },
  "streamId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "to": [
    "anna@example.com"
  ],
  "subject": "Your case has a new message",
  "text": "Hi Anna, there is a new message in your case. Sign in to read it."
}'
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',
  },
  streamId: '7c9e6679-7425-40de-944b-e07fc1f90ae7',
  to: ['anna@example.com'],
  subject: 'Your case has a new message',
  text: 'Hi Anna, there is a new message in your case. Sign in to read it.',
});

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",
    },
    stream_id="7c9e6679-7425-40de-944b-e07fc1f90ae7",
    to=["anna@example.com"],
    subject="Your case has a new message",
    text=(
        "Hi Anna, there is a new message in your case. "
        "Sign in to read it."
    ),
)

print(accepted.message_id)

Response

{
  "messageId": "9a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "status": "accepted",
  "submittedAt": "2026-09-16T12:00:00.000Z",
  "test": false
}

A send fails when the stream cannot take it:

CodeWhen
stream_not_foundThe stream is not one of this server’s.
stream_archivedThe stream is archived.
stream_not_sendableThe stream is an inbound stream. A batch item or an SMTP submission that names it fails the same way.
stream_pausedSendora paused the broadcast stream after too many complaints. It shows pausedAt until support resumes it.
unsubscribe_placeholder_missingThe stream is a broadcast stream, and a part of the message lacks {{ unsubscribe_url }}.
list_unsubscribe_reservedThe stream is a broadcast stream, and the message sets its own List-Unsubscribe header.

The last three follow from the rules of a broadcast stream.

List, rename, archive

GET /v1/streams
curl https://api.sendora.se/v1/streams \
  -H "Authorization: Bearer $SENDORA_API_TOKEN"
import { Sendora } from '@sendora/sdk';

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

const { streams } = await sendora.streams.list();

for (const stream of streams) {
  console.log(
    stream.name,
    stream.kind,
    stream.isDefault ? 'default' : '',
    stream.archivedAt ?? '',
  );
}
import os

from sendora import Sendora

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

listed = sendora.streams.list()

for stream in listed.streams:
    print(
        stream.name,
        stream.kind,
        "default" if stream.is_default else "",
        stream.archived_at or "",
    )
Response (26 lines)
{
  "streams": [
    {
      "streamId": "3d1a2b4c-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "kind": "transactional",
      "name": "transactional",
      "isDefault": true,
      "createdAt": "2026-09-15T12:00:00.000Z",
      "archivedAt": null,
      "pausedAt": null,
      "inboundAddress": null,
      "contentRetentionDays": null
    },
    {
      "streamId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "kind": "transactional",
      "name": "Notices",
      "isDefault": false,
      "createdAt": "2026-09-16T12:00:00.000Z",
      "archivedAt": null,
      "pausedAt": null,
      "inboundAddress": null,
      "contentRetentionDays": null
    }
  ]
}

Every message in the log carries its streamId, and the search filters by it.

  • Update a stream to rename it. Its id stays the same. The same call sets an inbound stream’s contentRetentionDays.
  • Archive a stream to stop new messages on it. What it already holds is still delivered, and its log stays readable. An archive cannot be undone.
  • Archiving the default stream fails with default_stream.

In the dashboard, open a server under Servers. Its Streams tab lists the streams with their last 30 days. Each stream’s page shows its messages, its suppression list and its settings, where you rename or archive it.

sendora.streams.update(streamId, { name }) renames a stream, and sendora.streams.update(streamId, { contentRetentionDays }) sets an inbound stream’s content window. sendora.streams.archive(streamId) archives it. Each returns the stream.

sendora.streams.update(stream_id, name=) renames a stream, and sendora.streams.update(stream_id, content_retention_days=) sets an inbound stream’s content window. sendora.streams.archive(stream_id) archives it. Each returns the stream.