Simple Email
Simple Email is a compatibility wire format: a set of endpoints under /v1/simple-email whose request and response shapes match a widely used JavaScript email SDK. If your application already sends through that SDK, point its baseUrl at Primitive and keep your send code as it is:
const client = new EmailSdk(process.env.PRIMITIVE_API_KEY!, { baseUrl: 'https://api.primitive.dev/v1/simple-email', }); const { data, error } = await client.emails.send({ from: 'support@yourdomain.com', to: 'alice@example.com', subject: 'Order confirmed', text: 'Your order is on its way.', });
Every send dispatches through the exact same path as POST /v1/send-mail: same authentication, validation, recipient gates, rate limits, and send records. Simple Email is a translation layer, not a second send pipeline, so a send made here shows up on GET /v1/sent-emails like any other.
Base URL and Authentication
https://api.primitive.dev/v1/simple-emailAuthentication is a normal Primitive credential: Authorization: Bearer prim_<api_key>. There is no separate key type for this surface.
Endpoints
| Method and path | Purpose |
|---|---|
POST /v1/simple-email/emails | Send one email. Returns 200 with a bare { "id": ... }. |
POST /v1/simple-email/emails/batch | Send up to 100 emails in one request (bare JSON array body). |
GET /v1/simple-email/emails/{id} | Read a sent email back in the compat shape. |
PATCH /v1/simple-email/emails/{id} | Reschedule a still-scheduled send ({ "scheduled_at": ... }). |
POST /v1/simple-email/emails/{id}/cancel | Cancel a still-scheduled send. |
Response and Error Shapes
This surface does not use the standard Primitive { success, data, error } envelope. Success bodies are the bare compat objects ({ "id": ... } on send, { "object": "email", "id": ... } on reschedule and cancel), and errors are:
{ "name": "invalid_parameter", "message": "`template` is not supported by this endpoint", "statusCode": 422 }
statusCode mirrors the HTTP status, so SDK clients can branch without reading the status line. Rate-limited responses keep their Retry-After and RateLimit-* headers.
Internal error codes are mapped to the compat vocabulary; the ones you are most likely to branch on:
Compat name | Status | When |
|---|---|---|
invalid_parameter | 422 | Validation failure, or an unsupported field (see below). Also used at 403 for a gated recipient (the message names the denied address) and at 409 for a reschedule or cancel that lost to execution. |
missing_required_field | 422 | from, to, subject, or both of text/html missing. |
invalid_api_key | 401 | Missing or invalid bearer token. |
invalid_from_address | 403 | The from domain is not a verified outbound sender for your org. |
restricted_api_key | 403 | Outbound mail is not enabled for this account. |
invalid_access | 403 | A usage cap or credit gate refused the send. |
invalid_idempotency_key | 409 | Idempotency conflict. |
rate_limit_exceeded | 429 | Rate limited; honor Retry-After. |
not_found | 404 | No email with this id is visible to your org. |
application_error | 500/5xx | Unexpected internal failure. |
Sending
POST /v1/simple-email/emails accepts these fields:
from,to,subject(required), plus at least one oftextorhtml;cc,bcc,reply_to(string or array of strings, liketo);headers(same allowlist as/v1/send-mail);tags(same rules as/v1/send-mail);scheduled_at(same bounds: strictly future, at most 30 days out);attachments, each withfilename,content(a base64 string), and optionalcontent_typeandcontent_id(note the field iscontenthere, notcontent_base64).
Field semantics are enforced by the canonical send-mail validator after translation, so address syntax, the header allowlist, tag charset, recipient caps, and scheduled-send bounds behave exactly as documented on Sending Mail. Sends on this surface are always asynchronous; there is no equivalent of wait.
Unsupported Fields Return 422
Any field outside the supported set is rejected with a 422 naming the field, including compat SDK features Primitive deliberately does not implement here (for example template and topic_id). This is by design: SDKs on this wire format are commonly configured with a fallback provider, and a 422 routes the message through the fallback instead of failing the send outright.
The same applies inside attachments: remote-file attachments (path) and Buffer-object content are rejected with a 422; inline the bytes as a base64 string.
Reading a Send Back
GET /v1/simple-email/emails/{id} returns the compat read-back shape:
{ "object": "email", "id": "...", "to": ["alice@example.com"], "from": "support@yourdomain.com", "subject": "Order confirmed", "html": null, "text": "Your order is on its way.", "created_at": "2026-07-17T12:00:00.000Z", "last_event": "delivered", "bcc": null, "cc": null, "reply_to": null, "message_id": "<...>", "scheduled_at": null }
last_event collapses Primitive's internal statuses into the compat vocabulary: queued, sent, delivered, bounced, delivery_delayed, failed, scheduled, or canceled. Statuses without a direct equivalent report sent (the send was accepted; the compat vocabulary cannot say more). For the full-fidelity record, read the same id from GET /v1/sent-emails/{id}.
Rescheduling and Canceling
PATCH /v1/simple-email/emails/{id} with { "scheduled_at": ... } moves a still-scheduled send; POST /v1/simple-email/emails/{id}/cancel cancels one. Both acknowledge with { "object": "email", "id": ... } and carry the same compare-and-swap semantics as the canonical endpoints: once the send has executed or was already canceled, they return a 409 whose message names the current status.
Batch Sending
POST /v1/simple-email/emails/batch takes a bare JSON array of 1 to 100 send bodies and answers with the accepted ids in input order:
{ "data": [ { "id": "..." }, { "id": "..." } ] }
Unlike POST /v1/send-mail/batch, which reports per-message results, this surface reports a single batch-level outcome, so it is adapted as all-or-nothing in two layers:
- Validate before dispatch. Every item is translated and validated up front. Any invalid item rejects the whole batch with a
422naming the item index (emails[3]: ...) and zero sends dispatched, so a validation failure is never partially accepted. - Serial fail-fast dispatch. Items are dispatched one at a time, in order, and the first failure returns immediately; nothing after the failing item is ever dispatched.
What serial dispatch cannot undo: items accepted before the failing one are already queued for delivery and cannot be recalled. The batch error message names the failing item's index and the count of already-accepted items, and tells you not to re-send the full batch; retry only the items from the failing index onward, or route just those through your fallback.
Two more batch behaviors to know:
- An
Idempotency-Keyheader is not applied per item; each item derives its own key from its payload. Consequently, byte-identical duplicate items within one batch dedupe to a single send and echo the same id in each slot. - Serial dispatch means latency grows linearly with batch size. Throughput-sensitive callers should prefer
POST /v1/send-mail/batch, whose per-item results avoid whole-batch retries.
Related Pages
- Sending Mail: the canonical send surface and full field semantics.
- REST API: the standard envelope the rest of the v1 API uses.
- Errors: the canonical error-code taxonomy these compat errors map from.