postkit
Guides

Templates

Create and render Handlebars email templates

Postkit templates let you create reusable email layouts with Handlebars variables. Define your template once, then send personalized emails by providing variable data at send time.

You need an API key to use the templates API. See the quickstart to get your key.

Create a template

Define your template

Create a template with POST /v1/templates. Provide a name, subject line, HTML body, and optionally a plain text version. Use Handlebars double-brace syntax for dynamic content.

curl -X POST https://api.postkit.eu/v1/templates \
  -H "Authorization: Bearer pk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order Confirmation",
    "subject": "Your order {{order_id}} has been confirmed",
    "html": "<h1>Hi {{customer_name}},</h1><p>Your order {{order_id}} is confirmed. Total: {{total}}</p>",
    "text": "Hi {{customer_name}}, Your order {{order_id}} is confirmed. Total: {{total}}",
    "engine": "handlebars",
    "variables": [
      { "name": "order_id", "type": "string", "required": true },
      { "name": "customer_name", "type": "string", "required": true },
      { "name": "total", "type": "string", "required": true }
    ]
  }'
const response = await fetch('https://api.postkit.eu/v1/templates', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pk_live_abc123...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Order Confirmation',
    subject: 'Your order {{order_id}} has been confirmed',
    html: '<h1>Hi {{customer_name}},</h1><p>Your order {{order_id}} is confirmed. Total: {{total}}</p>',
    text: 'Hi {{customer_name}}, Your order {{order_id}} is confirmed. Total: {{total}}',
    engine: 'handlebars',
    variables: [
      { name: 'order_id', type: 'string', required: true },
      { name: 'customer_name', type: 'string', required: true },
      { name: 'total', type: 'string', required: true },
    ],
  }),
});
const template = await response.json();
console.log(template.id); // tmpl_abc123
import requests

response = requests.post(
    'https://api.postkit.eu/v1/templates',
    headers={'Authorization': 'Bearer pk_live_abc123...'},
    json={
        'name': 'Order Confirmation',
        'subject': 'Your order {{order_id}} has been confirmed',
        'html': '<h1>Hi {{customer_name}},</h1><p>Your order {{order_id}} is confirmed. Total: {{total}}</p>',
        'text': 'Hi {{customer_name}}, Your order {{order_id}} is confirmed. Total: {{total}}',
        'engine': 'handlebars',
        'variables': [
            {'name': 'order_id', 'type': 'string', 'required': True},
            {'name': 'customer_name', 'type': 'string', 'required': True},
            {'name': 'total', 'type': 'string', 'required': True},
        ],
    },
)
template = response.json()
print(template['id'])  # tmpl_abc123

The response includes the template ID and its initial status:

{
  "id": "tmpl_abc123",
  "name": "Order Confirmation",
  "status": "draft",
  "version": 1,
  "engine": "handlebars"
}

Iterate on your draft

Templates start in draft status. You can update a draft as many times as you need before publishing. Each update increments the version number. The published version (if any) remains active while you iterate.

Template variables

Handlebars provides a straightforward syntax for dynamic content:

{{variable_name}}          -- insert a variable value
{{#if condition}}...{{/if}}   -- conditional block
{{#each items}}...{{/each}}   -- iterate over arrays

The variables array in the create request defines expected variables with metadata for validation and documentation:

{
  "variables": [
    { "name": "customer_name", "type": "string", "required": true },
    { "name": "order_id", "type": "string", "required": true },
    { "name": "total", "type": "number", "required": true },
    { "name": "is_premium", "type": "boolean", "required": false, "default_value": "false" },
    { "name": "order_date", "type": "date", "required": false },
    { "name": "tracking_url", "type": "url", "required": false }
  ]
}

Supported variable types:

TypeDescriptionExample
stringText value"Max Mustermann"
numberNumeric value49.99
booleanTrue or falsetrue
dateISO 8601 date"2026-04-01"
urlURL string"https://example.com/track"

Preview a template

Render a template with sample data to preview the output without sending an email. Use POST /v1/templates/render with the template ID and variable data.

curl -X POST https://api.postkit.eu/v1/templates/render \
  -H "Authorization: Bearer pk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tmpl_abc123",
    "template_data": {
      "customer_name": "Max Mustermann",
      "order_id": "ORD-4821",
      "total": "49.99 EUR"
    }
  }'
const response = await fetch('https://api.postkit.eu/v1/templates/render', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pk_live_abc123...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template_id: 'tmpl_abc123',
    template_data: {
      customer_name: 'Max Mustermann',
      order_id: 'ORD-4821',
      total: '49.99 EUR',
    },
  }),
});
const rendered = await response.json();
console.log(rendered.html);
response = requests.post(
    'https://api.postkit.eu/v1/templates/render',
    headers={'Authorization': 'Bearer pk_live_abc123...'},
    json={
        'template_id': 'tmpl_abc123',
        'template_data': {
            'customer_name': 'Max Mustermann',
            'order_id': 'ORD-4821',
            'total': '49.99 EUR',
        },
    },
)
rendered = response.json()
print(rendered['html'])

The response includes the fully rendered HTML and text:

{
  "html": "<h1>Hi Max Mustermann,</h1><p>Your order ORD-4821 is confirmed. Total: 49.99 EUR</p>",
  "text": "Hi Max Mustermann, Your order ORD-4821 is confirmed. Total: 49.99 EUR"
}

Publish a template

Once you are satisfied with your draft, publish it with POST /v1/templates/:id/publish. Only published templates can be used when sending emails.

curl -X POST https://api.postkit.eu/v1/templates/tmpl_abc123/publish \
  -H "Authorization: Bearer pk_live_abc123..."
const response = await fetch(
  'https://api.postkit.eu/v1/templates/tmpl_abc123/publish',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer pk_live_abc123...',
    },
  }
);
const template = await response.json();
// template.status === "published"
response = requests.post(
    'https://api.postkit.eu/v1/templates/tmpl_abc123/publish',
    headers={'Authorization': 'Bearer pk_live_abc123...'},
)
template = response.json()
# template['status'] == 'published'

The template's status changes to published and its published_version is set to the current version number.

Publishing is a one-way action for that version. To make changes, update the template (which creates a new draft version) and publish again, or duplicate the template.

Send with a template

Once a template is published, use it when sending emails by passing template_id and template_data instead of html and text. The template's subject line is used by default, but you can override it.

curl -X POST https://api.postkit.eu/v1/emails \
  -H "Authorization: Bearer pk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <noreply@acme.eu>",
    "to": ["user@example.com"],
    "template_id": "tmpl_abc123",
    "template_data": {
      "customer_name": "Max Mustermann",
      "order_id": "ORD-4821",
      "total": "49.99 EUR"
    }
  }'
const response = await fetch('https://api.postkit.eu/v1/emails', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pk_live_abc123...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'Acme <noreply@acme.eu>',
    to: ['user@example.com'],
    template_id: 'tmpl_abc123',
    template_data: {
      customer_name: 'Max Mustermann',
      order_id: 'ORD-4821',
      total: '49.99 EUR',
    },
  }),
});
const email = await response.json();
response = requests.post(
    'https://api.postkit.eu/v1/emails',
    headers={'Authorization': 'Bearer pk_live_abc123...'},
    json={
        'from': 'Acme <noreply@acme.eu>',
        'to': ['user@example.com'],
        'template_id': 'tmpl_abc123',
        'template_data': {
            'customer_name': 'Max Mustermann',
            'order_id': 'ORD-4821',
            'total': '49.99 EUR',
        },
    },
)

See the Sending Email guide for all sending options including batch sending, scheduling, and attachments.

Duplicate a template

Create a copy of an existing template with POST /v1/templates/:id/duplicate. The copy starts in draft status so you can iterate on it independently. You can optionally provide a custom name for the duplicate.

curl -X POST https://api.postkit.eu/v1/templates/tmpl_abc123/duplicate \
  -H "Authorization: Bearer pk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order Confirmation v2"
  }'
const response = await fetch(
  'https://api.postkit.eu/v1/templates/tmpl_abc123/duplicate',
  {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer pk_live_abc123...',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: 'Order Confirmation v2',
    }),
  }
);
const copy = await response.json();
// copy.status === "draft"
response = requests.post(
    'https://api.postkit.eu/v1/templates/tmpl_abc123/duplicate',
    headers={'Authorization': 'Bearer pk_live_abc123...'},
    json={'name': 'Order Confirmation v2'},
)
copy = response.json()
# copy['status'] == 'draft'

If you omit the name field, the duplicate is named "Copy of Original Name".

List and manage templates

List templates

Retrieve all templates with cursor-based pagination:

curl https://api.postkit.eu/v1/templates?limit=10 \
  -H "Authorization: Bearer pk_live_abc123..."
const response = await fetch('https://api.postkit.eu/v1/templates?limit=10', {
  headers: {
    'Authorization': 'Bearer pk_live_abc123...',
  },
});
const { data } = await response.json();
response = requests.get(
    'https://api.postkit.eu/v1/templates',
    headers={'Authorization': 'Bearer pk_live_abc123...'},
    params={'limit': 10},
)
templates = response.json()['data']

Delete a template

Remove a template permanently with DELETE /v1/templates/:id:

curl -X DELETE https://api.postkit.eu/v1/templates/tmpl_abc123 \
  -H "Authorization: Bearer pk_live_abc123..."
const response = await fetch(
  'https://api.postkit.eu/v1/templates/tmpl_abc123',
  {
    method: 'DELETE',
    headers: {
      'Authorization': 'Bearer pk_live_abc123...',
    },
  }
);
const result = await response.json();
// result.deleted === true
response = requests.delete(
    'https://api.postkit.eu/v1/templates/tmpl_abc123',
    headers={'Authorization': 'Bearer pk_live_abc123...'},
)
result = response.json()
# result['deleted'] == True

Deleting a template is permanent. Any emails that reference the deleted template's ID will fail to send.

What's next?

  • Sending Email -- all sending features including batch, scheduling, and attachments
  • API Reference -- full template endpoint documentation