API reference
Send email
Queue a single email for delivery.
Why use this
For example
A customer checks out in your shop. Your server makes one call to send, and they get their order confirmation — from your own address, tracked end to end.
Example
import { YourMail } from "yourmail";
const yourmail = new YourMail(process.env.YOURMAIL_API_KEY!);
const { id } = await yourmail.send({
from: "hello@mail.acme.com",
to: ["alice@example.com"],
cc: ["bob@example.com"],
bcc: ["archive@acme.com"],
replyTo: "support@acme.com",
subject: "Welcome to Acme",
html: "<h1>Hello Alice</h1>",
text: "Hello Alice",
attachments: [
{
filename: "invoice.pdf",
content: "<base64-encoded content>",
contentType: "application/pdf",
},
],
tags: [{ name: "category", value: "welcome" }],
idempotencyKey: "welcome-usr_123",
headers: {
"List-Unsubscribe": "<mailto:unsub@acme.com>",
"X-Campaign-ID": "launch-2026",
},
});
console.log(id);const res = await fetch(
"https://api.yourmail.dev/v1/emails",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "hello@mail.acme.com",
to: ["alice@example.com"],
cc: ["bob@example.com"],
bcc: ["archive@acme.com"],
replyTo: "support@acme.com",
subject: "Welcome to Acme",
html: "<h1>Hello Alice</h1>",
text: "Hello Alice",
attachments: [
{
filename: "invoice.pdf",
content: "<base64-encoded content>",
contentType: "application/pdf",
},
],
tags: [{ name: "category", value: "welcome" }],
idempotencyKey: "welcome-usr_123",
headers: {
"List-Unsubscribe": "<mailto:unsub@acme.com>",
"X-Campaign-ID": "launch-2026",
},
}),
}
);
const { id } = await res.json();curl -X POST https://api.yourmail.dev/v1/emails \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "hello@mail.acme.com",
"to": ["alice@example.com"],
"subject": "Welcome to Acme",
"html": "<h1>Hello Alice</h1>"
}'Request body
All fields are JSON. from, to, subject, and at least one of html / text are required.
fromstring- RequiredA bare address (hello@mail.acme.com) or one with a display name (Acme Support <hello@mail.acme.com>) — the display name is what the recipient's inbox shows. Names containing a comma or dot are quoted for you. Must be on a verified domain you own; verification is domain-level, so once mail.acme.com is verified every address on it sends — support@, hello@, no-reply@ — with no per-address setup anywhere. The one exception is no-reply@yourmail.dev, the shared sandbox sender, which puts the send in test mode instead. Ownership is checked against the address, never the display name.
tostring | string[]- RequiredOne or more recipient addresses, each with an optional display name. Combined with cc + bcc: max 50 total. In test mode (from: no-reply@yourmail.dev) any recipient other than your signup email or an SES simulator address is silently rewritten to your signup inbox (cc/bcc dropped), not rejected — you may omit to entirely and it defaults to your own inbox.
ccstring | string[]- OptionalCarbon-copy recipients, display names allowed. Counts towards the 50-recipient cap.
bccstring | string[]- OptionalBlind-copy recipients, display names allowed. Counts towards the 50-recipient cap.
replyTostring- OptionalReply-To address, display name allowed. Must be a valid email. Worth setting on anything a human might answer: a from address is not a mailbox. Verifying a domain authorises sending from it, it does not create anywhere to receive — that needs MX records. If your domain has none (check with dig MX acme.com), every reply to your from address bounces, and the bounce goes to the recipient rather than to you, so you never find out. Point this at an inbox someone reads.
subjectstring- RequiredNon-empty string.
htmlstring- ConditionalHTML body. At least one of html or text is required. Combined html + text must be under 1 MB (UTF-8 bytes).
textstring- ConditionalPlain-text body. At least one of html or text is required.
attachmentsAttachment[]- OptionalMax 20 files. Each file: max 10 MB decoded. Total: max 25 MB decoded. Each item needs filename (string) and content (base64 string); contentType defaults to application/octet-stream.
tagsTag[]- OptionalMax 10 tags. Each tag is { name: string; value: string }. Used for filtering and analytics.
idempotencyKeystring- OptionalIf a send with this key was already queued, the original id is returned without re-sending. Safe to retry on network failures.
scheduledAtstring | number- OptionalHold the message until this time (ISO 8601 or Unix ms), up to 30 days ahead. A time at or before now sends immediately rather than erroring; Unix seconds are rejected with a 400, because read as ms they land in 1970 and would send at once. The message reads “scheduled” until it dispatches and can be cancelled with DELETE /v1/emails/:id until then — quota is charged at dispatch, so cancelling costs nothing. Outstanding scheduled sends are capped at your plan’s monthly allowance.
headersobject- OptionalCustom mail headers as a string→string object. Max 10 headers; each name max 64 chars; each value max 1 KB; combined total max 4 KB. Reserved headers (From, To, Subject, Content-Type, DKIM-Signature, etc.) are rejected.
bulkboolean- OptionalMarks the send as bulk (newsletters, announcements) rather than transactional. Requires exactly one `to` recipient and no cc/bcc — anything else is a 400. On a live send we inject one-click List-Unsubscribe headers so mailbox providers show an unsubscribe button, unless you set those headers yourself (test-mode sends go to your own inbox, so they get none). Recipients who unsubscribe are blocked from bulk mail only; your transactional email still reaches them. Defaults to false.
Response
On success the API returns HTTP 200 with the new email ID.
// 200 OK
{ "id": "jn7abc123def456" }Errors
All errors follow the shape { error: { type, message, docs_url } }, where docs_url links straight to that type's entry. See Errors for the full reference. Common errors for this endpoint:
validation_errorMissing or invalid field (from, to, subject, html/text)
authentication_errorAPI key missing, malformed, or not recognised
domain_errorSending domain not verified or not owned by this account
account_pending_reviewAccount not yet approved — see below
validation_errorBody exceeds 1 MB, or attachment exceeds 10 MB / 25 MB total
suppressed_recipientRecipient is on this account's suppression list
sandbox_recipientTest mode only, and rare: a disallowed recipient is normally rewritten to your signup inbox rather than rejected — this fires only when your account has no signup email on file to rewrite to
rate_limitedPer-second burst exceeded — Retry-After header tells you when to retry
quota_exceededDaily or monthly send quota exhausted
New accounts are reviewed before they can email the outside world. Until a human approves your account, a send may only reach an address at a domain you have verified, your own account email, or an AWS SES simulator address — anyone else gets 403 account_pending_review. This is not a bug and not rate limiting, so don't retry it: keep integrating and testing against your own verified domain, which exercises the identical send path, while review usually finishes within a few hours. See account_pending_review for the full reference, or email support@yourmail.dev if it's blocking a launch.