Sending email
Send a transactional email
POST an email through the REST API from curl, Node, or Python.
The transactional endpoint accepts a single message and returns a
correlation id you can use to look up delivery, opens, and clicks later.
It is idempotent — retrying the same request with the same
Idempotency-Key header returns the original response instead of sending
twice.
Endpoint#
POST /api/v1/emails
Authentication#
Bearer token. Every request includes the API key you minted in Settings → API keys:
Authorization: Bearer bm_live_xxxxxxxxxxxxxxxxTest keys (bm_test_*) route through the sandbox and never touch a real
recipient. Use them in CI and for local development.
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
from | string | yes | RFC 5322 address — "Name <hi@your.com>" or "hi@your.com". Must sit on a verified domain. |
to | string[] | yes | One to fifty recipients. |
cc | string[] | no | |
bcc | string[] | no | |
reply_to | string[] | no | Overrides the Reply-To header. |
subject | string | yes | |
html | string | at least one of html/text | HTML body. |
text | string | at least one of html/text | Plain-text body. |
headers | object | no | Extra headers merged into the outbound message. Reserved names (From, To, Subject, DKIM-Signature) are rejected. |
tags | object | no | Free-form key/value labels surfaced on Logs and webhook payloads. |
attachments | object[] | no | { filename, content: base64, content_type? } — max 15MB combined. |
Response#
202 Accepted
{
"id": "em_2N5kQpZxV9wKqL3jH7yT",
"status": "queued",
"to": ["customer@example.com"],
"created_at": "2026-04-14T20:31:14.892Z"
}4xx responses share the shape documented in Errors.
curl#
curl -X POST https://mail.bytloop.com/api/v1/emails \
-H "Authorization: Bearer bm_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"from": "Support <support@send.acme.com>",
"to": ["customer@example.com"],
"subject": "Your receipt for order #4821",
"html": "<p>Thanks for the order! Attached is your receipt.</p>",
"tags": { "receipt_for_order": "4821" }
}'Node — fetch#
const response = await fetch("https://mail.bytloop.com/api/v1/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BYTLOOP_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `receipt-${orderId}`,
},
body: JSON.stringify({
from: "Support <support@send.acme.com>",
to: [customerEmail],
subject: `Your receipt for order #${orderId}`,
html: renderReceiptHtml(order),
tags: { receipt_for_order: String(orderId) },
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Send failed: ${error.message}`);
}
const { id } = await response.json();Python — requests#
import os
import requests
response = requests.post(
"https://mail.bytloop.com/api/v1/emails",
headers={
"Authorization": f"Bearer {os.environ['BYTLOOP_API_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": f"receipt-{order_id}",
},
json={
"from": "Support <support@send.acme.com>",
"to": [customer_email],
"subject": f"Your receipt for order #{order_id}",
"html": render_receipt_html(order),
"tags": {"receipt_for_order": str(order_id)},
},
timeout=15,
)
response.raise_for_status()
email_id = response.json()["id"]Common pitfalls#
- From domain not verified. The API refuses to send from a domain that is not fully verified in your workspace — including SPF alignment. Check status in Domains.
- Missing both
htmlandtext. Empty-body sends are rejected. Provide at least one; ideally provide both. - Recipient on the suppression list. A hard bounce or complaint moves
the recipient onto the workspace suppression list automatically. The
API refuses to send to those addresses with a
422— see Errors.
Next#
- Register a webhook endpoint to hear about delivery, bounces, and opens.
- Read the rate limits page so you know how the platform behaves under bursts.