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-email

Authentication is a normal Primitive credential: Authorization: Bearer prim_<api_key>. There is no separate key type for this surface.

Endpoints

Method and pathPurpose
POST /v1/simple-email/emailsSend one email. Returns 200 with a bare { "id": ... }.
POST /v1/simple-email/emails/batchSend 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}/cancelCancel 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 nameStatusWhen
invalid_parameter422Validation 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_field422from, to, subject, or both of text/html missing.
invalid_api_key401Missing or invalid bearer token.
invalid_from_address403The from domain is not a verified outbound sender for your org.
restricted_api_key403Outbound mail is not enabled for this account.
invalid_access403A usage cap or credit gate refused the send.
invalid_idempotency_key409Idempotency conflict.
rate_limit_exceeded429Rate limited; honor Retry-After.
not_found404No email with this id is visible to your org.
application_error500/5xxUnexpected internal failure.

Sending

POST /v1/simple-email/emails accepts these fields:

  • from, to, subject (required), plus at least one of text or html;
  • cc, bcc, reply_to (string or array of strings, like to);
  • 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 with filename, content (a base64 string), and optional content_type and content_id (note the field is content here, not content_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:

  1. Validate before dispatch. Every item is translated and validated up front. Any invalid item rejects the whole batch with a 422 naming the item index (emails[3]: ...) and zero sends dispatched, so a validation failure is never partially accepted.
  2. 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-Key header 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.