Sending email
Authenticate, send a single email or a batch, and read the response - with a working curl example against the real API.
This page covers the direct REST API. Everything here works the same whether or not your application is an orboto workspace.
Base URL and authentication
https://mail.orboto.io/api/v1/*Every request carries a Bearer token:
Authorization: Bearer oms_live_<your-key>Keys look like oms_live_... for production sending or oms_test_...
for a sandbox key, and are managed at account.orboto.io/mail/api-keys
(create, name, rotate, revoke).
The full secret is shown exactly once, at creation - orboto Mail only
ever stores a salted hash of it, so a lost key can't be recovered, only
rotated. If your orboto workspace is connected to orboto Mail (see
orboto integration), a key is issued for
you automatically and you don't need to visit the dashboard for this
step.
Send a single email
curl https://mail.orboto.io/api/v1/send \
-X POST \
-H "Authorization: Bearer oms_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme Support <support@yourdomain.example>",
"to": "user@example.com",
"subject": "Welcome to Acme",
"html": "<h1>Welcome!</h1><p>Thanks for signing up.</p>",
"text": "Welcome! Thanks for signing up.",
"tags": { "workflow": "welcome" }
}'A successful send returns 200:
{
"messageId": "0100019a-1b2c-4d5e-8f90-abcdef123456",
"status": "queued",
"overage": false,
"remainingQuota": {
"current": 48,
"total": 10000,
"resetAt": "2026-09-01T00:00:00.000Z",
"percentUsed": 0.0048,
"softWarnAt": 0.8,
"softWarnTriggered": false,
"dailyCap": null,
"creditBalance": 0
}
}status is always "queued" on success - orboto Mail hands the message
to SES synchronously and returns immediately; delivery, bounce, and
complaint outcomes arrive later as delivery events.
remainingQuota reports the account's quota state after this send so
you can decide client-side whether to slow down before the next one.
Request fields
| Field | Required | Notes |
|---|---|---|
from | yes | Must be on a verified sending domain for your account. Accepts a bare address or Display Name <address>. |
to | yes | A single recipient address. |
cc, bcc | no | Arrays, up to 50 addresses each. Bcc recipients never appear in headers seen by anyone else on the message. |
subject | conditionally | Required unless templateId supplies one. Max 998 characters (RFC 5322 line length). |
html, text | conditionally | At least one of html, text, or templateId is required. html and text cannot be combined with templateId in the same call - pick raw content or a template. |
templateId + variables | no | Send via a stored template instead of raw content. |
tags | no | A flat object of string keys/values (max 256 chars each), stored on the send record for your own filtering/reporting. |
attachments | no | Array, max 20 items. See below. |
Attachments
{
"attachments": [
{
"filename": "invoice.pdf",
"contentType": "application/pdf",
"content": "<base64-encoded file bytes>"
}
]
}Up to 20 attachments per send, base64-encoded in the JSON body, with a
combined decoded size limit of 30 MB. Each entry also accepts an
optional contentId for inline references (e.g. an image referenced
from the HTML body via cid:).
Send a batch
curl https://mail.orboto.io/api/v1/send/batch \
-X POST \
-H "Authorization: Bearer oms_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{ "from": "support@yourdomain.example", "to": "a@example.com", "subject": "Hi A", "html": "<p>Hi A</p>" },
{ "from": "support@yourdomain.example", "to": "b@example.com", "subject": "Hi B", "html": "<p>Hi B</p>" }
]
}'POST /v1/send/batch runs every message through the same validation,
quota, and send pipeline as a single send, up to 100 messages per call.
It always returns 200, even if every message failed - read summary
and each item's ok flag rather than the HTTP status:
{
"results": [
{ "index": 0, "ok": true, "messageId": "...", "status": "queued", "overage": false },
{ "index": 1, "ok": false, "error": "recipient_suppressed", "message": "..." }
],
"remainingQuota": { "...": "last snapshot after the batch" },
"summary": { "queued": 1, "rejected": 0, "suppressed": 1, "skipped": 0 }
}Messages are processed in array order. If the account's quota runs out
partway through the batch, every remaining item comes back with
quotaSkipped: true instead of being retried - the quota won't refill
mid-batch, so there's no point checking again.
Checking your quota without sending
curl https://mail.orboto.io/api/v1/quota \
-H "Authorization: Bearer oms_live_your_key_here"Returns the same remainingQuota shape shown above, wrapped in
{ "quota": { ... } } - useful before kicking off a large batch, or for
your own usage dashboards.
Viewing what was sent
curl "https://mail.orboto.io/api/v1/sends?limit=20&status=bounced" \
-H "Authorization: Bearer oms_live_your_key_here"GET /v1/sends lists your account's sends, most-recent first, cursor-
paginated via ?cursor=<value>&limit=<1-100> (nextCursor in the
response feeds the next call). Filter with status
(queued/delivered/bounced/complained/rejected), region
(eu-central-1/eu-west-1), or since (ISO timestamp). GET /v1/sends/:id returns one send's full detail, including open-tracking
counters if enabled on the sending domain. The same history is browsable
at account.orboto.io/mail/sends.
Error responses
Every rejection returns a JSON body with error, reason, and a
human-readable message. The reason values you'll actually hit:
| HTTP | reason | Meaning |
|---|---|---|
| 400 | from_domain_not_authorized | The from address's domain isn't on this account at all. Add it - see Sending domains. |
| 400 | from_domain_not_verified | The domain is on the account but its DNS records aren't verified yet. |
| 400 | subject_required | No subject given and no templateId to supply one. |
| 400 | template_variable_validation | variables didn't satisfy the template's schema. |
| 422 | recipient_suppressed | The recipient is on your suppression list (a previous hard bounce, complaint, or manual add). The send is skipped and does not consume quota. |
| 402 | quota_exhausted_* / payment_required | Monthly quota (and any overage allowance) is used up. message explains the specific reason and what to do next. |
| 503 | send_failed / wallet_unavailable | A transient upstream problem (both SES regions unreachable, or - for above-quota sends - the billing wallet couldn't be reached). Safe to retry. |
Troubleshooting
| Symptom | Fix |
|---|---|
401 token_missing / token_malformed | Check the Authorization: Bearer oms_live_... header is present and the key wasn't truncated when copied. |
401 token_revoked | The key was revoked (manually, or because the OAuth connection it came from was disconnected). Issue a new one. |
400 from_domain_not_authorized | The from address's domain has to be added and verified first - see Sending domains and DNS. |
422 recipient_suppressed on an address you expect to be fine | Check GET /v1/suppression/:email - a past bounce or complaint may have added it. Remove it via DELETE /v1/suppression/:email if it was a false positive. |
Send succeeds (200 queued) but the recipient never got it | Query GET /v1/sends/:id a little later for the delivered/bounced outcome, or subscribe a webhook to get notified the moment it changes. |
Getting 404/405 on every call | Almost always a missing or duplicated /api segment - the correct base is https://mail.orboto.io/api/v1/.... |