Docs
Errors and limits.
What each error means, what usually causes it and what to do next. Then every limit the API enforces, the rules for webhook URLs, and when a retry is safe.
OverviewQuickstartConceptsErrors and limits
Error shape
Every error from /v0 is JSON with two fields: name, a short machine-readable code to branch on, and message, a sentence for people and logs.
HTTP/2 403
Content-Type: application/json
{
"name": "forbidden",
"message": "Recipient jordan@partnerco.com is not in this key's allow_recipients"
}A bot should decide what to do from the status and name, and log message.
Status codes
| Status | name | Common causes | What to do |
|---|---|---|---|
401 | unauthorized | No Authorization header, a header not in the form Bearer <key>, a mistyped key, or a revoked key. | Check the key and the header. Do not retry in a loop: wrong keys count toward the lockout. |
403 | forbidden | The key lacks the scope for this call; an admin call made with a non-admin or inbox-limited key; a users call made without the owner's console session; a recipient outside allow_recipients on a key without an approval gate; a key approving its own parked send. | Read message: it names the missing scope or the refused address. Use a key with the right scope, add the recipient to the allowlist, set approval: "outside", or have a different key or a person approve. |
403 | csrf_failed | Console only: a signed-in browser write without the right Origin or X-Emlbot-CSRF header. | Reload the console. API keys never see this. |
404 | not_found | A wrong path or method (No such endpoint); an inbox, message, thread, attachment, key, webhook, user or approval that does not exist; an inbox outside an inbox-limited key's list; an id that was not URL-encoded. | Check the path and that ids are URL-encoded. For a limited key, check its inbox_ids with GET /me. |
405 | method_not_allowed | An OPTIONS request, usually a browser CORS preflight. | Call the API from your server, not from a web page. There is no CORS. |
409 | already_exists | Creating an inbox whose address is taken (including a deleted inbox's address); inviting a user who already exists. | Pick another username, or create inboxes with a client_id so a retry returns the existing inbox. |
409 | conflict | Approving or rejecting an approval that was already decided; approving after the inbox was deleted or the requesting key revoked; demoting or disabling the last owner. | Refresh the item. For approvals, check its status with GET /approvals. |
413 | payload_too_large | A request body over 30 MB. Base64 attachments are the usual reason: they are about a third larger than the files. | Send fewer or smaller attachments. Attachments are capped at 25 MB per message anyway. |
422 | validation_error | A body that is not a JSON object; a missing or malformed field; a value over a limit; a bad limit, after or page_token. | Fix the request. message names the field and the rule. Retrying unchanged gives the same answer. |
429 | rate_limited | Two different causes; see below. | Wait for Retry-After seconds before trying again. |
500 | internal_error | Something failed on our side. The message is always Internal error; details go to our logs, never to the response. | Retry reads with backoff. For sends, check before retrying (see retries). Tell info@emlbot.com if it persists. |
502 | send_failed | The outbound email provider refused the message. Nothing was sent and the send was not counted against the daily cap. | Retry with backoff. If it keeps failing for the same message, check the recipients and attachments. |
The two kinds of 429
Both use name: "rate_limited" and a Retry-After header in seconds. Tell them apart by the call and the headers:
- Daily send cap spent. Only on send, reply and approve. The message says
Daily send cap of 100 reached for this key(with your cap), and the response also carriesX-RateLimit-Limit,X-RateLimit-Remaining: 0andX-RateLimit-Reset. The cap resets at midnight UTC. Queue the send until then, or raise the cap with a new key if the bot genuinely needs more. - Too many failed sign-ins from your IP address. Any call. After 20 requests with a wrong key from one IP address within 10 minutes, that address is locked out, even for valid keys, until the oldest failure is more than 10 minutes old.
Retry-Aftersays how long. Find the process using a bad or revoked key and stop it. Successful calls and calls with no key at all do not count.
Limits
Requests
| What | Limit | Over the limit |
|---|---|---|
| JSON request body | 30 MB | 413 payload_too_large |
| Failed authentications per IP address | 20 per 10 minutes | 429 rate_limited with Retry-After |
Page size (limit) for inboxes, messages, threads, approvals, audit | Default 50, maximum 500 | Larger values are lowered to 500 |
| Page size for events | Default 100, maximum 1000 | Larger values are lowered to 1000 |
There is no per-key request rate limit today beyond these. Please poll at a sensible interval; a webhook removes the need to poll at all.
Sending
| What | Limit |
|---|---|
| Recipients per send | 50 in total across to, cc and bcc |
reply_to addresses | 10 |
| Attachments | 20 per message, 25 MB in total (decoded); file names up to 255 characters |
| Subject | 500 characters |
Custom headers | 20 per message; names up to 64 characters from A-Z a-z 0-9 -; values up to 998 characters |
| Daily sends per key | daily_send_cap: 1 to 100,000, default 200, per UTC day |
| Addresses | A plain address or Name <address>. Names containing @ , ; " must be fully quoted. No control characters. |
Custom headers cannot override how a message is routed or authenticated. These names are reserved, and a custom header using one is left out of the message: From, To, Cc, Bcc, Subject, Message-ID, Date, MIME-Version, Reply-To, In-Reply-To, References, Sender, Return-Path, Received, DKIM-Signature, Authentication-Results, List-Unsubscribe-Post, anything starting with Content-, and anything starting with X-SES-.
Receiving
| What | Limit |
|---|---|
| Inbound message size | 25 MB, including attachments. Larger mail is refused during SMTP delivery, so the sending server reports the failure to the sender. |
| Unknown address | Mail to an address with no inbox is refused with "Unknown mailbox". |
Names and labels
| What | Rule |
|---|---|
| Inbox username | 1 to 64 characters from a-z 0-9 . _ -, starting with a letter or digit, no .. |
Inbox display_name | Up to 100 characters |
Inbox client_id | Up to 128 characters |
| Labels | 1 to 64 characters from a-z 0-9 . _ : -; up to 20 in add_labels, 20 in remove_labels and 20 in labels on a send |
Key name | Up to 100 characters |
Key inbox_ids | Up to 100 inboxes |
Key allow_recipients | Up to 200 entries, each up to 254 characters |
Webhooks
| What | Limit |
|---|---|
| Response time | 10 seconds for a 2xx, or the attempt fails |
| Retries | 8, over about 28 hours (5 s, 30 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h) |
| Consecutive failures before the webhook is disabled | 50 |
| URL length | 2048 characters |
Webhook destination rules
Webhook URLs are checked when you create the webhook and again before every delivery, so EMLbot can never be pointed at a private network. A URL is refused with 422 validation_error unless all of these hold:
- It uses
https:on the default port, 443. - It contains no username or password.
- Its host is a name, not an IP address in any form (dotted, decimal, hex, octal, short or IPv6). This rules out loopback, private ranges, link-local and cloud metadata addresses.
- Its host has at least one dot, is not
localhost,metadataorinstance-data, and does not end in.local,.localhost,.internal,.localdomain,.lan,.home.arpa,.intranetor.corp.
At delivery time, redirects are not followed (a 3xx counts as a failure), the request times out after 10 seconds, and your response body is discarded unread. Point the webhook at the final URL, and answer with a plain 2xx.
Retry guidance
Safe to retry as they are
- Every
GET. PATCHon labels: adding a label twice or removing one that is gone changes nothing.POST /inboxeswith aclient_id.DELETEon keys (revoking twice keeps the first time).- Approve and reject: a second attempt answers
409 conflictinstead of acting twice.
Retry with care
- Send and reply. There is no idempotency key for sends. If a send times out on your side, it may have gone through. Before sending again, list the inbox with
labels=sent(or read the event log formessage.sent) and check whether it is there. - Creating keys and webhooks. A retry after a lost response creates a second one. List them, keep one, and revoke or delete the extra.
When to back off and when to stop
429: wait at leastRetry-Afterseconds.500and502, and network errors: retry with exponential backoff and jitter, for example 1 s, 2 s, 4 s, 8 s, then give up and alert.- Other
4xxerrors: do not retry unchanged. The same request gets the same answer. Fix the key, the path or the body first.