API reference
Every emitd endpoint, generated from the OpenAPI spec.
Every page below is generated from the OpenAPI spec — the single source of truth the SDKs are built from. Pages update automatically whenever the spec changes.
Send transactional/marketing email, manage scheduled sends.
Send an email
Pipeline: (optional) template render → validate → suppression check (global + per-tenant + erasure) → persist → enqueue to `sender`. `edge-api` never…
Send a batch of emails
Send up to 100 emails in one call. Each is validated and processed independently through the same pipeline as `POST /v1/email`; a per-email result is…
Reschedule a queued or scheduled send
Requires `Action::Manage` (a `full_access` key) — not available to a `sending_access` key.
Cancel a queued or scheduled send
Requires `Action::Manage` (a `full_access` key).
List simulated events for test-mode sends
Events generated by `test_mode`/`X-Relay-Test` sends only (joined against `messages.test_mode = 1`). Cursor is the bare epoch-ms `created_at` of the…
Messages
Read-only message activity log.
List message activity
Newest-first, cursor is the bare epoch-ms `created_at` of the last row returned (`?cursor=` returns rows strictly older). Tag filtering uses D1 `json…
Get message detail
Contacts
Marketing contact store (CRUD, batch, CSV import/export).
List contacts
Newest-first keyset page over `(created_at, id)`.
Create or upsert a contact
Upserts by normalized (lowercased/trimmed) email. Enforces the plan's `max_contacts` cap, but only when the email does not already exist — re-identif…
Get a contact
Update a contact
`attributes` is a merge (incoming keys overlay existing; keys not present are kept) — not a replace. `status` changes only affect marketing eligibili…
Delete a contact
Hard delete. Also removes the contact's audience memberships.
Upsert up to 100 contacts
Each item is validated/upserted independently — a bad item is a per-item failure, not a whole-batch rejection. The plan cap is checked once up front…
Start an async CSV contact import
Uploads the CSV to R2 and inserts a `pending` job; a per-minute cron parses and upserts it in bounded batches (`IMPORT_BATCH` = 500 data rows/tick) s…
Get a contact import job's status
Start an async GDPR contact export
Exports the caller's own tenant's contacts as CSV. `Action::Read` (not `Manage`) — exporting your own data is a read. A per-minute cron builds the CS…
Get a contact export job's status
Download a finished contact export
Streams the CSV from R2. Returns 404 (never 403) if the job doesn't exist, isn't owned by the caller's tenant, or isn't `done` yet — an export id nev…
Audiences
Static contact lists and their membership.
List audiences
Newest-first keyset page over `(created_at, id)`.
Create an audience
Creates an empty static contact list.
Get an audience
Rename or redescribe an audience
Fields omitted from the body keep their current value.
Delete an audience
Hard delete, including all membership rows.
List an audience's members
Oldest-first keyset page over `(created_at, id)` of the member contacts.
Add one contact to an audience
Idempotent — re-adding an existing member is a no-op (200), not an error. On a genuine add, enrolls the contact into any active `audience_added`-trig…
Add up to 100 contacts to an audience
Same idempotent-add + automation-enrollment behavior as the single-member endpoint, tallied into a summary. A `contact_id` not owned by the tenant is…
Remove a contact from an audience
Idempotent — removing a non-member (or an already-removed one) still returns 200 `deleted:true`.
Segments
Saved (and ad-hoc) contact filters.
List segments
Newest-first keyset page over `(created_at, id)`.
Create a saved segment
`filter` is validated by `email_core::parse_filter` and stored as submitted; it is re-validated and re-compiled to SQL on every `preview`/`count`/bro…
Preview an ad-hoc filter without saving it
Compiles `filter` and returns its live count plus a first page of matching contacts — powers a live segment builder while the user is still editing.
Get a segment
Rename or replace a segment's filter
Fields omitted from the body keep their current value; a replacement `filter` is re-validated.
Delete a segment
Preview a saved segment's matching contacts
Oldest-first keyset page over `(created_at, id)` of contacts matching the segment's compiled filter.
Get a saved segment's live match count
Suppressions
Per-tenant suppression (do-not-send) list.
List suppressions
Newest-first; cursor is the bare epoch-ms `created_at` of the last row returned.
Add a suppression
API-added suppressions are always stored as `reason: "manual"` — any `reason` supplied in the body is ignored (`hard_bounce`/ `complaint`/`unsubscrib…
Add up to 100 suppressions
Remove up to 100 suppressions
Uses the JSON-body-with-DELETE pattern.
Remove a suppression
Always returns 200 `deleted:true` regardless of whether the email was actually suppressed — this endpoint does not check existence first.
Templates
Server-side rendered templates with immutable versions.
List templates
Returns the tenant's full set of templates, newest first. Not paginated.
Create a template
Creates the template shell only — call `POST /v1/templates/{alias}/versions` to give it renderable content. `alias` must be unique per tenant.
Get a template and its versions
Create a new template version
Each call creates a new immutable version. When `activate` (default `true`), every other version is deactivated first — `POST /v1/email`'s `template_…
Webhooks
Event webhook endpoint registration and replay.
List webhook endpoints
Returns the tenant's full set (not paginated). Note: unlike the creation response, each item's `secret` is always an empty string here (the list quer…
Create a webhook endpoint
`secret` (an HMAC-SHA256 signing key, `whsec_...`) is generated server-side and returned **only in this response** — it is never shown again (subsequ…
Delete a webhook endpoint
Always returns 200 `deleted:true`; does not check existence first.
Replay webhook deliveries
Re-executes up to the 50 most recent matching deliveries, recording each as a new `webhook_deliveries` row (never mutates the original).
Inbound
Received email (Cloudflare Email Routing) activity.
List received email
Messages received via Cloudflare Email Routing. Newest-first; cursor is the bare epoch-ms `created_at` of the last row returned. Note the `from_addr`…
Get a received message, including raw MIME
The raw message is returned byte-for-byte from R2 exactly as ingested (never parsed) — `raw` is `null` if the R2 object is missing (metadata is still…
Attachments
Upload and retrieve email attachments via R2.
Upload an attachment
Uploads a file to R2 under `{tenant_id}/{message_id}/{filename}`. Returns an `r2_key` to reference in a `SendRequest`'s `attachments[].r2_key`. `mess…
Download an attachment
Not under `/v1` — a tenant-scoped convenience endpoint, not a public CDN. Requires an API key whose tenant matches the object key's `{tenant_id}/...`…
Health
Unauthenticated service health check.
Broadcasts
One-off marketing sends to an audience or segment.
List broadcasts
Newest-first keyset page over `(created_at, id)`.
Create a draft broadcast
`name` is the only required field — everything else can be filled in later via `PATCH` while still a `draft`. `target_kind`/ `target_id` must be supp…
Get a broadcast
Edit a draft broadcast
Only allowed while `status = draft` (else 409 `not_editable`) — editing a scheduled/sending/finished broadcast out from under the fan-out cron would…
Delete a broadcast
Hard delete, tenant-scoped. No status restriction — this can delete a broadcast in any state.
Validate and send (or schedule) a draft broadcast
Only callable from `draft` (else 409 `not_draft`). Requires a non-empty subject, a non-empty `body_html` or `body_text`, a target that still resolves…
Cancel a draft or scheduled broadcast
`draft`/`scheduled` → `cancelled`. Any other status (already `sending`/`paused`/`sent`/`cancelled`) is a 409 `not_cancelable` — including the race wh…
Automations
Event-triggered, multi-step marketing workflows.
List automations
Newest-first keyset page over `(created_at, id)`.
Create a draft automation
`name` + a valid `trigger_kind` are required; everything else can be filled in via `PATCH` before activation. `steps` (default `[]`) is validated by…
Get an automation
Edit a draft automation
Only allowed while `status = draft` (else 409 `not_editable`) — editing a live program out from under in-flight enrollments would be unsafe. Fields o…
Delete an automation
Hard delete the automation and its enrollments.
Activate an automation
`draft`/`paused` → `active`. Requires a non-empty, valid step program, a valid trigger, and a verified `from_domain_id` (send steps need a verified m…
Pause an active automation
`active` → `paused`. In-flight enrollments are held (not cancelled) — reactivating resumes them where they parked.
Archive an automation
Terminal. Any non-archived status → `archived`, and every still-active enrollment is cancelled in the same call.
Manually enroll a contact
The automation must be `active`. Idempotent — `UNIQUE(automation_id, contact_id)` makes re-enrolling a no-op (200), a genuine enrollment is 201.