Skip to content

HTTP API

POST https://api.mailway.net/v1/send # production
POST https://api.mailway.dev/v1/send # sandbox — stored, never auto-forwarded

The machine-readable contract is published at mailway.net/openapi.yaml (OpenAPI 3.1) — every shape on this page, code-verified by the API’s own test suite so it can’t drift. Generate a typed client from it, import it into Postman, or hand it to a coding agent.

Authorization: Bearer mw_live_… # production keys — accepted only on api.mailway.net
Authorization: Bearer mw_test_… # test keys — drive the sandbox

Keys are created per-project in the console (the project’s Access section) and shown once — the key is the project selection, so there’s no project field in the request body. Full details (host binding, storage, rotation): Authentication & API keys.

Terminal window
curl https://api.mailway.net/v1/send \
-H "Authorization: Bearer $MAILWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": { "email": "billing@yourapp.com", "name": "YourApp" },
"to": "customer@gmail.com",
"subject": "Invoice #2042",
"html": "<p>Your invoice #2042 is ready.</p>",
"text": "Your invoice #2042 is ready."
}'
{
"id": "m.01k8w9abcdefghjkmnpqrstvwx",
"status": "queued",
"scheduled_at": null,
"paused": false,
"category": null,
"project_id": "p.01k8w9…",
"routing": { "provider": null, "strategy": null }
}

id is the mail ID, the permanent identifier that threads through the console, webhook events, and share links. Webhooks send the same value as data.mail.uid, and project_id as data.mail.project.uid. status is queued, scheduled (when scheduled_at is in the future), paused, or captured (a sandbox send — stored, never auto-forwarded). queued and scheduled are the pending state in the console and on webhooks; paused and captured are states of their own.

Field Type Notes
from address Required. "a@b.com" or { "email": …, "name": … } (a display name goes in name; the "Name <a@b.com>" string form isn’t accepted, so use { "email", "name" }).
to, cc, bcc address | address[] to is required; cc and bcc are optional. At most 50 combined across the three; each entry takes any address form.
reply_to address | address[] Optional.
subject string Required. ≤ 998 characters (the RFC 5322 line limit).
html, text string At least one, ≤ 2 MB each. Send both when you can — some inboxes still want the text part.
headers object Custom headers (e.g. List-Unsubscribe). A name is printable ASCII with no space or colon, and can’t start with a dot; anything else is a 422 (headers:invalid). Transport-critical headers and the X-Mailway-* prefix are reserved and rejected. Providers can limit them: MailerSend accepts custom headers over its API only on a Professional plan or higher, and some providers change or drop specific headers (What providers change).
attachments object[] ≤ 20, each { "filename", "content" (base64), "content_type", "content_id"?, "disposition"? } — 10 MB decoded each, 25 MB combined. content_id + "disposition": "inline" for inline images, referenced from the HTML as cid:<content_id>. A bare id such as logo is sent as logo@mailway (a Content-ID needs an @), and the HTML’s cid:logo references are pointed at it. Brevo’s and Nuntly’s APIs can’t embed images: through them an inline image arrives as a regular attachment.
tags string[] Optional. ≤ 10 labels for groups of mails (e.g. password-reset), each ≤ 64 chars of [A-Za-z0-9_-]. They become the project’s tags: you can filter mail by them in the console, a rule can match them (tag_present), and every webhook event lists them in data.mail.tags. Case doesn’t matter within a project: Invoice and invoice are one tag, and webhook events show the spelling used first. Once a project has 1,000 distinct tags, a send with a new one is rejected (tags:limit_reached). Reusing a tag always works, and so does a rule’s tag action.
metadata object Optional. Your own values for this one mail, such as an order or user ID: ≤ 10 keys, keys ≤ 40 chars, string values ≤ 500 chars. Key order isn’t kept. Every webhook event for the mail carries them in data.mail.metadata, so you can match an event to your records without a lookup. Mailway deletes them with the mail’s content when its retention period ends (webhook deliveries already made keep their copy for 30 days).
category string One per mail — the reporting bucket your per-category stats group by (e.g. "Password reset"). ≤ 64 chars, letters/digits/space/_-./:, case-sensitive, echoed in the response. A project holds at most 100 distinct categories: reusing one is always free, a 101st new name is rejected (category:limit_reached). Use category for reporting, tags for groups of mails, and metadata to match events to your own records.
scheduled_at ISO 8601 Defer the send — up to 72 hours ahead (Pro and up; on other plans, 402 scheduling_unavailable).
paused boolean Accept + store the message but hold it until released in the console.
routing.provider string | string[] Pin the send to a provider (or an ordered preference list) attached to the project, by family — amazon-ses, sendgrid, mailgun, mandrill, postmark, brevo, gmail, mailtrap, smtp, resend, mailjet, mailersend, nuntly, smtp2go. Mailway still picks the account’s transport.
routing.strategy string Override the project’s provider-selection strategy for this send: fallback, round-robin, random, or free-first.
provider_options object Provider-specific extras, allowlist-validated — keyed by provider family — { "amazon-ses": { "configuration_set": … } } (SES), { "postmark": { "message_stream": … } } (Postmark) — and applied whichever transport the send takes, except Postmark’s stream on a send through Postmark’s API: that doesn’t take effect yet, and the mail goes to Postmark’s default transactional stream. Unknown options are rejected, never silently dropped.

The full cap sheet — including rate limits and plan quotas — is on one page: Limits & quotas.

Pass an Idempotency-Key header (12–40 chars of [A-Za-z0-9_-]) to make retries safe:

Idempotency-Key: invoice-2042-receipt

A repeated key returns the original message’s 202 instead of sending again. Keys are unique per project and never expire: a key is used once, for the life of the project (see Data retention).

POST /v1/send/batch

Send up to 100 messages in one request — the body is { "messages": [ … ] }, each element using the exact /v1/send message schema. Elements are independent sends: one failing validation doesn’t sink the rest, which is why the response is 207 Multi-Status — data is an array in your input order, each element either the normal send object ({ "id": "m.…", "status": … }) or an error envelope for that message. Check per element; don’t assume all-or-nothing. Idempotency-Key is not honored on batch, and each accepted element counts against your quotas.

Point the same code at https://api.mailway.dev with a mw_test_… key: messages are validated, stored, and visible in the console with a sandbox badge — but never forwarded automatically. The payload shape is identical, so promoting to production is a hostname + key swap — and if a specific capture turns out to be the real thing, you can release it from the console instead of re-running the send.

Status Meaning
401 Missing or invalid API key, or a key used on the wrong host (mw_live_ on api.mailway.dev, mw_test_ on api.mailway.net).
402 A plan limit — the monthly message quota or data volume, or scheduled_at on a plan without scheduling. The body’s error.code says which.
422 Validation failed — the body lists field-level errors, e.g. routing_provider_not_configured or an invalid Idempotency-Key.
429 Rate limited — back off and retry.

The full code catalog with handling advice is on the Errors page; when a send doesn’t behave the way you expect, troubleshooting maps the symptom to a fix.