API Reference
API Reference
GitHub
Templates

Preview & Test

Preview templates and send test emails

There are three preview routes and one test send. They differ in what they render, and picking the wrong one is how you end up previewing something the send path would never produce.

RouteRendersUse it for
POST /api/v1/templates/previewContent in the request bodyThe editor, while somebody types
POST /api/v1/templates/{id}/versions/{versionId}/previewA stored version, in one languageChecking a draft before activating
POST /api/v1/emails/previewWhat a send would produce, from the active versionConfirming what your integration will actually mail

They also differ in how they treat missing data, and that difference is deliberate:

  • The two template routes render leniently. A variable your data does not supply comes out empty, so an author previewing half-written copy sees the layout instead of an error.
  • POST /api/v1/emails/preview renders strictly, exactly as a transactional send does — a missing key fails the request. That is what makes it the honest rehearsal: if the preview refuses, the send would have refused too.

Ad-hoc content

POST /api/v1/templates/preview

Nothing is read from or written to the store, so this works before a template exists.

curl -X POST http://localhost:3000/api/v1/templates/preview \
  -H "Authorization: Bearer myk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Welcome, {{ name }}",
    "html": "<h1>Welcome, {{ name }}</h1><p><a href=\"{{ activation_url }}\">Activate</a></p>",
    "text": "Welcome, {{ name }} - activate at {{ activation_url }}",
    "css": "h1 { color: #1a1a1a }",
    "data": {
      "name": "Alice",
      "activation_url": "https://example.com/activate?token=abc"
    }
  }'

subject is required, everything else is optional. Passing css runs the same inlining a send would, which is the point of having it here — you see the style attributes the recipient will get.

{
  "preview": {
    "subject": "Welcome, Alice",
    "html": "<h1 style=\"color:#1a1a1a\">Welcome, Alice</h1><p><a href=\"https://example.com/activate?token=abc\">Activate</a></p>",
    "text": "Welcome, Alice - activate at https://example.com/activate?token=abc"
  }
}

A stored version

POST /api/v1/templates/{templateId}/versions/{versionId}/preview
{ "language": "fr", "data": {"name": "Marie"} }

Both fields are optional. Omit language and it resolves through the usual four steps . Omit data and it falls back to the version’s sample data, then to the template’s — so a template with sample data set previews sensibly with an empty body.

The response names which localization it actually rendered, which matters when you asked for a language that does not exist yet:

{ "preview": { "subject": "...", "html": "...", "text": "..." }, "language": "fr" }

This route is how you check a draft. It reads the version you name, not the active one.

What a send would produce

POST /api/v1/emails/preview
curl -X POST http://localhost:3000/api/v1/emails/preview \
  -H "Authorization: Bearer myk_..." \
  -H "Content-Type: application/json" \
  -d '{"template_name": "welcome", "language": "en", "data": {"name": "Alice"}}'

Addresses the template the way a send does — by name or id, resolving the active version, refusing a missing data key — so this is the one to call from an integration test. It renders and returns, and queues nothing. The response names the template it resolved:

{ "template": "welcome", "preview": { "subject": "...", "html": "...", "text": "..." } }

A preview is not the delivered message

None of these apply tracking. There is no open pixel and links are not rewritten, because both are per-message and no message exists. System variables render empty for the same reason.

A preview also skips everything the send path does around rendering: sender verification, suppression filtering, quota, and the template’s attachments. Use send-test when you need to see those.

Test send

POST /api/v1/templates/{templateId}/send-test
curl -X POST http://localhost:3000/api/v1/templates/$TPL/send-test \
  -H "Authorization: Bearer myk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@example.com",
    "to": ["you@example.com"],
    "language": "en",
    "data": {"name": "Test User"}
  }'

from is required and its domain must be verified by the project, exactly as on a real send. to accepts up to five addresses.

This is a genuine send. It renders the active version, attaches the template’s files, checks suppressions and quota, and puts a row in the email log like any other message:

{
  "email": { "id": "0198f6a1-3c7e-7b21-9f4d-2a5c8e0b1d33", "status": "queued" },
  "suppressed_recipients": []
}

An address on the project’s suppression list comes back in suppressed_recipients and is not mailed — worth checking when a test to your own address appears to vanish.

Send one before activating

A preview renders in a browser and a mail client is not a browser. Outlook ignores rules a preview honours, Gmail clips a long message, and dark mode inverts colours nothing declared. The five-address limit exists because this step belongs in the release, not in a loop.