Skip to content

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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).

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”

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.

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.

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.

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.

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.

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: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: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.

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.

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.

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.

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

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.