Skip to content

Errors

Every Send API error uses one envelope:

{
"error": {
"type": "validation_error",
"code": "routing_provider_not_configured",
"message": "Pinned provider(s) not configured on this project: postmark-api. 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 cap runs out mid-batch, the elements past the cap 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
401 authentication_error missing_api_key · invalid_api_key · key_expired · mode_mismatch
402 quota_error monthly_quota_exceeded · throughput_quota_exceeded
422 permission_error no_providers_configured
422 validation_error routing_provider_not_configured · suppressed_recipient · sender_domain:limit_reached · category: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. No Authorization header, or one that isn’t Bearer mw_live_… / Bearer mw_test_….

Fix. Send Authorization: Bearer mw_live_…. Keys are 40 characters after the prefix — see the credential grammar.

Cause. The key doesn’t match any active key: mistyped, revoked, or belonging to a deleted project.

Fix. Mint a fresh key in the dashboard. 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 tier only — paid plans soft-overage 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 throughput add-on or upgrade. Also worth checking whether the volume is intended — see the limits reference for each plan’s cap.

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

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) exhausts the cap quickly.

Fix. Reuse an existing category or drop the field. Categories are meant to be a small, stable vocabulary — a per-message identifier belongs in your own metadata, not here.

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_far

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 tier’s daily send ceiling. Typed rate_limit_error rather than a 402 on purpose: it’s a ceiling 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 valid key 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.