Emails
Password resets, receipts, magic links. Same verified domain and same monthly allowance as your campaigns: one provider, one reputation.
The request shape is Resend-compatible. Migrating usually means changing the
base URL, the key and from.
The Email object
{
"id": "eml_0dz4k9x2v7q1",
"object": "email",
"from": "Acme <noreply@yourdomain.com>",
"to": ["jane@acme.com"],
"reply_to": null,
"list_unsubscribe": null,
"subject": "Reset your password",
"status": "delivered",
"error": null,
"tags": { "type": "password-reset" },
"sandbox": false,
"created_at": "2026-08-21T09:00:00.000Z",
"sent_at": "2026-08-21T09:00:01.000Z",
"delivered_at": "2026-08-21T09:00:03.000Z",
"bounced_at": null,
"complained_at": null
}Status
queued → sent → delivered, or it ends at bounced, complained,
failed or suppressed.
| Status | Meaning |
|---|---|
queued | Accepted, waiting for the worker. |
sent | Handed to the mail provider. Not yet delivered. |
delivered | The receiving server accepted it. |
bounced | Came back. The address may now be suppressed. |
complained | Marked as spam. The address is suppressed. |
failed | Never left. error says why. |
suppressed | Blocked before sending: the address is unmailable. |
Delivery is asynchronous. Transactional sends are top-priority jobs and never wait behind a campaign, so they usually complete in seconds.
Send an email
/v1/emailscurl -X POST https://go.day3.app/api/v1/emails \
-H "Authorization: Bearer $DAY3_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pwreset-user-8412-1755764400" \
-d '{
"from": "Acme <noreply@yourdomain.com>",
"to": ["jane@acme.com"],
"subject": "Reset your password",
"html": "<p>Click to reset.</p>",
"text": "Click to reset.",
"reply_to": "support@yourdomain.com",
"headers": { "X-Entity-Ref-ID": "pwreset-8412" },
"tags": { "type": "password-reset" }
}'Body
| Field | Type | Required | Notes |
|---|---|---|---|
from | string | Yes | addr or "Name <addr>". Domain must be verified. |
to | string or string[] | Yes | Up to 50 addresses. See Multiple recipients. |
subject | string | Yes | Control characters are rejected. |
html | string | One of | HTML body. |
text | string | One of | Plain-text body. Send both when you can. |
reply_to | string | No | Any address, on any domain. |
headers | object | No | Up to 20 custom headers. Reserved names are rejected. |
tags | object | No | Flat string map for your own filtering. |
Send at least one of html or text. Returns 200 with the Email object,
status: "queued".
Always set Idempotency-Key. A retried password reset that sends twice is a
support ticket. See Idempotency.
from must be on a verified domain
Verify the domain in the app under Sending. Then any local part works with
no sender setup: noreply@, receipts@, billing@.
An unverified domain is 403 domain_not_verified. Verify it; don’t retry.
The day3 test address
Every new organization gets one pre-verified address on a day3-owned domain, so your first send needs no DNS:
acme@sandbox.day3.appThe local part comes from your organization name. Find the exact address in the app under Sending, on the card above the domain list.
A send from the test address must be a sandbox send: your account is in
sandbox mode and every recipient is a member of your organization. It stops
working the moment you upgrade. Anything else is 403 domain_not_verified with
a message saying so, including a send to a teammate on a paid plan.
The address shares a sending reputation with every day3 account, so it only reaches as far as sandbox limits do. The check fails closed: anything it can’t prove is a sandbox send is refused.
- The footer carries day3’s postal address, not yours.
- It never counts as your verified domain in the app.
Use it to get working on day one. Verify your own domain before you upgrade;
the only code change is from.
Multiple recipients
to takes a string or an array of up to 50 addresses. An array is one
message with every address in the To header. Everyone sees everyone. For 50
private emails, send 50 requests.
Addresses are trimmed, lowercased and de-duplicated. Each one left counts against your monthly allowance.
Custom headers
Up to 20. These are rejected with 400:
- Body-derived:
From,To,Subject,Reply-Toand friends. - MIME plumbing.
- Authentication and trace:
DKIM-Signature,Authentication-Results,ARC-*,Received,Sender,Resent-*. - The unsubscribe headers.
- day3’s own
X-Account-IDandX-Transactional-Email-ID. - Anything starting with
X-SES-.
Limits
| Limit | Value |
|---|---|
| Recipients per message | 50 |
| Custom headers | 20 |
html + text + headers + tags | 1.5 MB of UTF-8 |
| Rate | 120 sends per minute per organization |
Control characters are rejected in subject, header values and addresses. A
display name can’t contain <, >, " or \. Both are header-injection
defence.
Retrieve an email
/v1/emails/{email_id}Returns the Email object plus events, the provider’s delivery timeline:
{
"id": "eml_0dz4k9x2v7q1",
"object": "email",
"status": "delivered",
"events": [
{ "type": "sent", "created_at": "2026-08-21T09:00:01.000Z" },
{ "type": "delivery", "created_at": "2026-08-21T09:00:03.000Z" }
]
}Bodies are pruned after 30 days. The metadata row is kept forever, and the API reports the content as expired rather than returning an empty string.
List emails
/v1/emailsNewest first, cursor-paginated. Filter with
?status=queued|sent|delivered|bounced|complained|failed|suppressed.
curl "https://go.day3.app/api/v1/emails?status=bounced&limit=100" \
-H "Authorization: Bearer $DAY3_API_KEY"Suppression
Transactional mail follows different rules from campaigns:
- Unsubscribes don’t block it. Leaving a newsletter must never stop a password reset.
- Hard bounces, complaints and provider suppressions do, with
400 email_suppressed. Don’t retry. It damages your reputation.
Bounces and complaints here feed the same suppression list and the same reputation auto-pause as campaigns. There is no separate reputation.
Sandbox mode
On a free plan, sends are real but only reach members of your organization, on
a small monthly allowance. Responses carry "sandbox": true.
| Situation | Response |
|---|---|
| Recipient outside your organization | 403 sandbox_recipient_not_allowed |
| Allowance exhausted | 403 plan_limit_reached |
Upgrading lifts both with no code change. See Authentication.
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | invalid_request | Missing required field, reserved header, or over a size limit. |
| 400 | invalid_email | An address is not valid. |
| 400 | email_suppressed | The address hard-bounced, complained, or is provider-suppressed. |
| 403 | domain_not_verified | from domain not verified, or the test address on a non-sandbox send. |
| 403 | sandbox_recipient_not_allowed | Free plan, recipient outside the organization. |
| 403 | plan_limit_reached | Monthly send allowance exhausted. |
| 403 | sending_disabled | Sending is off for this account. |
| 429 | rate_limit_exceeded | Over 120 sends per minute. Honour Retry-After. |
Delivery events
Polling GET /emails/{id} works. Webhooks are better: day3 POSTs
email.delivered, email.bounced, email.complained and
suppression.created as they happen. Use suppression.created to keep your
own database honest about dead addresses.