Skip to the documentation
API

List templates

Every template on the connection, newest first, keyset paginated.

GETapi.openemail.uk/templates

Runs the real call against your workspace, with your own key.

GET /templates

Every template on the connection, newest first, keyset paginated.

Two engines

shell
export OE=https://api.openemail.ukexport AUTH="Authorization: Bearer $OPENEMAIL_API_KEY"

A template is a body stored once and sent many times, and it belongs to the CONNECTION rather than to whoever wrote it. A workspace key sees the same templates a colleague does, and deleting the author’s account does not take them with it.

engine: "blocks" stores a tree whose node kinds are the exports of @react-email/components (Section, Row, Column, Container, Text, Heading, Button, Link, Img, Hr, Markdown, CodeBlock and CodeInline) and whose props are those components’ own. It is validated on the way in, so a bad node is a 422 on the call that wrote it rather than a broken email later.

engine: "html" stores markup you already have, sanitised once when the version is published. This is the one to reach for when your templates are react-email components living in your own repo: render the component with @react-email/render in your own build and post the result. There is no JSX endpoint and there is not going to be one. The API takes HTML because HTML is what a mail client reads, and executing a caller’s component would buy a sandbox nobody needs.

Declared asFilled byMissing at send
`slots`whoever edits the templatethe slot’s own default renders
`props`whoever sendsmissing_template_prop, a 422, and no mail leaves

Both are written as {{key}} in the body and in the subject, and both carry a kind (text, url or image) which decides how the value is escaped when it is substituted. A key nothing declares fails at publish; a url value whose scheme is not http, https or mailto is refused rather than rendered.

A published version is frozen. Editing the body of a published template mints a new draft instead of changing what live sends resolve, so a colleague rewriting the copy cannot change what your code already sends, and pinning version means it cannot change it when they publish either.

Example

Needs templates:read. limit goes to 100, status narrows to draft, active or archived, and cursor is opaque, so pass back the nextCursor you were given rather than building one.

curl
curl "$OE/templates?limit=25&status=active" -H "$AUTH"
Response
{  "object": "list",  "data": [    {      "object": "template",      "id": "tpl_9c1f0a4b7e05d3862c1f0a44",      "name": "Order shipped",      "slug": "order-shipped",      "description": null,      "status": "active",      "publishedVersion": 3,      "latestVersion": 4,      "createdAt": "2026-08-01T09:12:44.000Z",      "updatedAt": "2026-08-28T16:03:10.000Z"    }  ],  "hasMore": false,  "nextCursor": null}

publishedVersion is what a send without a version resolves and latestVersion is the draft sitting on top of it. The two differing means somebody has edited and not published. It is not an error, and it is worth surfacing in a deploy log.

A row is metadata. The declared slots and props come back from a retrieval, which is the call to make when you need to know what to pass one.