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": "dom_01JZX8K7Q2M3N4P5R6S7T8V9W0" } } } } }'
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.
A further cap applies per agent, on top of the account limit above. An agent still building a record sends at most 5 per second and 5,000 per day; an agent whose sender reputation has degraded into the restricted tier is held to 1 per second and 100 per day. The daily window opens on the agent's first send and runs for 24 hours from there — it is not a calendar day, so the allowance cannot be spent twice by sending either side of midnight. Established agents are bound only by your account's tier. These rejections also return 429, but with error code agent_throttled instead of rate_limited, plus a Retry-After header. Sends that carry no agent_id are never capped this way. How reputation works →
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.