Docs · Errors
Errors and rate limits
A failed request answers with an HTTP status and a problem document (RFC 9457, application/problem+json). Branch on the status and the problem type; show detail to a developer, not to an end user.
The error format
{
"type": "https://owlpost.to/problems/idempotency-conflict",
"title": "This Idempotency-Key was used for a different request",
"status": 409,
"detail": "Use a new key for a new message; a retry must repeat the original request exactly.",
"instance": "01J9ZQ5X7K3M2N4P6R8S0T1V2W"
}| Field | Meaning |
|---|---|
type | A URI naming the problem. Match on its last path segment, the slug (idempotency-conflict). Owlpost's own problems live under https://owlpost.to/problems/; the ones the underlying platform raises (authentication, rate limits, not found, internal) under https://factory0.ventures/problems/. The URIs name the problem; they are not pages to fetch. |
title | A short, stable summary of the problem type |
status | The HTTP status, repeated |
detail | What went wrong this time, in words. May be absent. |
instance | The request id, also in the x-request-id response header. Quote it when you contact us. |
errors | On POST /v1/emails and /batch validation failures only: every broken field as {field, message}. Fields are paths as you wrote them: to[2], attachments[0].content, headers.Subject, and [7].to in a batch. |
One exception: a body that is not valid JSON, or is missing a required field, on the domain, webhook, inbound and suppression routes is refused by the web framework before Owlpost reads it, with a short plain-text 4xx instead of a problem document. The send routes always answer with a problem.
Status codes and problem types
| Status | Slug | When |
|---|---|---|
200, 201, 202 | Success. 201 when a resource was created (inbox, rule, webhook, suppression); 202 when a reply was queued. | |
400 | validation-failed | A rule on the suppression and topic routes: a bad address or topic, a complaint removed without a reason. (The same slug is a 422 elsewhere; see below.) |
401 | api-key-unauthorized | The key is missing, malformed, unknown or revoked. One answer for all four, so a probe learns nothing. |
403 | api-key-forbidden | The key is valid but lacks the route's scope |
domain-not-verified | Live mail from a domain the account has not verified | |
inbox-limit | The account has all the inboxes it may have (100) | |
held-needs-manage | Listing held mail without inbound:manage | |
404 | not-found | No such route, or no such resource in your account. Another account's ids read as missing. |
link-expired | An attachment link that expired (after seven days) or was altered | |
409 | idempotency-conflict | A key reused with a different request, or a batch that raced another (details) |
domain-taken | The domain is already added, by this account or another | |
inbox-exists | That inbox address on your domain is taken | |
route-exists | A routing rule for that match exists | |
not-held | Releasing a message that is not held | |
413 | request-too-large | The body is over the route's limit (below). A send between 8 and 32 MiB is refused by the framework instead, with a plain-text 413. |
422 | validation-failed | The request broke one or more rules: sends, batches, domains, inboxes, routing rules, replies, webhooks |
429 | rate-limited | The key's rate limit (below) |
inbox-rate-limited | An agent inbox sent 60 messages in the last hour | |
thread-rate-limited | A thread got 10 messages from Owlpost in the last hour | |
500 | internal | Our fault. The body carries no internals; retry with backoff and send us the instance. |
502 | ses-refused | Amazon SES refused a domain operation |
503 | ses-unavailable | SES did not answer a domain operation; try again |
ses-not-configured | Sending domains are not available on this deployment | |
not-ready | A storage lookup failed; try again |
Retry 429, 500, 502 and 503 with backoff. With an Idempotency-Key, retrying a send is always safe. Do not retry other 4xx answers unchanged.
Rate limits
Each API key may make 60 requests a minute. A batch counts as one request per message in it. Over the limit, the answer is 429 with the slug rate-limited, and nothing was stored. The 429 does not carry a Retry-After header yet, so back off on your side: wait a few seconds, then retry with growing pauses.
The limit applies to the email send and read routes, domains, webhooks and every inbound route. The suppression and topic routes are not rate-limited today.
When the rate limiter itself cannot be reached, the API refuses rather than letting traffic through unmetered, so a 429 can also mean "try again in a moment".
Agent inboxes have two more limits, counted per hour: 60 messages from one inbox and 10 into one thread (see loop limits).
Size limits
| What | Limit |
|---|---|
| One message, attachments included after base64 | 5 MiB |
POST /v1/emails request body | 8 MiB |
POST /v1/emails/batch request body | 32 MiB, 100 messages |
| Request bodies on other routes | 64 KiB |
| Recipients per message (to, cc and bcc together) | 50 |
| Attachments, headers, tags per message | 50 each |
| A received message | 10 MiB |
| Text plus HTML of a reply from an inbox | 512 KiB |