Skip to content

API ​

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

All endpoints at a glance ​

MethodPathPurpose
GET/api/v1/templatesList the templates in the key's workspace, latest change first.
POST/api/v1/templatesCreate a template (visual or code mode).
POST/api/v1/templates/validateCheck code without storing it, for the diagnostics.
GET/api/v1/templates/:idRead one template (code, styles, blocks, example data).
PUT/api/v1/templates/:idUpdate a template (rename, edit, switch to code mode).
DELETE/api/v1/templates/:idDelete a template. Renders and reads of it stop at once.
POST/api/v1/templates/:id/publishMake the draft what /render serves.
DELETE/api/v1/templates/:id/draftThrow away unpublished changes.
POST/api/v1/templates/:id/renderRender against runtime context to one HTML string.
GET/api/v1/templates/:id/versionRead the template's timestamps without its content.
GET/api/v1/templates/:id/presigned-urlMint 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, or any other OpenAPI-aware tool at it for a typed client. The bytes are the same ones this site is built from.