Sending
More ways to use this page
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
frommust be an address on a verified sending domain of your account.to,ccandbcctake plain addresses or objects withemailand a displayname. The recipients of all three count together.subjectis required, and so is at least one oftextandhtml. Send both when you can, since each receiver picks what it shows.streamIdsets the stream the message goes on. Without it, the message goes on the server’s default transactional stream.
Attachments, headers, tags and metadata
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.contentTypedefaults toapplication/. Setoctet-stream contentIdto 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 asReply-To,In-Reply-ToorX-headers. Sendora writes the address, date, ID and trace headers itself, such asFrom,To,Subject,Date,Message-ID,Return-PathandDKIM-Signature. Setting one of them fails withinvalid_, and so does any header that starts withrequest Content-,Resent-orARC-. Theheadersfield in the reference lists every one.tag: one label per message, such asinvoiceorwelcome. 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_.
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.
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_, 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_, 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_. The SDKs send a random key when you give none.
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 withidempotency_.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_orlimited monthly_in their results.cap_ reached
What happens next
Sendora stores the message encrypted and hands it to its mail server within seconds. Its recipients[] 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.