Send an email

POST
/v1/email

Pipeline: (optional) template render → validate → suppression check (global + per-tenant + erasure) → persist → enqueue to sender. edge-api never calls SES directly.

Test mode (test_mode: true or X-Relay-Test: true) simulates delivery: no SES call, no queue write. Recipient local-parts at relay.dev drive the simulated outcome: delivered@relay.dev → sent + delivery event, bounced@relay.dev → failed + bounce event

  • auto-suppression, complained@relay.dev → sent + complaint event
  • auto-suppression. Any other address defaults to delivered.

Authorization

bearerAuth
headerAuthorizationBearer <token>

API key sent as Authorization: Bearer <key>. Looked up by SHA-256 hash (email_core::sha256_hex) against api_keys.hash; revoked keys and keys belonging to a suspended tenant are rejected. See the top-level Authentication section for permission levels.

Header Parameters

Idempotency-Key?string

Unique key for safe retries, scoped to the tenant. A replay (same key) returns the original result without sending or consuming quota again.

X-Relay-Test?string

If "true", simulate delivery without sending (see description).

Value in"true""false"

Request Body

application/json
  1. body

Mirrors email_core::SendRequest. Only from is required for the JSON body to deserialize; validate_send_request additionally requires at least one recipient (across to/cc/bcc), a non-empty subject, and at least one of html_body/text_body — UNLESS template_alias is set, in which case the template supplies subject/body before validation runs.

from*string

Sender address; its domain must be a verified sending domain for the tenant (and match the key's domain scope, if scoped).

Formatemail
to?array<>
Default[]
cc?array<>
Default[]
bcc?array<>
Default[]
reply_to?|
Formatemail
subject?string

Required by validation unless a template supplies it.

Lengthlength <= 1000
html_body?string|null
text_body?string|null
message_stream?string

Postmark-style stream separation.

Default"outbound"
Value in"outbound""broadcast"
tag?|

Legacy single tag; prefer tags.

tags?|null
template_alias?|

Server-side template alias to render (see Templates). Renders subject/html_body/text_body before validation.

template_model?|

Variables for template_alias rendering.

scheduled_at?|

Epoch milliseconds. When in the future, the message is persisted as scheduled and enqueued by a per-minute cron once due.

Formatint64
attachments?array<>|
Itemsitems <= 10
test_mode?|

Simulate delivery without sending (same effect as the X-Relay-Test header).

Response Body

Suppressed, scheduled, test-mode, or idempotent-replay result (not queued for immediate delivery). See SendResult for the possible shapes.

application/json
  1. response

The heterogeneous per-email result shape returned by POST /v1/email (200/202) and as each item of POST /v1/email/batch's data array. Exactly which fields are present depends on status/error.

message_id?string
status?string
Value in"queued""suppressed""scheduled""sent""failed"
ses_message_id?string

Present only for test-mode sends (a fake placeholder id).

test_mode?boolean
scheduled_at?integer

Present when status is scheduled.

Formatint64
reason?string

Present when suppressed via the cross-tenant blocklist: "global_suppression".

idempotent_replay?boolean

True when this response is a replay of a previous idempotent request.

error?string

Stable error code when this item was rejected (e.g. validation_failed, from_domain_not_verified, template_not_found).

detail?string
alias?string

Present on template_not_found.

from?string

Present on from_domain_not_verified.

[key: string]?any
curl -X POST "https://example.com/v1/email" \  -H "Content-Type: application/json" \  -d '{    "from": "hello@example.com",    "to": [      "user@example.com"    ],    "subject": "Welcome to emitd!",    "html_body": "<p>Hello!</p>"  }'
{  "message_id": "string",  "status": "queued",  "ses_message_id": "string",  "test_mode": true,  "scheduled_at": 0,  "reason": "string",  "idempotent_replay": true,  "error": "string",  "detail": "string",  "alias": "string",  "from": "string"}