Emails
POST /api/v1/emails
Send a single email. Source: src/app/api/v1/emails/route.ts.
Request
{
"from": "sender@yourdomain.com",
"to": ["recipient@example.com"],
"subject": "Hello",
"html": "<p>Hi</p>",
"text": "Hi",
"tags": ["welcome"],
"metadata": { "campaign": "launch" },
"scheduled_at": "2026-05-01T12:00:00Z",
"attachments": [
{ "filename": "invoice.pdf", "content": "<base64>", "content_type": "application/pdf" }
],
"template": "welcome-email",
"variables": { "name": "Fareed" }
}
Validation (sendEmailSchema):
| Field | Type | Rules |
|---|---|---|
from |
string (email) | required |
to |
string[] (email) | 1–50 items |
subject |
string | required unless template supplies it |
html |
string | optional |
text |
string | optional |
tags |
string[] | max 10 |
metadata |
Record<string,string> | optional |
scheduled_at |
ISO 8601 UTC | if in future (>1s), message stored with status: "scheduled" |
attachments |
object[] | max 10 — { filename, content (base64), content_type } |
template |
string | slug of a template — subject/html/text derived from it when omitted |
variables |
object | string/number/boolean values, substituted into {{key}} |
Headers
Authorization: Bearer <key>— requiredIdempotency-Key: <uuid>— optional, 24h replay window
Response — 200
Immediate send:
{ "id": "msg_...", "channel": "email", "status": "sent", "created_at": "..." }
Scheduled:
{
"id": "msg_...",
"channel": "email",
"status": "scheduled",
"scheduled_at": "2026-05-01T12:00:00.000Z",
"created_at": "..."
}
Errors
401 UNAUTHORIZED— missing/invalid key or expired key422 VALIDATION_ERROR— zod failure, missing subject with no template, unknown template429 USAGE_LIMIT_EXCEEDED— free plan cap hit429 RATE_LIMITED— per-plan burst exceeded500 INTERNAL_ERROR— SES failed (message inserted withstatus: "failed")
POST /api/v1/emails/batch
Scope: send:email. maxDuration pinned to 300s. Accepts two shapes:
Shape 1 — per-item array (1–100 items)
Each item is one envelope. Item's to array becomes the visible TO header
so multi-recipient emails work (e.g. to: ["a@", "b@"] lands as
To: a@, b@ — both recipients see each other).
[
{ "from": "hello@yourdomain.com", "to": ["a@example.com"], "subject": "Personalized 1", "html": "<p>hi 1</p>" },
{ "from": "hello@yourdomain.com", "to": ["b@example.com"], "subject": "Personalized 2", "html": "<p>hi 2</p>" }
]
Shape 2 — bulk broadcast (one body to N recipients)
Each to entry becomes its own envelope — recipients don't see each
other, each gets their own message id, tracking pixel, and unsubscribe
header. Up to 100 recipients per call.
{
"from": "hello@yourdomain.com",
"to": ["a@example.com", "b@example.com", "c@example.com"],
"subject": "Newsletter",
"html": "<h1>This month's update</h1>",
"reply_to": ["support@yourdomain.com"],
"tags": ["newsletter"],
"metadata": { "campaign": "may_2026" },
"attachments": [
{ "filename": "may.pdf", "url": "https://example.com/may.pdf", "content_type": "application/pdf" }
]
}
Mirrors SendGrid Personalizations / Resend's bulk send. Internally fans out to per-recipient items; the gates and response shape are identical to Shape 1.
Response (both shapes)
{
"data": [
{ "index": 0, "id": "msg_...", "status": "sent", "error": null },
{ "index": 1, "id": null, "status": "failed", "error": { "code": "SUPPRESSED", "message": "..." } }
],
"total": 2,
"succeeded": 1,
"failed": 1
}
- Usage incremented per successful item.
- Plan quota enforced upfront — batches exceeding remaining quota are rejected (429) before any send.
- Suppressed recipients fail per-item with
SUPPRESSED; rest of the batch proceeds. - Idempotency-Key not honored on batch — use single-send
/v1/emailswithIdempotency-Keyfor at-most-once. template+scheduled_atnot supported on either shape — use single-send.
GET /api/v1/emails
List emails for the auth'd org + environment.
Query params
| Param | Default | Notes |
|---|---|---|
limit |
20 | max 100 |
cursor |
— | base64url of `{ISO} |
status |
— | sent / delivered / bounced / failed / scheduled |
tag |
— | messages carrying this tag |
to |
— | exact recipient address match |
created_after |
— | ISO 8601; inclusive lower bound on created_at |
created_before |
— | ISO 8601; inclusive upper bound on created_at |
Response
{
"data": [{
"id": "msg_...",
"channel": "email",
"status": "delivered",
"from": "a@x.com",
"to": "b@y.com",
"subject": "...",
"created_at": "...",
"sent_at": "...",
"delivered_at": "..."
}],
"has_more": true,
"next_cursor": "eyIyMDI2..."
}
Pass next_cursor as ?cursor= on the next request.
GET /api/v1/emails/{id}
Retrieve one email by ID. 404 if not owned by caller's org.