Errors
Every HTTP API error uses one envelope:
{ "error": { "type": "validation_error", "code": "routing_provider_not_configured", "message": "Pinned provider(s) not configured on this project: postmark. Attach them or drop the routing pin." }}Branch on code — it’s stable. message is for humans and may change.
Field-validation failures carry the same envelope plus error.details, one
entry per failing field; error.code mirrors the first, so a single-mistake
request stays branchable on code alone:
{ "error": { "type": "validation_error", "code": "from:required", "message": "Validation failed — `details` lists each failing field.", "details": [ { "field": "from", "code": "from:required" }, { "field": "subject", "code": "subject:required" } ] }}Batch requests respond 207 Multi-Status: each element of data is either a
send object or one of these envelopes, so
check per element. A persistence failure on one
element surfaces as api_error / server_error — retry just that element.
If a Free-plan limit runs out mid-batch, the elements past the limit surface the
same monthly_quota_exceeded / daily_send_cap_exceeded envelopes a single
send would get — the accepted elements before them still send.
The full contract, including these envelopes, is machine-readable at
mailway.net/openapi.yaml.
At a glance
Section titled “At a glance”| Status | type |
code |
|---|---|---|
| 400 | validation_error |
content_encoding:invalid |
| 401 | authentication_error |
missing_api_key · invalid_api_key · key_expired · mode_mismatch |
| 402 | quota_error |
monthly_quota_exceeded · throughput_quota_exceeded · scheduling_unavailable · free_team_limit |
| 422 | permission_error |
no_providers_configured |
| 413 | validation_error |
body:too_large |
| 422 | validation_error |
routing_provider_not_configured · suppressed_recipient · sender_domain:limit_reached · category:limit_reached · tags:limit_reached · idempotency_key:* · scheduled_at:* · attachments:too_large · attachments:total_too_large · field errors |
| 429 | rate_limit_error |
daily_send_cap_exceeded · rate_limited |
Three rules cover almost every case:
- 402 is a billing state, not a bug. Surface it to whoever owns the plan. Retrying won’t help until the month rolls over or the plan changes.
- 422 means the request will never succeed as-is. Fix the payload; don’t retry blindly.
- Retry 429 and 5xx with backoff and an
Idempotency-Key, so retries can’t double-send.
400 — the body can’t be read
Section titled “400 — the body can’t be read”content_encoding:invalid
Section titled “content_encoding:invalid”Cause. The request sends a Content-Encoding header (gzip or br), but
the body doesn’t decode as that encoding.
Fix. Compress the body with the encoding the header names, or send plain
JSON without a Content-Encoding header.
401 — authentication
Section titled “401 — authentication”missing_api_key
Section titled “missing_api_key”Cause. No Authorization header, or one that isn’t
Bearer mw_live_… / Bearer mw_test_….
Fix. Send Authorization: Bearer mw_live_…. A key is its prefix plus 40
letters and digits — see what a key looks like.
invalid_api_key
Section titled “invalid_api_key”Cause. The key doesn’t match any active key: mistyped, revoked, or belonging to a deleted project.
Fix. Create a new key in the console (the project’s Access section). Keys are shown once at creation and stored hashed, so a lost key can’t be recovered — only replaced.
key_expired
Section titled “key_expired”Cause. The key had an expiry date and it has passed.
Fix. Create a new key. If you rotate on a schedule, note that expiry is per key, so overlap the new key before the old one lapses.
mode_mismatch
Section titled “mode_mismatch”Cause. The key’s mode doesn’t match the host. A mw_test_… key was used
on api.mailway.net, or a mw_live_… key on the sandbox host.
Fix. Match them: live keys on api.mailway.net, test keys on the
sandbox. This binding is deliberate — it’s what stops a
misconfigured environment variable from sending real mail from a test suite.
402 — quota
Section titled “402 — quota”monthly_quota_exceeded
Section titled “monthly_quota_exceeded”Cause. The plan’s monthly message quota is spent. Free plan only: every other plan soft-overages instead of blocking.
Fix. Upgrade the plan, or wait for the calendar month to roll over. A plan change lifts the limit immediately.
throughput_quota_exceeded
Section titled “throughput_quota_exceeded”Cause. The plan’s monthly data-volume cap is spent, including the grace margin paid tiers get. Large attachments reach this before the message count.
Fix. Add a data-volume add-on or upgrade. Also worth checking whether the volume is intended — see the limits reference for each plan’s cap.
free_team_limit
Section titled “free_team_limit”Cause. The team is on the Free plan, and the person who created
it already has an older Free team. Only one Free team per person can send real
mail (Limits & quotas).
Test-key mail on api.mailway.dev isn’t affected.
Fix. An admin or someone with the Finance role chooses a paid plan for this team in the console, under Billing. Upgrading or deleting an older Free team lifts it too.
scheduling_unavailable
Section titled “scheduling_unavailable”Cause. The send asked for scheduled_at, and the plan doesn’t include
scheduled sends (Pro and up). Nothing is stored and no quota is used. In a batch,
only that element gets the error.
Fix. Send without scheduled_at, or move to a plan that includes scheduling (Pro and up).
413 — the body is too large
Section titled “413 — the body is too large”body:too_large
Section titled “body:too_large”Cause. The request body is larger than the 25 MB limit. For a compressed body the limit applies once it is decompressed.
Fix. Send smaller attachments, or split a batch into smaller requests.
422 — the request can’t succeed as sent
Section titled “422 — the request can’t succeed as sent”no_providers_configured
Section titled “no_providers_configured”Cause. The project has no attached provider, so there is nothing to send
through. Typed permission_error rather than validation_error because the
payload is fine — the project isn’t ready.
Fix. Attach a provider to the project. Mailway is BYOK: it never sends through an account of its own, so a project with no provider genuinely cannot deliver.
routing_provider_not_configured
Section titled “routing_provider_not_configured”Cause. routing.provider pinned a provider that isn’t attached to this
project. The message names the offending provider families.
Fix. Attach that provider, or drop the pin and let the project’s routing strategy choose. A pin is never silently rerouted — if you asked for a specific provider, Mailway would rather fail loudly than quietly send through a different one.
suppressed_recipient
Section titled “suppressed_recipient”Cause. At least one recipient is on the project’s
suppression list, from a hard bounce or a complaint. The
message names the addresses.
Fix. Drop those recipients, or un-suppress the address in the console if you know the bounce is stale. Sending anyway is the wrong instinct: repeat hard bounces are exactly what damages a sending reputation, which is why the gate exists.
sender_domain:limit_reached
Section titled “sender_domain:limit_reached”Cause. The from domain is new to this project and the plan’s
sender-domain limit is already reached. Domains register on first use, so this
fires on the first send from a new domain, not on setup.
Fix. Send from an already-registered domain, remove an unused one in the console, or upgrade. Note that subdomains count separately.
category:limit_reached
Section titled “category:limit_reached”Cause. The project already has its maximum number of distinct
category values. Categories are created on first use, so an unbounded or
templated category value (an order ID, a timestamp) reaches the limit quickly.
Fix. Reuse an existing category or drop the field. Categories are meant to
be a small, stable vocabulary — a per-mail identifier belongs in
metadata, not here.
tags:limit_reached
Section titled “tags:limit_reached”Cause. The send request carries a tag that is new to the project, and the
project already has 1,000 distinct tags. Tags are created on first use, so a
per-mail value in tags (an order ID, a timestamp) reaches the limit quickly.
Fix. Reuse existing tags. A value that identifies one mail belongs in
metadata, which has no project-wide limit and comes back on every webhook
event.
idempotency_key:*
Section titled “idempotency_key:*”idempotency_key:invalid · idempotency_key:invalid_length
Cause. The Idempotency-Key header is malformed. It must be 12–40
characters of [A-Za-z0-9_-].
Fix. Use a UUID or a similar opaque token. Note the key scopes to the project and the first body wins: replaying the same key returns the original send rather than sending again. See idempotency.
scheduled_at:*
Section titled “scheduled_at:*”scheduled_at:invalid · scheduled_at:too_soon · scheduled_at:too_late
Cause. scheduled_at must be a valid ISO 8601 instant, in the future, and
no more than 72 hours out.
Fix. Send an absolute timestamp with an offset (2026-08-04T09:00:00Z).
For anything beyond 72 hours, hold the message in your own scheduler and
submit it when it’s due.
attachments:too_large
Section titled “attachments:too_large”Cause. One attachment exceeds 10 MB decoded. The cap is on decoded bytes, so base64 in the request body is ~33% larger than the number being checked.
Fix. Compress it, or host the file and send a link. Large attachments also hurt deliverability independently of this limit.
attachments:total_too_large
Section titled “attachments:total_too_large”Cause. All attachments together exceed 25 MB decoded.
Fix. Same as above. Note that up to 20 attachments are allowed, and inline images count toward both caps.
Field errors
Section titled “Field errors”Cause. Ordinary body validation: a missing from, a malformed address, no
content, more than 50 recipients across to/cc/bcc, an over-long
filename. error.details lists every offending field as
{ "field", "code" } — field is the dotted path (subject, to.1,
attachments.0.filename), and error.code mirrors the first entry.
Fix. Read error.details and correct the payload. The full set of caps is
in the limits reference. If you legitimately
need more than 50 recipients per message, send in batches — and see
the SES recipient cap for what
providers do with large recipient sets.
429 — slow down
Section titled “429 — slow down”daily_send_cap_exceeded
Section titled “daily_send_cap_exceeded”Cause. The Free plan’s daily send limit. Typed rate_limit_error rather
than a 402 on purpose: it’s a limit that resets, not a plan you’ve outgrown.
Fix. Wait — it resets at 00:00 UTC — or upgrade for a higher limit.
Rate limited
Section titled “Rate limited”rate_limited
Cause. Too many requests. The budgets are per key:
| Endpoint | Limit |
|---|---|
POST /v1/send |
600 requests/min per key |
POST /v1/send/batch |
6 requests/min per key (100 messages each, so the same 600 messages/min) |
Requests without a well-formed Authorization: Bearer mw_… header fall back to a much tighter per-IP bucket, so a
malformed Authorization header can look like a rate-limit problem when it’s
really an auth problem.
Fix. Honor the Retry-After header and back off. If you’re sending in
bulk, POST /v1/send/batch moves 100 messages per request and is the cheaper
path against the same message budget.
Cookie consent
We may run ads on Google and Reddit. If we do, knowing whether they work needs your OK for one cookie each from them, used only to tie a signup back to the ad that brought it. Analytics here is cookieless either way, and declining changes nothing else.
Change your mind any time with "Cookie choices" in the footer. Details in the privacy policy.