Broadcasts
More ways to use this page
Send a broadcast
POST/v1/
TypeScript sendora
Python sendora
Key sk_… a server key
Stores the content once and one message per entry of messages, up to 50,000, on a broadcast stream, in one transaction. Each message is a message in the log with its own events and webhooks, sent with the recipient’s own unsubscribe link where {{ unsubscribe_url }} stands. Each message may carry substitutions, up to 20 strings that replace {{ key }} in the subject, the text and the HTML, escaped in the HTML; a key the content names must be given by every message. Addresses on the stream’s suppression list are dropped and counted; a list with nothing left, a missing placeholder, a missing substitution, a transactional or archived stream and a count over the monthly cap are refused whole. The Idempotency-Key is required: a retry under the same key answers the same broadcast.
Parameters
idempotency-keyin headerstringrequiredAny text of 1 to 255 printable characters that identifies this broadcast; the same key with a different body is refused.
Request body
streamIdstring (uuid)requiredThe broadcast stream of this server the messages go on.
fromone of 2requiredThe sender. Its domain must be a verified sending domain of the server.
- string (email)
emailstring (email)requirednamestring (at least 1, at most 200 characters) or nullA display name of up to 200 characters.
subjectstring (at least 1, at most 998 characters)requiredThe subject line.
textstring (at least 1 character)The plain-text part, with {{ unsubscribe_url }} where the link goes. At least one of text and html is required.
htmlstring (at least 1 character)The HTML part, with {{ unsubscribe_url }} where the link goes.
headersmap of string to stringdefault {}Custom headers on every message, at most 20. The List-Unsubscribe pair is Sendora’s too. Sendora writes these itself, so a message may not set them: From, Sender, To, Cc, Bcc, Subject, Date, Message-ID, MIME-Version, Return-Path, Received, Delivered-To, DKIM-Signature, DomainKey-Signature, Authentication-Results, Received-SPF, and any name that starts with Content-, Resent-, ARC- or X-Kumo.
attachmentsarray of object, at most 20At most 20, sent with every message. A message with its attachments encoded may be at most 10 MB.
namestringrequiredcontentstring (base64)requiredcontentTypestringcontentIdstring or null
tagstring or nullA label of up to 100 characters, returned with the message and its events.
metadatamap of string to stringdefault {}Up to 20 key-value pairs of your own, on the broadcast and on every message.
messagesarray of object, at least 1, at most 50000requiredOne entry per message, at most 50000; a larger list is several broadcasts under one tag.
toarray of one of 2, at least 1requiredRecipients of this message; at most 50 across to, cc and bcc.
- string (email)
emailstring (email)requirednamestring (at least 1, at most 200 characters) or nullA display name of up to 200 characters.
ccarray of one of 2Copied recipients, shown in the message.
- string (email)
emailstring (email)requirednamestring (at least 1, at most 200 characters) or nullA display name of up to 200 characters.
bccarray of one of 2Blind-copied recipients.
- string (email)
emailstring (email)requirednamestring (at least 1, at most 200 characters) or nullA display name of up to 200 characters.
metadatamap of string to stringdefault {}Up to 20 key-value pairs for this message, merged over the broadcast's.
headersmap of string to stringdefault {}Custom headers for this message, merged over the broadcast's.
substitutionsmap of string to stringdefault {}Up to 20 strings that replace {{ key }} in the subject, the text and the HTML of this message; a key the content names must be given.
{
"streamId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"from": {
"email": "news@acme.se",
"name": "Acme"
},
"subject": "News from Acme in October",
"text": "Hi!\n\nHere is the news for October.\n\nRather not get these? {{ unsubscribe_url }}",
"html": "<p>Hi!</p><p>Here is the news for 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"
}
]
}
]
}Responses
200The broadcast was stored; Sendora sends its messages from here.
broadcastIdstring (uuid)requiredThe id the messages, the progress and the cancel refer to.
status"accepted"requiredtotalintegerrequiredMessages stored; each is a message in the log.
suppressedintegerrequiredAddresses dropped for standing on the stream’s suppression list.
submittedAtstring (date-time)requiredtestbooleanrequiredTrue when the server is a test server: the messages go through everything but delivery, and nobody receives them.
Idempotent-Replayedheader"true"true when this is the first answer again, to a request sent before under the same Idempotency-Key; absent otherwise.
Example
{
"broadcastId": "2f1c8a4e-3b6d-4f0a-9c21-7d5e6a8b9c01",
"status": "accepted",
"total": 2,
"suppressed": 0,
"submittedAt": "2026-10-01T08:00:00.000Z",
"test": false
}Errors
Every error answers error, the code, and message, a sentence for a person. A code that adds fields is shown in full below the table.
| Code | Status | Meaning |
|---|---|---|
invalid_ | 400 | The body, the query or a header does not match what the route takes. |
idempotency_ | 400 | Batch sends need an Idempotency-Key header. |
unauthorized | 401 | The key is missing, malformed or revoked. |
payment_ | 402 | The account has no active subscription. |
wrong_ | 403 | The key is of the other kind: a server key (sk_) where an account key (ak_) is needed, or the reverse. The message names the kind the operation takes. |
account_ | 403 | The account is paused by Sendora. |
account_ | 403 | The account is closed, or not yet approved for a live server's sends or an inbound stream. |
request_ | 413 | The body exceeds 10 MB. |
from_ | 422 | The From domain is not a verified sending domain of this server. |
stream_ | 422 | The streamId names no stream of this server. |
stream_ | 422 | The stream is archived and takes no new messages. |
stream_ | 422 | Sendora has paused the stream after complaints; it takes no messages until support has resumed it. |
stream_ | 422 | A broadcast goes on a broadcast stream; the streamId names a transactional one. |
unsubscribe_ | 422 | A message on a broadcast stream must carry {{ unsubscribe_url }} in every part it has. |
list_ | 422 | On a broadcast stream Sendora writes the List-Unsubscribe pair itself, so a message may not carry one. |
substitution_ | 422 | The subject, text or HTML names a {{ key }} that a message does not give. |
recipient_ | 422 | One or more recipients are on the suppression list of the stream. |
test_ | 422 | Addresses at simulator.sendora.se act out an outcome on a test server; a live server never sends to them. |
idempotency_ | 422 | The Idempotency-Key was already used for a different request. |
monthly_ | 429 | The monthly cap of the account, the server or the test servers is used up. |
sending_ | 503 | Sending is disabled for everyone for the moment; retry after the seconds in Retry-After. |
Retry-Afterheader on 503integerSeconds to wait before trying again.
invalid_request
issuesarray of objectOne entry per invalid field; absent when a header is wrong.
pathstringrequiredThe field, dotted, such as to.0.email; empty when the whole body is wrong.
messagestringrequired
Example
{
"error": "invalid_request",
"message": "The request is invalid: to.0: Invalid email address",
"issues": [
{
"path": "to.0",
"message": "Invalid email address"
}
]
}substitution_missing
indexintegerrequiredThe position of the message in
messages.keysarray of stringrequiredThe keys the content names that the message lacks.
Example
{
"error": "substitution_missing",
"message": "The content names {{ firstName }}, which message 1 does not give.",
"index": 1,
"keys": [
"firstName"
]
}recipient_suppressed
streamIdstring (uuid)requiredThe stream whose suppression list refused the send.
suppressedarray of objectrequiredEach refused address, lower-cased, with the reason it is on the list.
addressstringrequiredreason"hard_bounce" | "spam_ requiredcomplaint" | "manual" | "unsubscribe"
Example
{
"error": "recipient_suppressed",
"message": "One or more recipients are on the suppression list of this stream; suppressed says which and why.",
"streamId": "3d1a2b4c-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"suppressed": [
{
"address": "anna@example.com",
"reason": "hard_bounce"
}
]
}test_address_on_live_server
addressesarray of stringrequiredThe recipients at simulator.sendora.se, each once.
Example
{
"error": "test_address_on_live_server",
"message": "Addresses at simulator.sendora.se act out an outcome on a test server and are never sent to from a live one. Send them from a test server.",
"addresses": [
"hardbounce@simulator.sendora.se"
]
}monthly_cap_reached
scope"account" | "server" | "test"requiredWhose limit it is:
accountfor the account's,serverfor the server's own,testfor the cap the account's test servers share.capintegerrequiredEmails the scope may send in a calendar month, in UTC.
usedintegerrequiredEmails sent this month, the refused send not counted.
resetsAtstring (date-time)requiredWhen the month counter resets.
Example
{
"error": "monthly_cap_reached",
"message": "This account has used 50000 of 50000 emails this month. The cap resets at 2026-11-01T00:00:00.000Z.",
"scope": "account",
"cap": 50000,
"used": 50000,
"resetsAt": "2026-11-01T00:00:00.000Z"
}