orbotodocs
orboto Mail

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

FieldRequiredNotes
fromyesMust be on a verified sending domain for your account. Accepts a bare address or Display Name <address>.
toyesA single recipient address.
cc, bccnoArrays, up to 50 addresses each. Bcc recipients never appear in headers seen by anyone else on the message.
subjectconditionallyRequired unless templateId supplies one. Max 998 characters (RFC 5322 line length).
html, textconditionallyAt 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 + variablesnoSend via a stored template instead of raw content.
tagsnoA flat object of string keys/values (max 256 chars each), stored on the send record for your own filtering/reporting.
attachmentsnoArray, 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:

HTTPreasonMeaning
400from_domain_not_authorizedThe from address's domain isn't on this account at all. Add it - see Sending domains.
400from_domain_not_verifiedThe domain is on the account but its DNS records aren't verified yet.
400subject_requiredNo subject given and no templateId to supply one.
400template_variable_validationvariables didn't satisfy the template's schema.
422recipient_suppressedThe recipient is on your suppression list (a previous hard bounce, complaint, or manual add). The send is skipped and does not consume quota.
402quota_exhausted_* / payment_requiredMonthly quota (and any overage allowance) is used up. message explains the specific reason and what to do next.
503send_failed / wallet_unavailableA transient upstream problem (both SES regions unreachable, or - for above-quota sends - the billing wallet couldn't be reached). Safe to retry.

Troubleshooting

SymptomFix
401 token_missing / token_malformedCheck the Authorization: Bearer oms_live_... header is present and the key wasn't truncated when copied.
401 token_revokedThe key was revoked (manually, or because the OAuth connection it came from was disconnected). Issue a new one.
400 from_domain_not_authorizedThe 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 fineCheck 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 itQuery 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 callAlmost always a missing or duplicated /api segment - the correct base is https://mail.orboto.io/api/v1/....

On this page