Sending
This page covers transactional email — one message to a recipient, plus optional cc/bcc copies, triggered by your code: receipts, resets, notifications. To send one message to an audience, see Campaigns. Both send from a verified domain and feed the same event tracking and webhooks.
Transactional email
POST /outbound-emails queues a single email for delivery:
| Attribute | Required | Notes |
|---|---|---|
domain |
yes | A verified domain you own |
from |
yes | Sender address on that domain |
to |
yes | One recipient address |
cc |
no | Carbon-copy recipients — one address or an array of up to 10. See Cc and bcc |
bcc |
no | Blind-carbon-copy recipients — one address or an array of up to 10. See Cc and bcc |
reply_to |
no | Where replies land — one address or an array of up to 10, on your sending domain's family. See Reply-to |
subject |
yes | Up to 998 characters |
html |
yes | HTML body |
text |
no | Plain-text alternative — include one; it helps spam filtering and accessibility |
idempotency_key |
no | Deduplicates retries (see below) |
agent_id |
no | Attributes the send to an AI agent for per-agent reputation (up to 128 chars, [A-Za-z0-9_-.]) |
attachments |
no | Files to attach — see Attachments and inline images |
The response is a 201 with the email's identifier and current status. Track it afterwards with GET /outbound-emails/{outboundEmail} or aggregate numbers from GET /outbound-emails/{outboundEmail}/stats.
Cc and bcc
Add cc and/or bcc to a transactional send — each accepts a single address or an array of up to 10 addresses:
ccrecipients receive the message and are visible to everyone on it.bccrecipients receive the message but never appear in its headers — other recipients cannot see them.
A few things happen for you automatically:
- Suppression covers everyone. Every address —
to,cc, andbcc— is checked against your domain'sall-scope suppressions (bounces, complaints, manual entries — marketing unsubscribes never block transactional mail); a suppressed address rejects the whole send with a422naming the field, so nothing goes out partially. - Every recipient counts. Each
cc/bccaddress counts as one email toward your plan's volume and the free tier's quota, exactly liketo. - Duplicates are dropped. An address repeated within a list, or already present in
to(or incc, forbcc), is silently de-duplicated. - Status follows
to. The email'sstatustracks the primary recipient's journey. A bounce or complaint from acc/bccaddress is recorded as an event, fires its webhook, and suppresses that address — without flipping the send's status.
Reply-to
Set reply_to to steer replies somewhere other than your from address — a single address or an array of up to 10.
Reply-to addresses must stay in your sending domain's family: the sending domain itself, one of its subdomains, or its parent (sending from mail.acme.com? support@acme.com works). An address outside that family is rejected with a 422 — replies to mail you send should land with you, never with a third party.
Reply-to addresses are not recipients: they receive nothing at send time, so they are never suppression-checked and don't count toward your quota.
Attachments and inline images
Add an attachments array to a transactional send. Each entry needs a filename and exactly one source:
- A local file — set
contentto the file's Base64-encoded bytes. - A remote file — set
pathto an HTTPS URL and we fetch it for you.
Optional per attachment: content_type (inferred from the filename when omitted) and content_id (see inline images below).
curl https://api.minimailer.app/outbound-emails \ -X POST \ -H "Authorization: Bearer $MINIMAILER_TOKEN" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -d '{ "data": { "type": "outbound-emails", "attributes": { "from": "billing@acme.com", "to": "customer@example.com", "subject": "Your invoice", "html": "<p>Invoice attached.</p>", "attachments": [ { "filename": "invoice.pdf", "content": "JVBERi0xLjcK..." }, { "filename": "terms.pdf", "path": "https://cdn.acme.com/terms.pdf" } ] }, "relationships": { "domain": { "data": { "type": "domains", "id": "{domain}" } } } } }'
Limits. The whole message, including attachments after encoding, must stay under 40 MB. Executable and script file types (.exe, .js, .bat, and similar) are rejected for the recipient's safety. A remote path must be a public HTTPS URL.
Inline images. To embed an image in the body instead of appending it as a download, give the attachment a content_id and reference it from your HTML with cid::
"attachments": [ { "filename": "logo.png", "content": "iVBORw0KGgo...", "content_id": "logo" } ]
<img src="cid:logo" alt="Acme" />
Attachment metadata (filename, content type, size, and whether it is inline) is returned on the email resource; the file bytes themselves are never stored or returned.
Idempotency
Network timeouts make "did that send go through?" a real question. Pass an idempotency_key (any string up to 255 characters — an order ID, a job UUID) and retries of the same logical send become safe: the same key never produces a duplicate email.
Suppression checks happen for you
Every send is checked against your domain's suppression list before it goes out — every recipient on it, to, cc, and bcc alike. A recipient who hard-bounced or marked your mail as spam is rejected with a 422 instead of being delivered. This is not optional, and that's deliberate: repeatedly mailing a dead or unwilling address is the fastest way to destroy deliverability. Marketing unsubscribes are different — they block your campaigns, not your transactional mail, so an opted-out customer still gets their receipts and password resets. How suppression works →
Open and click tracking
Tracking is off by default and opt-in at two levels:
- Domain level — enable open and/or click tracking on the domain. This is the capability switch.
- Per send —
track_opens/track_clickson an individual email or campaign can narrow within what the domain allows (e.g. domain has tracking on, one send opts out). A per-send flag can't enable tracking the domain has off.
Opens are detected with a tracking pixel, clicks by wrapping links through a redirect. Both feed the email.opened / email.clicked webhooks and the stats endpoints. The default-off posture is a compliance choice: enable tracking when you have a lawful basis for it. EU data residency and privacy →
Rate limits and quotas
API rate limits scale with your subscription tier, applied per account across the whole API (including MCP):
| Tier | Requests per minute |
|---|---|
| Spark (free) | 60 |
| Starter | 300 |
| Growth | 900 |
| Scale | 3,000 |
Hitting a limit returns 429 — back off and retry. Separately from rate limits, the free tier enforces its sending quota (5,000 emails/month, 1,000/day); a send over quota is rejected with an explanatory error rather than silently queued. Each recipient counts toward quota — a send with to plus three cc addresses consumes four emails, and a send that doesn't fully fit in the remaining quota is rejected whole rather than partially delivered.
Delivery lifecycle
After acceptance, an email moves through delivery events — sent, delivered, bounced, complained, opened, clicked. You can poll the stats endpoints, but the intended integration is webhooks: every lifecycle event pushes to your endpoint as it happens.