Skip to content

Email API

Send transactional email from any address on your verified domain: single sends, batches, idempotent retries and status.

01

Send an email#

POST /v0/emails with an account- or domain-scoped key. From can be any address on a domain you verified in BotInbox, with an optional display name. Provide text, html, or both. The response is 201 with the email's id and status queued; BotInbox delivers it within seconds and retries temporary failures for you.

Shell
curl https://botinbox.dev/v0/emails \
  -X POST -H "Authorization: Bearer $BOTINBOX_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <noreply@acme.com>",
    "to": ["ada@example.com"],
    "reply_to": "support@acme.com",
    "subject": "Your receipt",
    "html": "<p>Thanks for your order.</p>",
    "text": "Thanks for your order.",
    "tags": [{ "name": "category", "value": "receipt" }]
  }'

# 201 {"id":"em_01j9z3k8q4t7w2x5v6b8n0m1c3","status":"queued"}

02

Sending domains#

The From domain must be a verified domain in your organization (Domains in the dashboard, or POST /v0/domains). Any local part works: noreply@, billing@, alerts@. Sending from a domain that isn't verified returns 422 domain_not_verified. If a verified domain's DKIM record goes missing, sends from it return 403 domain_paused until the record is back. Before you verify a domain you can try the API from the sandbox sender onboarding@mail.botinbox.dev, which only delivers to verified members of your organization (403 sandbox_restricted otherwise).

03

Fields and limits#

to, cc, bcc and reply_to take one address or a list; together to, cc and bcc can hold up to 50 recipients (new accounts have a lower limit during their first days). Bcc recipients never appear in the message headers. headers accepts X-* headers plus List-Unsubscribe and List-Unsubscribe-Post; other headers are rejected. attachments are base64-encoded, up to 25 MB each and 40 MB in total after encoding. tags are up to 10 name/value pairs using letters, digits, _ and -.

JSON
{
  "from": "Acme Billing <billing@acme.com>",
  "to": "ada@example.com",
  "bcc": ["audit@acme.com"],
  "subject": "Invoice INV-1042",
  "text": "Your invoice is attached.",
  "headers": { "X-Entity-Ref-ID": "INV-1042" },
  "attachments": [
    { "filename": "INV-1042.pdf", "content": "JVBERi0xLjcK...", "content_type": "application/pdf" }
  ],
  "tags": [{ "name": "category", "value": "invoice" }]
}

04

Batch send#

POST /v0/emails/batch takes a JSON array of up to 100 emails with the same fields, except attachments. The batch is validated as a whole: if any email is invalid or refused, nothing is sent and the error includes the index of the first failing email. On success, data lists the ids in the same order as the request.

Shell
curl https://botinbox.dev/v0/emails/batch \
  -X POST -H "Authorization: Bearer $BOTINBOX_API_KEY" -H "Content-Type: application/json" \
  -d '[
    { "from": "noreply@acme.com", "to": "ada@example.com", "subject": "Welcome", "text": "Hi Ada" },
    { "from": "noreply@acme.com", "to": "alan@example.com", "subject": "Welcome", "text": "Hi Alan" }
  ]'

# 200 {"data":[{"id":"em_..."},{"id":"em_..."}]}

05

Idempotent retries#

Send an Idempotency-Key header (1 to 256 characters, for example your order id) to make retries safe for 24 hours. Repeating a request with the same key and the same body returns the original response with Idempotent-Replayed: true and sends nothing. The same key with a different body returns 409 invalid_idempotent_request, and a retry while the first request is still running returns 409 concurrent_idempotent_requests. Rate-limit and server errors don't consume the key. The header works on POST /v0/emails, POST /v0/emails/batch and inbox sends.

Shell
curl https://botinbox.dev/v0/emails -X POST -H "Authorization: Bearer $BOTINBOX_API_KEY" \
  -H "Idempotency-Key: order-1042-receipt" -H "Content-Type: application/json" \
  -d '{"from":"noreply@acme.com","to":"ada@example.com","subject":"Receipt","text":"Thanks!"}'

06

Check status#

GET /v0/emails/:id returns the email's status with timestamps and open and click counts. Statuses: queued, sending, sent (accepted for delivery), delivery_delayed (the receiving server asked us to retry), delivered, bounced, complained (the recipient marked it as spam), failed (rejected before delivery, with an error_code and a short explanation) and suppressed (every recipient was on your suppression list, so nothing was sent). Delivery reports can arrive late or out of order; the status only moves forward, and with several recipients it shows the most serious outcome. Account keys also receive email.* events on the live event stream (/v0/events).

Shell
curl https://botinbox.dev/v0/emails/em_01j9z3k8q4t7w2x5v6b8n0m1c3 -H "Authorization: Bearer $BOTINBOX_API_KEY"

07

Event timeline#

GET /v0/emails/:id/events lists everything that happened to an email, oldest first: email.sent, email.delivered, email.delivery_delayed, email.bounced (with bounce.type hard or soft), email.complained, email.failed, email.suppressed, email.opened and email.clicked (with the link). Delivery events are per recipient. Opens are approximate: some mail apps load images automatically, and others block them.

Shell
curl https://botinbox.dev/v0/emails/em_01j9z3k8q4t7w2x5v6b8n0m1c3/events -H "Authorization: Bearer $BOTINBOX_API_KEY"

# {"object":"list","email_id":"em_...","data":[
#   {"type":"email.sent","occurred_at":"2026-09-27T10:00:01.000Z"},
#   {"type":"email.delivered","recipient":"ada@example.com","occurred_at":"2026-09-27T10:00:03.000Z"}]}

08

List emails#

GET /v0/emails lists your emails newest first, without bodies. Filter with status (one or a comma-separated list), domain_id, tag (name:value), to (a recipient address), api_key_id, created_after and created_before (ISO 8601). Pages hold up to 100 emails (limit); pass next_cursor as cursor to get the next page. The Emails page in the dashboard shows the same log with daily numbers: sent, delivered, bounce rate, complaint rate, opens and clicks.

Shell
curl "https://botinbox.dev/v0/emails?status=bounced,complained&tag=category:receipt&limit=50" -H "Authorization: Bearer $BOTINBOX_API_KEY"

# {"object":"list","data":[{"id":"em_...","status":"bounced",...}],"has_more":true,"next_cursor":"em_..."}

09

Keys for app servers#

Create a key with sending permission for your app servers: it can send and read the status, events and list of the emails it sent, and nothing else (no inboxes, domains, webhooks, suppressions or keys). Add a domain scope to limit it to one sending domain; a domain-scoped key sending from another domain gets 403 insufficient_scope. Owners and Admins can create these keys in the dashboard or with POST /v0/api-keys.

Shell
curl https://botinbox.dev/v0/api-keys -X POST -H "Authorization: Bearer $BOTINBOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Checkout service","permission":"sending","domain_id":"DOMAIN_ID"}'

10

Limits and errors#

Email API sends share your plan's daily and monthly send allowance with agent inbox sends, and each plan accepts a set number of emails per second. Over a limit the API returns 429 with rate_limited (retry after the Retry-After header), plan_limit or trust_limit. Validation problems return 400 validation_error with an issues list.

11

Suppressed recipients#

Recipients on your suppression list (after a hard bounce, a complaint, repeated soft bounces, or an entry you added) are dropped from the email instead of failing the request. The response lists them in suppressed, and an email.suppressed event is sent. If every recipient is suppressed the email is stored with status suppressed and nothing is sent. Manage the list with /v0/suppressions.

Shell
# 201 {"id":"em_...","status":"queued","suppressed":["gone@example.com"]}