# API

Pick an operation from the sidebar to see its request body, response shape, and error codes.

## All endpoints at a glance

| Method | Path                                  | Purpose                                                         |
| ------ | ------------------------------------- | --------------------------------------------------------------- |
| GET    | `/api/v1/templates`                   | List the templates in the key's workspace, latest change first. |
| POST   | `/api/v1/templates`                   | Create a template (visual or code mode).                        |
| POST   | `/api/v1/templates/validate`          | Check code without storing it, for the diagnostics.             |
| GET    | `/api/v1/templates/:id`               | Read one template (code, styles, blocks, example data).         |
| PUT    | `/api/v1/templates/:id`               | Update a template (rename, edit, switch to code mode).          |
| DELETE | `/api/v1/templates/:id`               | Delete a template. Renders and reads of it stop at once.        |
| POST   | `/api/v1/templates/:id/publish`       | Make the draft what `/render` serves.                           |
| DELETE | `/api/v1/templates/:id/draft`         | Throw away unpublished changes.                                 |
| POST   | `/api/v1/templates/:id/render`        | Render against runtime context to one HTML string.              |
| GET    | `/api/v1/templates/:id/version`       | Read the template's timestamps without its content.             |
| GET    | `/api/v1/templates/:id/presigned-url` | Mint a presigned editor URL.                                    |

## Diagnostics

Every read and every write returns a `diagnostics` array: the same problems the
editor's own panel shows. Undefined and unused variables, loop bindings that
resolve to nothing, helpers that are not registered, Handlebars that will not
compile.

They are advisory. A template that renders can still carry warnings, and only a
diagnostic with `severity: "error"` means it will not render at all.

If you generate templates, check them before you replace one that is live:

```
POST /api/v1/templates/validate
{ "code": "Hello {{name}}", "exampleContext": { "name": "Ada" } }
```

That stores nothing and touches no template. It answers with the diagnostics a
write would have returned, so a template that is being rendered right now is
never the thing you find the mistake on.

## Subject line

A template can carry an email subject. It is optional, and it is Handlebars
like the body, so it fills from the same context with the same helpers.

Set it on create or update, on its own or in the same call as the body:

```bash
curl -X PUT https://postcraft.io/api/v1/templates/<id> \
  -H "x-api-key: $API_KEY" \
  -H "content-type: application/json" \
  -d '{ "subject": "Your order {{orderNumber}} has shipped" }'
```

A write that leaves `subject` out keeps the stored one. Send `null`, or an
empty string, to remove it:

```
PUT /api/v1/templates/:id
{ "subject": null }
```

A subject holds up to 998 characters, the longest line an email header allows.
Like any content write, a `PUT` lands in the published copy unless it says
`"stage": "draft"`. `GET /api/v1/templates/:id` returns the subject as stored,
or `null` when there is none.

Every render returns the subject filled in from the render's context, as one
line of plain text with HTML left unescaped:

```
POST /api/v1/templates/:id/render
{ "context": { "orderNumber": "1042" }, "target": "email" }
{ "html": "...", "text": "...", "subject": "Your order 1042 has shipped", ... }
```

The subject is `null` when the template has none, or when it renders to
nothing, such as a lone `{{firstName}}` the context lacks, so `null` always
means "use your own default". A subject that does not
render fails the call with `422`, and the message starts with `Subject:`, so
you know to look there rather than in the body.

A subject with a syntax error saves the way a body with one does. The write
succeeds, its `diagnostics` name the error, and renders fail until it is
fixed.

## Concurrent edits

A write is last-writer-wins by default. If an author has the template open in
the editor while your integration writes to it, one of the two silently loses.

Send `ifUnmodifiedSince` on `PUT /api/v1/templates/:id` to opt out of that. Pass
the `updatedAt` from your last read or write, and the call is refused with `409`
if anyone has written since:

```
PUT /api/v1/templates/:id
{ "code": "...", "ifUnmodifiedSince": "2026-09-17T10:04:00.000Z" }
```

Re-read the template, reapply your change, and retry with the new `updatedAt`.
Every write returns one, so a sequence of writes never needs a read between them.

## Knowing when a template changed

Every render answers with `publishedAt`: when the published copy it rendered
last changed. Saving a draft leaves it alone; publishing moves it. If you keep
anything derived from a template, such as a review of its content or a cached
copy, store the `publishedAt` it was derived from and compare it with the one
your next render returns. A different value means what renders has changed.

To check without rendering, read the template's timestamps on their own:

```
GET /api/v1/templates/:id/version
{ "updatedAt": "...", "draftUpdatedAt": null, "publishedAt": "2026-09-18T17:57:16.164Z" }
```

`updatedAt` moves on every write, draft saves included, so compare
`publishedAt` when what you care about is what recipients receive.

## Rate limits

Over the limit answers `429` with a `Retry-After` header. Wait that long and
retry.

## The spec, live

Postcraft serves the OpenAPI spec at:

```
GET https://postcraft.io/api/v1/openapi.json
```

Point Postman, [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen), or any other OpenAPI-aware tool at it for a typed client. The bytes are the same ones this site is built from.
