Bytloop MailDocs
Status

Getting started

  • Introduction
  • Quickstart

Sending email

  • Send a transactional email
  • Sending domains vs mailboxes

Framework guides

  • Send email from Next.js
  • Send email from Ruby on Rails
  • Send email from Django
  • Send email from Laravel

Domains

  • Verify a domain

Receiving email

  • Receive email
  • Webhooks

Reference

  • Rate limits
  • Errors
  • SDKs

Sending email

Send a transactional email

POST an email through the REST API from curl, Node, or Python.

Try it live

Every endpoint in this doc runs against your workspace from the interactive API explorer. Prefill a request, hit Send, and see the response with your own keys.

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_xxxxxxxxxxxxxxxx
Authorization: Bearer bm_live_xxxxxxxxxxxxxxxx

Test keys (bm_test_*) route through the sandbox and never touch a real recipient. Use them in CI and for local development.

Request body#

FieldTypeRequiredDescription
fromstringyesRFC 5322 address — "Name <hi@your.com>" or "hi@your.com". Must sit on a verified domain.
tostring[]yesOne to fifty recipients.
ccstring[]no
bccstring[]no
reply_tostring[]noOverrides the Reply-To header.
subjectstringyes
htmlstringat least one of html/textHTML body.
textstringat least one of html/textPlain-text body.
headersobjectnoExtra headers merged into the outbound message. Reserved names (From, To, Subject, DKIM-Signature) are rejected.
tagsobjectnoFree-form key/value labels surfaced on Logs and webhook payloads.
attachmentsobject[]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"
}
{
  "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" }
  }'
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();
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"]
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 html and text. 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.
PreviousQuickstartNextSending domains vs mailboxes

On this page

  • Endpoint
  • Authentication
  • Request body
  • Response
  • curl
  • Node — fetch
  • Python — requests
  • Common pitfalls
  • Next