Send API & SMTP
Send transactional email from your verified domain over HTTPS or SMTP — or point the same request at the sandbox and nothing is ever delivered.
One key, two ways to send — authenticated with the send key (mw_live_…) you create on the
Sending Domains page once your domain is verified. Use the HTTP API below if your app can
make HTTPS calls; use the SMTP relay if it only speaks SMTP. No SDK required either way.
Endpoint
POST https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send
Headers
Authorization: Bearer <your API key>
Content-Type: application/json
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
From | string | Yes | Must be an address on the domain your key was created for — a mismatch is rejected before anything is sent. |
To | string[] | Yes | Up to your account's per-message recipient cap (50 by default — a platform admin can raise or lower it). Larger lists aren't supported here — that's a separate bulk-campaign feature. |
Subject | string | Yes | |
TextBody | string | No* | *At least one of TextBody/HtmlBody should be set, or the message has no content. |
HtmlBody | string | No* | |
Attachments | array | No | Up to 10 files, inline images included. Each item is { FileName, ContentType, ContentBase64 } — base64-encode the raw file bytes yourself — plus the optional ContentId and Disposition for inline images. Combined message size (attachments included, after base64 encoding) must stay under 10 MB total. |
Response
Always HTTP 200 if your request was well-formed — check the status field for the actual outcome:
| status | Meaning |
|---|---|
Sent | Delivered to SES — sesMessageId is set. |
PendingReview | Your very first send is held for a one-time manual review before it goes out. Every send after that first approval flows automatically. |
Rejected | Blocked — suspended account, daily cap reached, or every recipient is on your suppression list (after a hard bounce or complaint). See message for why. |
Failed | Accepted the request but SES itself couldn't deliver it. See message. Any credit charged for it is refunded. |
HeldForCredit | Your prepaid credit doesn't cover it. The message is held for up to 7 days and goes out automatically once you add credit; after that it expires. |
A non-200 response (401/403/400) means the request itself was rejected — bad/missing API
key, or From doesn't match your key's authorized domain. The body of those
responses is the reason as plain text, except malformed JSON, which answers
{"error":"invalid_json"}.
Examples
curl
curl -X POST "https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send" \
-H "Authorization: Bearer mw_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"From": "noreply@yourdomain.com",
"To": ["customer@example.com"],
"Subject": "Welcome!",
"TextBody": "Thanks for signing up."
}'
Node.js (fetch)
const response = await fetch("https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send", {
method: "POST",
headers: {
Authorization: "Bearer mw_live_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
From: "noreply@yourdomain.com",
To: ["customer@example.com"],
Subject: "Welcome!",
TextBody: "Thanks for signing up.",
}),
});
const result = await response.json();
console.log(result.status); // "Sent" | "PendingReview" | "Rejected" | "Failed" | "HeldForCredit"
Python (requests)
import requests
response = requests.post(
"https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send",
headers={"Authorization": "Bearer mw_live_your_key_here"},
json={
"From": "noreply@yourdomain.com",
"To": ["customer@example.com"],
"Subject": "Welcome!",
"TextBody": "Thanks for signing up.",
},
)
print(response.json()["status"])
C# (HttpClient)
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new("Bearer", "mw_live_your_key_here");
var response = await client.PostAsJsonAsync("https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send", new
{
From = "noreply@yourdomain.com",
To = new[] { "customer@example.com" },
Subject = "Welcome!",
TextBody = "Thanks for signing up.",
});
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine(result.GetProperty("status"));
Attachments (multiple files)
Attach as many files as you need in one call (up to 10) by adding more entries to the
Attachments array — each one is base64-encoded independently.
curl
INVOICE_B64=$(base64 -w0 invoice.pdf)
RECEIPT_B64=$(base64 -w0 receipt.png)
curl -X POST "https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send" \
-H "Authorization: Bearer mw_live_your_key_here" \
-H "Content-Type: application/json" \
-d "{
\"From\": \"noreply@yourdomain.com\",
\"To\": [\"customer@example.com\"],
\"Subject\": \"Your invoice and receipt\",
\"TextBody\": \"Both documents are attached.\",
\"Attachments\": [
{\"FileName\": \"invoice.pdf\", \"ContentType\": \"application/pdf\", \"ContentBase64\": \"$INVOICE_B64\"},
{\"FileName\": \"receipt.png\", \"ContentType\": \"image/png\", \"ContentBase64\": \"$RECEIPT_B64\"}
]
}"
Node.js (fetch)
import { readFileSync } from "fs";
const attachments = [
{ path: "invoice.pdf", contentType: "application/pdf" },
{ path: "receipt.png", contentType: "image/png" },
].map((file) => ({
FileName: file.path,
ContentType: file.contentType,
ContentBase64: readFileSync(file.path).toString("base64"),
}));
const response = await fetch("https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send", {
method: "POST",
headers: {
Authorization: "Bearer mw_live_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
From: "noreply@yourdomain.com",
To: ["customer@example.com"],
Subject: "Your invoice and receipt",
TextBody: "Both documents are attached.",
Attachments: attachments,
}),
});
console.log((await response.json()).status);
Python (requests)
import base64
import requests
def as_attachment(path, content_type):
with open(path, "rb") as f:
return {
"FileName": path,
"ContentType": content_type,
"ContentBase64": base64.b64encode(f.read()).decode("ascii"),
}
response = requests.post(
"https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send",
headers={"Authorization": "Bearer mw_live_your_key_here"},
json={
"From": "noreply@yourdomain.com",
"To": ["customer@example.com"],
"Subject": "Your invoice and receipt",
"TextBody": "Both documents are attached.",
"Attachments": [
as_attachment("invoice.pdf", "application/pdf"),
as_attachment("receipt.png", "image/png"),
],
},
)
print(response.json()["status"])
Inline images (Content-ID)
To show an image inside the email body — a logo, a chart — instead of as a separate file,
give its attachment a ContentId and refer to it from HtmlBody with a
cid: URL: <img src="cid:logo">. MailWarren sends it as an inline
part (multipart/related, Content-Disposition: inline,
Content-ID: <logo>) and leaves your HTML exactly as you wrote it, so the
recipient's mail client puts the image where the cid: reference is. It's the same
idea as SendGrid's content_id, Postmark's ContentID and Mailgun's
inline attachments.
| Field | Type | Notes |
|---|---|---|
ContentId | string | The id your HTML uses after cid:. logo, <logo> and cid:logo all mean the same thing. Printable ASCII with no spaces, quotes or angle brackets, up to 250 characters, and unique within the message (case doesn't count, so Logo and logo clash). logo@yourdomain.com style ids work too. |
Disposition | string | inline or attachment. If you leave it out, it's inline when ContentId is set and attachment otherwise — so for most inline images, ContentId is all you need. A regular attachment can have a ContentId too. |
These requests are refused with a 400 (nothing is sent, queued or charged): an inline
item with no ContentId; an inline item when there's no HtmlBody; the
same ContentId twice; a Disposition other than inline or
attachment; a ContentId with characters that aren't allowed. A
cid: reference in the HTML with no matching attachment is not an error — it
just shows as a broken image, same as in any other mail client, so check your HTML in the
sandbox first.
curl
LOGO_B64=$(base64 -w0 logo.png)
curl -X POST "https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send" \
-H "Authorization: Bearer mw_live_your_key_here" \
-H "Content-Type: application/json" \
-d "{
\"From\": \"noreply@yourdomain.com\",
\"To\": [\"customer@example.com\"],
\"Subject\": \"Your receipt\",
\"HtmlBody\": \"<img src='cid:logo' alt='Acme' width='120'><p>Thanks for your order.</p>\",
\"TextBody\": \"Thanks for your order.\",
\"Attachments\": [
{\"FileName\": \"logo.png\", \"ContentType\": \"image/png\", \"ContentBase64\": \"$LOGO_B64\", \"ContentId\": \"logo\"}
]
}"
Node.js (fetch)
import { readFileSync } from "fs";
const response = await fetch("https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send", {
method: "POST",
headers: {
Authorization: "Bearer mw_live_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
From: "noreply@yourdomain.com",
To: ["customer@example.com"],
Subject: "Your receipt",
HtmlBody: '<img src="cid:logo" alt="Acme" width="120"><p>Thanks for your order.</p>',
TextBody: "Thanks for your order.",
Attachments: [
{
FileName: "logo.png",
ContentType: "image/png",
ContentBase64: readFileSync("logo.png").toString("base64"),
ContentId: "logo", // Disposition defaults to "inline" when ContentId is set
},
{
FileName: "receipt.pdf",
ContentType: "application/pdf",
ContentBase64: readFileSync("receipt.pdf").toString("base64"),
},
],
}),
});
console.log((await response.json()).status);
Python (requests)
import base64
import requests
with open("logo.png", "rb") as f:
logo_b64 = base64.b64encode(f.read()).decode("ascii")
response = requests.post(
"https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/send",
headers={"Authorization": "Bearer mw_live_your_key_here"},
json={
"From": "noreply@yourdomain.com",
"To": ["customer@example.com"],
"Subject": "Your receipt",
"HtmlBody": '<img src="cid:logo" alt="Acme" width="120"><p>Thanks for your order.</p>',
"TextBody": "Thanks for your order.",
"Attachments": [
{
"FileName": "logo.png",
"ContentType": "image/png",
"ContentBase64": logo_b64,
"ContentId": "logo",
"Disposition": "inline",
},
],
},
)
print(response.json()["status"])
Inline images count toward the 10-attachment limit and the 10 MB message size, and some
clients (Outlook especially) also list them as attachments. Always give the image an
alt text: many readers see the email before, or without, its images.
Sandbox testing
Want to test your integration against MailWarren itself before you have a verified sending
domain — or without risking a real send while you're still developing? Point the exact same
request body at /sandbox/send instead, using a Sandbox API Key from one of your
Warren mailboxes rather than a Send key. There's no domain check, no
daily cap, no review queue, and nothing ever reaches a real inbox — the message is captured
straight into that mailbox, viewable the same way any SMTP-captured message is.
| Real Send API | Sandbox Send API | |
|---|---|---|
| Endpoint | /v1/send | /v1/sandbox/send |
| Auth | Send key (mw_live_...) | Sandbox API key (mw_sandbox_...), created per-mailbox on that mailbox's Credentials panel |
| Where a message goes | Real delivery via SES | Captured into the one mailbox the key belongs to |
From / To | From must match your verified domain | Anything — stored for display only, never validated |
| Daily cap / review gate | Applies | None |
| Response | Sent / PendingReview / Rejected / Failed | Always Sent on success — same field, same value, so identical response-handling code works against either endpoint |
curl
curl -X POST "https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/sandbox/send" \
-H "Authorization: Bearer mw_sandbox_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"From": "noreply@yourdomain.com",
"To": ["customer@example.com"],
"Subject": "Welcome!",
"TextBody": "Thanks for signing up."
}'
Node.js (fetch)
const response = await fetch("https://api.mailwarren.io/gateway/5d1da8dd-9504-4297-9bd3-8dfc3fc90faf/Prod/mailwarren/v1/sandbox/send", {
method: "POST",
headers: {
Authorization: "Bearer mw_sandbox_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({
From: "noreply@yourdomain.com",
To: ["customer@example.com"],
Subject: "Welcome!",
TextBody: "Thanks for signing up.",
}),
});
const result = await response.json();
console.log(result.status); // "Sent" — same value the real Send API returns on success
console.log(result.sesMessageId); // a real id, just not one SES actually issued
Same Attachments array shape as the real Send API — see the attachments
examples above, just swap the URL and key. That includes
inline images: ContentId and
Disposition are checked by exactly the same rules, the captured message keeps
them, and the mailbox's message viewer shows each inline image where its cid:
reference is. The inbox API returns them too (contentId and
disposition on each attachment).
SMTP relay
Prefer SMTP? Point your application's outgoing mail at MailWarren's relay instead — same key, same behavior (review gate, daily cap, recipient cap, attachments) as the HTTP API above, just a different protocol. Good for anything that already knows how to send SMTP (most frameworks, CMSes, and mail libraries do) and doesn't need custom code for the API.
| Setting | Value |
|---|---|
| Host | send.smtp.mailwarren.io |
| Port | 587 (STARTTLS) |
| Username | Anything — not checked, put whatever your mail client requires (e.g. your sending domain) |
| Password | Your Send key (the same one used for the HTTP API) |
Your From address, recipients, subject, body, and any attachments come from
the message itself — send a normal email exactly as your framework/library already
composes one. The one rule that still applies: From must be on the domain
your key was created for.
Inline images come through too: a part with a Content-ID (Nodemailer's
cid option, LinkedResource in .NET, add_related in
Python's email package) is delivered inline, so <img src="cid:..."> in your
HTML keeps working. An inline part MailWarren couldn't place (no Content-ID, a repeated
Content-ID, or no HTML body) is delivered as a regular attachment instead.
Node.js (Nodemailer)
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: "send.smtp.mailwarren.io",
port: 587,
secure: false, // STARTTLS, not implicit TLS
auth: { user: "yourdomain.com", pass: "mw_live_your_key_here" },
});
await transporter.sendMail({
from: "noreply@yourdomain.com",
to: "customer@example.com",
subject: "Welcome!",
text: "Thanks for signing up.",
});
Python (smtplib)
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "noreply@yourdomain.com"
msg["To"] = "customer@example.com"
msg["Subject"] = "Welcome!"
msg.set_content("Thanks for signing up.")
with smtplib.SMTP("send.smtp.mailwarren.io", 587) as server:
server.starttls()
server.login("yourdomain.com", "mw_live_your_key_here")
server.send_message(msg)
C# (MailKit)
using MimeKit;
using MailKit.Net.Smtp;
using MailKit.Security;
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse("noreply@yourdomain.com"));
message.To.Add(MailboxAddress.Parse("customer@example.com"));
message.Subject = "Welcome!";
message.Body = new TextPart("plain") { Text = "Thanks for signing up." };
using var client = new SmtpClient();
await client.ConnectAsync("send.smtp.mailwarren.io", 587, SecureSocketOptions.StartTls);
await client.AuthenticateAsync("yourdomain.com", "mw_live_your_key_here");
await client.SendAsync(message);
await client.DisconnectAsync(true);
Good to know
- Your first send ever is held for review — a platform admin approves it once, then sending flows automatically from then on. Applies the same way whether you send via API or SMTP.
- Each account starts at 200 sends/day and 50 recipients/message, ramping the daily cap up automatically as your sending stays clean; a platform admin can raise or lower either manually if you need different limits sooner.
- Keep your key secret — anyone with it can send as your verified domain, via API or SMTP. Revoke and recreate it on the Sending Domains page if it ever leaks.
For AI agents and tools
Everything on this page is also available in machine-readable form, so an AI agent or an API client can work out how to use MailWarren on its own:
/openapi.json— an OpenAPI 3.1 description of the Send, Sandbox and Inbox APIs./llms-full.txt— the whole API reference as one plain-text Markdown page./llms.txt— a short index of the docs (the llms.txt convention).- Reading captured email, the SDKs and the MCP server for agents: Test automation & AI agents.