Sending
More ways to use this page
Broadcasts
A broadcast sends the same content to many people who asked for it, such as a newsletter or a notice to every customer. You send the content once, with a list of messages. Sendora gives every recipient a copy with their own unsubscribe link, and tells you how far it has come.
A broadcast goes on a broadcast stream. Create one with kind: "broadcast" before your first broadcast, as Streams shows.
Send a broadcast
curl -X POST https://api.sendora.se/v1/broadcasts \
-H "Authorization: Bearer $SENDORA_API_TOKEN" \
-H "Idempotency-Key: newsletter-2026-10" \
-H "Content-Type: application/json" \
-d '{
"streamId": "a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"from": {
"email": "news@example.se",
"name": "Example AB"
},
"subject": "News from Example in October",
"text": "Hi!\n\nHere is what happened in October.\n\nNo more of these? {{ unsubscribe_url }}",
"html": "<p>Hi!</p><p>Here is what happened in October.</p><p><a href=\"{{ unsubscribe_url }}\">Unsubscribe</a></p>",
"tag": "newsletter-2026-10",
"messages": [
{
"to": [
"anna@example.com"
],
"metadata": {
"customer": "1042"
}
},
{
"to": [
{
"email": "bo@example.com",
"name": "Bo Berg"
}
]
}
]
}'import { Sendora } from '@sendora/sdk';
const sendora = new Sendora({
token: process.env.SENDORA_API_TOKEN,
});
const { broadcastId, total } =
await sendora.broadcasts.send(
{
streamId: 'a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
from: {
email: 'news@example.se',
name: 'Example AB',
},
subject: 'News from Example in October',
text: 'Hi!\n\nHere is what happened in October.\n\nNo more of these? {{ unsubscribe_url }}',
html: '<p>Hi!</p><p>Here is what happened in October.</p><p><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>',
tag: 'newsletter-2026-10',
messages: [
{
to: ['anna@example.com'],
metadata: { customer: '1042' },
},
{
to: [
{ email: 'bo@example.com', name: 'Bo Berg' },
],
},
],
},
{ idempotencyKey: 'newsletter-2026-10' },
);
console.log(broadcastId, total);
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
accepted = sendora.broadcasts.send(
stream_id="a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
from_={
"email": "news@example.se",
"name": "Example AB",
},
subject="News from Example in October",
text=(
"Hi!\n\n"
"Here is what happened in October.\n\n"
"No more of these? {{ unsubscribe_url }}"
),
html=(
"<p>Hi!</p>"
"<p>Here is what happened in October.</p>"
'<p><a href="{{ unsubscribe_url }}">'
"Unsubscribe</a></p>"
),
tag="newsletter-2026-10",
messages=[
{
"to": ["anna@example.com"],
"metadata": {"customer": "1042"},
},
{
"to": [
{
"email": "bo@example.com",
"name": "Bo Berg",
}
]
},
],
idempotency_key="newsletter-2026-10",
)
print(accepted.broadcast_id, accepted.total)
Response
{
"broadcastId": "2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01",
"status": "accepted",
"total": 2,
"suppressed": 0,
"submittedAt": "2026-10-01T08:00:00.000Z",
"test": false
}Give the content once: from, subject, text, html, headers, attachments, tag and metadata. Each entry of messages is one message with its own to, cc and bcc. An entry may also set metadata and headers, which are laid over the broadcast’s.
Limits gives the most messages and the largest size a broadcast may have. Each message in it has the size and recipient limits of a single send. Send a longer list as several broadcasts under one tag.
The Idempotency-Key header is required, and a request without it fails with idempotency_. A retry with the same key returns the same broadcast and never sends it twice. Choose the key yourself, such as the newsletter’s name, so that a retry from another process uses it too.
Pass the key as idempotencyKey, in the second argument to sendora. Without it, the SDK makes up a random key, which covers only its own retries. A refused broadcast throws SendoraError with its code.
Pass the key as idempotency_ to sendora. Without it, the SDK makes up a random key, which covers only its own retries. A refused broadcast raises SendoraError with its code.
Rules of a broadcast stream
These rules apply to every message on a broadcast stream, whether it goes in a broadcast or alone.
- Send only to people who asked for the mail, as the acceptable use rules say.
- Write
{{ unsubscribe_in the text and in the HTML, where the link should go. A message without it in every part fails withurl }} unsubscribe_.placeholder_ missing - Sendora sends each recipient a copy addressed to them alone, with their own link. A message with several addresses gives each address its own copy, so the recipients never see each other. Sendora also sets the
List-Unsubscribeheaders that mail clients show as a button. AList-Unsubscribeheader of your own fails withlist_.unsubscribe_ reserved - A recipient who unsubscribes goes on the stream’s suppression list, and your webhook gets the event
unsubscribed.
If complaints pass the rate on Limits, Sendora pauses the stream. Every send on it then fails with stream_, and the stream shows pausedAt. Support goes through your mailing list with you, then resumes the stream.
What is dropped and what is refused
- An address on the stream’s suppression list is dropped and counted in
suppressed, and the rest are still sent. A message with no address left is not stored. If no address is left at all, the broadcast fails withrecipient_.suppressed - Every address left counts against the monthly cap when the broadcast is accepted. If it would pass the cap, it fails with
monthly_and nothing is stored. The per-minute limits do not apply to a broadcast.cap_ reached - A broadcast on a transactional or an inbound stream fails with
stream_, and on an archived one withnot_ broadcast stream_.archived
Personalise
curl -X POST https://api.sendora.se/v1/broadcasts \
-H "Authorization: Bearer $SENDORA_API_TOKEN" \
-H "Idempotency-Key: welcome-2026-10" \
-H "Content-Type: application/json" \
-d '{
"streamId": "a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"from": {
"email": "news@example.se",
"name": "Example AB"
},
"subject": "{{ first_name }}, your October news",
"text": "Hi {{ first_name }}!\n\nYou have {{ points }} points to spend before the end of the year.\n\nNo more of these? {{ unsubscribe_url }}",
"messages": [
{
"to": [
"anna@example.com"
],
"substitutions": {
"first_name": "Anna",
"points": "120"
}
},
{
"to": [
"bo@example.com"
],
"substitutions": {
"first_name": "Bo",
"points": "35"
}
}
]
}'import { Sendora } from '@sendora/sdk';
const sendora = new Sendora({
token: process.env.SENDORA_API_TOKEN,
});
const { broadcastId } = await sendora.broadcasts.send(
{
streamId: 'a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
from: {
email: 'news@example.se',
name: 'Example AB',
},
subject: '{{ first_name }}, your October news',
text: 'Hi {{ first_name }}!\n\nYou have {{ points }} points to spend before the end of the year.\n\nNo more of these? {{ unsubscribe_url }}',
messages: [
{
to: ['anna@example.com'],
substitutions: {
first_name: 'Anna',
points: '120',
},
},
{
to: ['bo@example.com'],
substitutions: { first_name: 'Bo', points: '35' },
},
],
},
{ idempotencyKey: 'welcome-2026-10' },
);
console.log(broadcastId);
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
accepted = sendora.broadcasts.send(
stream_id="a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
from_={
"email": "news@example.se",
"name": "Example AB",
},
subject="{{ first_name }}, your October news",
text=(
"Hi {{ first_name }}!\n\n"
"You have {{ points }} points to spend "
"before the end of the year.\n\n"
"No more of these? {{ unsubscribe_url }}"
),
messages=[
{
"to": ["anna@example.com"],
"substitutions": {
"first_name": "Anna",
"points": "120",
},
},
{
"to": ["bo@example.com"],
"substitutions": {
"first_name": "Bo",
"points": "35",
},
},
],
idempotency_key="welcome-2026-10",
)
print(accepted.broadcast_id)
Response
{
"broadcastId": "9b7e5d3c-1a2f-4e6b-8c0d-2f4a6b8c0d1e",
"status": "accepted",
"total": 2,
"suppressed": 0,
"submittedAt": "2026-10-01T08:10:00.000Z",
"test": false
}Write {{ key }} in the subject, the text or the HTML, and give each message its values in substitutions. A key is letters, digits and underscores, and every value is a string. Limits gives how many values a message may have, and how long each may be.
Sendora fills in each message before it adds the unsubscribe link, and escapes the values in the HTML. The log shows the filled-in subject. If the content names a key that a message lacks, the whole broadcast fails with substitution_. The error names the message’s position and the missing keys.
Nothing else is rendered: there are no conditions, loops or filters.
Follow a broadcast
curl https://api.sendora.se/v1/broadcasts/{broadcastId} \
-H "Authorization: Bearer $SENDORA_API_TOKEN"import { Sendora } from '@sendora/sdk';
const sendora = new Sendora({
token: process.env.SENDORA_API_TOKEN,
});
const broadcast = await sendora.broadcasts.get(
'2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01',
);
console.log(
broadcast.status,
broadcast.released,
broadcast.total,
);
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
broadcast = sendora.broadcasts.get(
"2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01"
)
print(
broadcast.status, broadcast.released, broadcast.total
)
Response
{
"broadcastId": "2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01",
"streamId": "a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"status": "processing",
"from": "news@example.se",
"subject": "News from Example in October",
"tag": "newsletter-2026-10",
"metadata": {},
"total": 2,
"released": 1,
"failed": 0,
"suppressed": 0,
"submittedAt": "2026-10-01T08:00:00.000Z",
"completedAt": null
}total: the messages stored.released: the messages handed to the mail server so far. Sendora releases them at the pace on Limits. Divide the messages by that pace for the minutes a broadcast takes.failed: the messages that never reached the mail server, cancelled ones included. A bounce is not counted here. It shows on its message.status:accepted, thenprocessing, thencompletedonce nothing is left to send. A cancelled broadcast iscancelled.completedAtis set when it ends.
Each message of a broadcast is a message in the log, with its own timeline and the broadcastId. The search filters by broadcastId, and every webhook event about such a message includes it. Follow deliveries, bounces, complaints and unsubscribes there, as for any other message.
List the broadcasts returns them newest first. In the dashboard, a broadcast stream’s page lists its broadcasts. Each broadcast’s page shows its counts, and a cancel button while it runs.
Cancel a broadcast
curl -X POST https://api.sendora.se/v1/broadcasts/{broadcastId}/cancel \
-H "Authorization: Bearer $SENDORA_API_TOKEN"import { Sendora } from '@sendora/sdk';
const sendora = new Sendora({
token: process.env.SENDORA_API_TOKEN,
});
const cancelled = await sendora.broadcasts.cancel(
'2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01',
);
console.log(cancelled.status, cancelled.failed);
import os
from sendora import Sendora
sendora = Sendora(os.environ["SENDORA_API_TOKEN"])
cancelled = sendora.broadcasts.cancel(
"2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01"
)
print(cancelled.status, cancelled.failed)
Response
{
"broadcastId": "2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01",
"streamId": "a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"status": "cancelled",
"from": "news@example.se",
"subject": "News from Example in October",
"tag": "newsletter-2026-10",
"metadata": {},
"total": 2,
"released": 1,
"failed": 1,
"suppressed": 0,
"submittedAt": "2026-10-01T08:00:00.000Z",
"completedAt": "2026-10-01T08:05:00.000Z"
}A cancel stops every message that is not yet handed to the mail server. Those count as failed, and the broadcast becomes cancelled with completedAt set. A message already on its way is still delivered. Cancelling a completed or cancelled broadcast fails with broadcast_.