Send an email
Send email from your own domain in one API call — text, HTML, React, or a stored template, plus attachments, scheduling, and safe retries.
Sending an email is one call. Pick the inbox it comes from, name the recipients, give it a body — Dairo authenticates the mail, delivers it, and records what happens next. Everything else on this page is optional.
The basic send
Call POST /v1/messages (scope messages:send) with an inboxId — the verified
inbox the mail comes from — at least one recipient in to, and one body. That is
a complete send.
An immediate send is performed synchronously, so the response is the definitive outcome — you never get an acceptance that fails later on something knowable at submit time.
Keep the id — it keys delivery tracking and the webhook events Dairo emits as
the mail progresses.
status | When you get it |
|---|---|
sent | The email left Dairo. This is the normal result of an immediate send. |
scheduled | The request carried a future sendAt; scheduledAt echoes the fire time. |
queued | Returned only by audience broadcasts, which fan out in the background. |
failed | The send could not be completed; error carries the reason. |
Dairo picks the transport automatically: external addresses go out as email, and
when every recipient is another Dairo inbox the message is delivered directly
with channel: "a2a" — still returning sent. Pass channel to force one or
the other; see Channels.
Provide exactly one body — text, html, react, or template. Two body
fields in one request (even text plus html) is a 400, and so is a send with
no body at all unless it carries at least one attachment (an attachment-only
email is valid). A text or html body may be up to 1 MB.
Request fields
| Field | Description |
|---|---|
inboxId | Required. The verified inbox to send from. |
to | Required. One address or an array. A contact reference works anywhere an address does. |
cc, bcc | More recipients, single or array. |
subject | Subject line. Defaults to empty. |
text / html / react / template | The body — exactly one. |
contactId | Send to a stored contact: its primary handle is added to to, so no raw address is needed. |
attachments | Up to 10 files, inline or by storage reference. |
replyTo | A single address, set verbatim as the Reply-To header. |
replyToMessageId | The Dairo message this send answers: sets the reply headers and files the send in that message's thread. See Reply in a thread. |
inReplyTo, references | Raw reply headers (RFC 5322 Message-IDs) for answering mail that is not in Dairo. |
headers | Up to 25 custom MIME headers as { "name": "value" }. Protected header names, malformed names, and values containing CR or LF are rejected. |
tags | Up to 25 name/value pairs recorded on the send. Names and values are 1–256 characters of ASCII letters, digits, _, and -; names starting with dairo_ are reserved for Dairo and refused with a 400. |
idempotencyKey | De-duplicates retries — up to 128 characters. The Idempotency-Key header does the same. |
sendAt | A future time, up to 30 days ahead, to schedule delivery. |
channel | email or a2a; omit to route automatically. a2a requires every recipient to be a Dairo inbox and cannot combine with sendAt. |
ignoreComplaints | Deliberately email suppressed recipients (spam complaints and hard bounces). Default false. |
dryRun | Validate the send without performing it. Returns a preview; nothing is created, sent, or billed. Default false. |
A send takes at most 50 recipients across to, cc, and bcc combined.
Multiple recipients, CC and BCC
to, cc, and bcc each accept a single address or an array.
Send to a contact
You don't have to know a recipient's raw address. If you keep an address book of contacts, reference one and Dairo resolves it against the sending inbox's channel — for an email inbox, to the contact's primary email address. Raw addresses in the same list pass through untouched, so you can mix them freely.
| Reference | Resolves to |
|---|---|
@me | The contact bound to your own account. |
@alias | The contact with that alias. |
contact:<id> | The contact with that id. |
contactId (top-level field) | The contact's primary handle, added to to. |
A contact with no handle for the sending inbox's channel is a 422 — resolution
never guesses an address. The resolved contact is stamped on the outbound
message, so the send joins that contact's cross-channel history.
Send a React email
Pass a react object and Dairo renders your
React Email component into HTML — no build step or render
service on your side. Send the component source plus any props; imports are
limited to React Email components. source may be up to 64 KiB and props up
to 32 KiB of JSON.
To reuse a design across many sends, save it once as a
template and send it by id instead.
Attach files
Each attachment is an object, and you attach it one of two ways — never both:
- Inline bytes — base64-encode the file into
contentBase64. Requires afilename;contentTypeis recommended. Best for small files. - Storage reference — pass the
objectIdof a Dairo storage object you own (the id a bucket upload returns on finalize). Dairo fetches the bytes and attaches them natively, derivingfilenameandcontentTypefrom the object; override either in the same attachment object.
Limits: up to 10 attachments per send. Inline bytes are capped at
8 MiB per file and 8 MiB total across all inline attachments — they ride the
API request itself. A storage reference attaches up to 24 MiB. Over these
limits the send returns 413; Dairo never silently drops an attachment or edits
your body.
For a file larger than 24 MiB, share a link instead: create a
share link for the stored object and place it in your
text or html yourself. Dairo never rewrites your message or inserts links on
its own — an attachment with delivery: "link" is rejected for exactly this
reason.
Test a send without sending it
Set dryRun: true and Dairo validates the request exactly as it would a real
send — the body rules, the recipients, the sending inbox, complaint suppression,
your attachments — resolves which channel it would go out on, and then stops.
Nothing is written, queued, handed to the provider, metered, or billed, and there is no message id — a preview has no row to fetch and will never emit delivery events. A bad payload still fails the way it normally would: a dry run validates, it does not wave anything through.
dryRun is a per-request control. The dairo_test_ / dairo_live_ prefix
on an API key is a cosmetic label with no effect on delivery — a dairo_test_
key sends real mail to real people and is billed identically. If you want to
send nothing, say so on the request.
Prevent duplicate sends
Pass an idempotencyKey (or the Idempotency-Key header) and a retried request
won't send a duplicate — Dairo returns the result of the original send instead.
Build the key from the thing you're emailing about — an order ID, an event ID, a row's primary key — so the same logical email always carries the same key. See Retries & idempotency.
Schedule for later
Add sendAt to any send and Dairo holds the email until then — useful for
reminders, drip sequences, and time-zoned announcements.
sendAt is an RFC 3339 timestamp with an explicit timezone offset (for example
2026-07-01T09:00:00-04:00, or …Z for UTC — a bare local time is a 400). It
must be in the future, at most 30 days ahead.
The response comes back with status: "scheduled" and the exact scheduledAt
time the email will go out, normalized to UTC.
At fire time Dairo re-checks the send — inbox, domain, complaint suppression,
and quota — and either delivers it or marks it failed. A recipient who
reported spam after you scheduled the email is still protected.
Cancel a scheduled send
While an email is still scheduled, POST /v1/messages/{messageId}/cancel
stops it for good and sets its status to canceled. A send that is no longer
scheduled — already queued, sent, failed, or canceled — returns a 409.
Suppressed recipients
Dairo remembers every recipient who marked your mail as spam and every address
that hard-bounced, and refuses to email them again: a send to such a recipient
fails with a 400 naming the address and the reason. Set
ignoreComplaints: true to override deliberately — the send then proceeds and
the response's warnings array lists each affected recipient, with reason
complaint or bounce:
Emailing people who reported you as spam, or addresses that do not exist,
damages your domain's reputation for every inbox you send from and can push
future mail into spam folders. Override only when you are certain — and note
that the override never applies to scheduled sends: a suppression on file at
fire time marks the send failed. See
Land in the inbox.
List and read your sends
List outbound messages (most recent first) with GET /v1/messages filtered on
direction: "outbound". To follow one send, use the id the send call
returned: fetch it for its current status, or list its per-recipient delivery
events.
Next steps
- Track delivery — statuses, delivery events, bounces, and complaints for every send.
- Webhooks — get delivery events pushed to your app instead of polling.
- Messages & threads — handle the replies that come back.
- Audiences & broadcasts — send one email to a whole list.