Send an 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.
bearerAuthAuthorizationBearer <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.
Idempotency-Key?stringUnique 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?stringIf "true", simulate delivery without sending (see description).
"true""false"application/json- 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*stringSender address; its domain must be a verified sending domain for the tenant (and match the key's domain scope, if scoped).
emailto?array<>[]cc?array<>[]bcc?array<>[]reply_to?|emailsubject?stringRequired by validation unless a template supplies it.
length <= 1000html_body?string|nulltext_body?string|nullmessage_stream?stringPostmark-style stream separation.
"outbound""outbound""broadcast"tag?|Legacy single tag; prefer tags.
tags?|nulltemplate_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.
int64attachments?array<>|items <= 10test_mode?|Simulate delivery without sending (same effect as the X-Relay-Test header).
Suppressed, scheduled, test-mode, or idempotent-replay result (not queued for immediate delivery). See SendResult for the possible shapes.
application/json- 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?stringstatus?string"queued""suppressed""scheduled""sent""failed"ses_message_id?stringPresent only for test-mode sends (a fake placeholder id).
test_mode?booleanscheduled_at?integerPresent when status is scheduled.
int64reason?stringPresent when suppressed via the cross-tenant blocklist: "global_suppression".
idempotent_replay?booleanTrue when this response is a replay of a previous idempotent request.
error?stringStable error code when this item was rejected (e.g. validation_failed, from_domain_not_verified, template_not_found).
detail?stringalias?stringPresent on template_not_found.
from?stringPresent on from_domain_not_verified.
[key: string]?anycurl -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"}