postmark-templates

Public
0

Repository: activecampaign/postmark-skills

Log in or sign up to clone this skill.

A
activecampaign
Imported Mar 3, 2026

Low Risk

No security issues found

INFO

Skill manifest does not include a 'license' field. Specifying a license helps users understand usage terms.

Remediation Add 'license' field to SKILL.md frontmatter (e.g., MIT, Apache-2.0)

Scanned in 0.006s

Description

Use when creating, managing, or sending with Postmark server-side email templates — Handlebars syntax, layout inheritance, template validation, and cross-server pushing.

Details

License MIT
Metadata
author
postmark
version
1.0.0

Skill Files

Download .zip
SKILL.md
# Email Templates with Postmark

## Overview

Postmark templates are server-side email templates using Handlebars syntax. Templates are rendered on Postmark's servers — no client-side rendering library needed.

| Feature | Description |
|---------|-------------|
| **Syntax** | Handlebars (Mustache-compatible) |
| **Rendering** | Server-side — no React, no client library |
| **Types** | Standard templates and Layout templates |
| **Inheritance** | Standard templates can inherit from a Layout |
| **Validation** | API endpoint to test-render templates before sending |
| **Cross-server** | Push templates between servers (staging → production) |
| **Limit** | 100 templates per server (contact support for more) |

## Quick Start

1. **Create a template** via API or the [Postmark dashboard](https://account.postmarkapp.com)
2. **Define variables** using Handlebars syntax: `{{variable_name}}`
3. **Send with template** using `POST /email/withTemplate` or `POST /email/batchWithTemplates`
4. **Pass data** via `TemplateModel` — Postmark renders and sends

## Template Syntax (Handlebars)

```handlebars
Hello {{name}},                            {{! variable }}
{{{html_content}}}                         {{! unescaped HTML }}
{{#if premium}}Premium member{{/if}}       {{! conditional }}
{{#each items}}{{this.name}}{{/each}}      {{! iteration }}
{{customer.address.city}}                  {{! nested object }}
```

See [references/handlebars-syntax.md](references/handlebars-syntax.md) for the full syntax reference including conditionals, iteration with index, nested objects, and common mistakes.

## Template Types

| Type | `TemplateType` | Sendable | Purpose |
|------|---------------|----------|---------|
| **Standard** | `"Standard"` | Yes | Defines subject, HTML, and text body |
| **Layout** | `"Layout"` | No | Reusable wrapper injected via `{{{@content}}}` |

Standard templates can reference a Layout via `LayoutTemplate: "layout-alias"`. The Standard template's body replaces `{{{@content}}}` in the Layout at send time.

See [references/layout-templates.md](references/layout-templates.md) for layout creation, assignment, and examples.

## API Endpoints

### Template CRUD

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/templates` | `POST` | Create a new template |
| `/templates` | `GET` | List all templates (`?count=100&offset=0&templateType=Standard`) |
| `/templates/{idOrAlias}` | `GET` | Get a single template |
| `/templates/{idOrAlias}` | `PUT` | Update a template |
| `/templates/{idOrAlias}` | `DELETE` | Delete a template |
| `/templates/validate` | `POST` | Validate template rendering |
| `/templates/push` | `PUT` | Push templates to another server |

### Sending with Templates

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/email/withTemplate` | `POST` | Send single email with template |
| `/email/batchWithTemplates` | `POST` | Send batch with templates (up to 500) |

## Send with Template

Always use `TemplateAlias` over `TemplateId` — aliases survive re-creation and work across environments:

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

await client.sendEmailWithTemplate({
  From: '[email protected]',
  To: '[email protected]',
  TemplateAlias: 'welcome-email',
  TemplateModel: {
    name: 'Jane Doe',
    product_name: 'Acme App'
  },
  MessageStream: 'outbound'
});
```

## Validate a Template

Test-render without sending — useful in CI/CD before deploying template changes:

```javascript
const validation = await client.validateTemplate({
  Subject: 'Welcome {{name}}',
  HtmlBody: '<h1>Hello {{name}}</h1>',
  TextBody: 'Hello {{name}}',
  TestRenderModel: { name: 'Test User' }
});

console.log('Valid:', validation.AllContentIsValid);
console.log('Rendered:', validation.Subject.RenderedContent);
```

See [references/template-api.md](references/template-api.md) for full CRUD operations (create, list, update, delete), batch sending, validate, and push between servers.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Using `{{html}}` for HTML content | Use triple braces `{{{html}}}` for unescaped HTML |
| Forgetting `{{{@content}}}` in Layout | Layout templates must include `{{{@content}}}` placeholder |
| Deleting a Layout with dependents | Remove layout association from Standard templates first |
| Using Template ID across environments | Use `TemplateAlias` — it survives re-creation and works across servers |
| Not validating before deploy | Use `/templates/validate` to test-render before sending |
| Sending a Layout directly | Layouts are wrappers — you can only send Standard templates |
| Missing TemplateModel fields | Handlebars renders missing variables as empty strings — validate your data |
| Exceeding 100 templates | Contact Postmark support to increase the per-server limit |

## Notes

- Templates use Handlebars (Mustache-compatible) syntax — no React or client-side rendering needed
- Template aliases are strings; template IDs are integers — prefer aliases for portability
- `TemplateType` is either `Standard` (sendable) or `Layout` (wrapper)
- Layout inheritance: Standard template body replaces `{{{@content}}}` in the Layout
- Push templates between servers using the Account Token (not Server Token)
- Maximum 100 templates per server by default
- Template validation (`/templates/validate`) lets you test-render without sending
- Both `TemplateId` and `TemplateAlias` work for sending — use one, not both
references/handlebars-syntax.md Reference
# Handlebars Template Syntax

Postmark templates use Handlebars (Mustache-compatible) syntax. Templates are rendered server-side — no client-side library needed.

## Variables

```handlebars
Hello {{name}},

Your order {{order_id}} has been confirmed.
Thank you for shopping with {{company_name}}.
```

Variables render to an empty string if missing from `TemplateModel` — they do not throw errors.

## Unescaped HTML

Use triple braces to render raw HTML without escaping:

```handlebars
{{{html_content}}}
```

- Use `{{variable}}` for user-supplied content (HTML-escaped for security)
- Use `{{{variable}}}` only for trusted HTML you control

## Conditionals

```handlebars
{{#if premium_member}}
  <p>Thank you for being a premium member!</p>
{{else}}
  <p>Upgrade to premium for exclusive benefits.</p>
{{/if}}
```

Falsy values (`false`, `0`, `""`, `null`, `undefined`, `[]`) trigger the `else` branch.

### Nested Conditionals

```handlebars
{{#if order_shipped}}
  {{#if tracking_number}}
    <p>Track your order: {{tracking_number}}</p>
  {{else}}
    <p>Your order is on its way!</p>
  {{/if}}
{{else}}
  <p>Your order is being prepared.</p>
{{/if}}
```

## Iteration

```handlebars
<table>
  {{#each items}}
  <tr>
    <td>{{this.name}}</td>
    <td>{{this.quantity}}</td>
    <td>{{this.price}}</td>
  </tr>
  {{/each}}
</table>
```

Inside `{{#each}}`, use `{{this.field}}` or just `{{field}}` to access item properties.

### Index and First/Last

```handlebars
{{#each items}}
  <p>{{@index}}. {{this.name}}</p>
{{/each}}
```

`@index` is 0-based. `@first` and `@last` are also available as boolean helpers.

## Nested Objects

```handlebars
{{customer.name}}
{{customer.address.city}}
{{customer.address.zip}}
```

## Layout Placeholder

In Layout templates, use this special placeholder where the Standard template body is injected:

```handlebars
{{{@content}}}
```

Must use triple braces — the Standard template's rendered body is HTML and should not be escaped.

## Full TemplateModel Example

Template:

```handlebars
Hello {{name}},

{{#if premium_member}}
  Thank you for being a premium member!
{{/if}}

Your order {{order_id}} contains:
{{#each items}}
  - {{this.name}} x{{this.quantity}} — {{this.price}}
{{/each}}

Shipping to: {{customer.address.city}}, {{customer.address.state}}

{{{custom_message}}}
```

Corresponding `TemplateModel`:

```json
{
  "name": "Jane Doe",
  "premium_member": true,
  "order_id": "ORD-12345",
  "items": [
    { "name": "Widget", "quantity": 2, "price": "$19.99" },
    { "name": "Gadget", "quantity": 1, "price": "$29.99" }
  ],
  "customer": {
    "address": {
      "city": "San Francisco",
      "state": "CA"
    }
  },
  "custom_message": "<strong>Special offer inside!</strong>"
}
```

## Common Syntax Mistakes

| Mistake | Fix |
|---------|-----|
| `{{html_body}}` renders escaped HTML | Use `{{{html_body}}}` for unescaped HTML |
| `{{#each}}` items without `this.` | Use `{{this.field}}` inside each blocks |
| Missing variable shows as blank | Handlebars renders missing vars as empty string — validate your model |
| `{{@content}}` in Layout | Must use triple braces: `{{{@content}}}` |
references/layout-templates.md Reference
# Layout Templates

Layout templates provide a reusable wrapper (CSS, header, footer) that Standard templates inherit from. This enforces consistent branding without duplicating boilerplate across every template.

## How It Works

```
Layout Template
├── <html><head>styles</head><body>
├── <header>Logo, navigation</header>
├── {{{@content}}}   ← Standard template body injected here
├── <footer>Links, unsubscribe</footer>
└── </body></html>
```

The Standard template only defines the content section — the Layout wraps it automatically when sending.

## Template Types

### Standard Templates

Regular sendable templates that define subject, HTML body, and text body:

```json
{
  "Name": "Order Confirmation",
  "Alias": "order-confirmation",
  "Subject": "Order {{order_id}} confirmed",
  "HtmlBody": "<h1>Order Confirmed</h1><p>Hi {{name}}, your order {{order_id}} is confirmed.</p>",
  "TextBody": "Order Confirmed\nHi {{name}}, your order {{order_id}} is confirmed.",
  "TemplateType": "Standard"
}
```

### Layout Templates

Reusable wrappers that inject Standard template content via `{{{@content}}}`:

```json
{
  "Name": "Base Layout",
  "Alias": "base-layout",
  "HtmlBody": "<html><body><header>...</header><main>{{{@content}}}</main><footer>...</footer></body></html>",
  "TextBody": "{{{@content}}}\n\n---\n(c) 2025 Your Company",
  "TemplateType": "Layout"
}
```

**Layouts do not have a Subject** — the subject is defined on the Standard template.

## Creating a Layout

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

const layout = await client.createTemplate({
  Name: 'Company Layout',
  Alias: 'company-layout',
  HtmlBody: `
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <style>
        body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 0; background: #f4f4f4; }
        .wrapper { max-width: 600px; margin: 0 auto; background: #ffffff; }
        .header { background: #0a2540; padding: 24px; text-align: center; }
        .header img { height: 32px; }
        .content { padding: 32px 40px; }
        .footer { padding: 20px 40px; text-align: center; color: #666; font-size: 12px; }
        .footer a { color: #666; }
      </style>
    </head>
    <body>
      <div class="wrapper">
        <div class="header">
          <img src="https://yourdomain.com/logo.png" alt="Your Company">
        </div>
        <div class="content">
          {{{@content}}}
        </div>
        <div class="footer">
          <p>&copy; {{year}} Your Company, Inc.</p>
          {{#if unsubscribe_url}}
            <p><a href="{{unsubscribe_url}}">Unsubscribe</a></p>
          {{/if}}
        </div>
      </div>
    </body>
    </html>
  `,
  TextBody: `{{{@content}}}

---
(c) {{year}} Your Company, Inc.
{{#if unsubscribe_url}}Unsubscribe: {{unsubscribe_url}}{{/if}}`,
  TemplateType: 'Layout'
});

console.log('Layout ID:', layout.TemplateId);
```

## Assigning a Layout to a Standard Template

Set `LayoutTemplate` to the layout's alias (or ID):

```javascript
const template = await client.createTemplate({
  Name: 'Order Confirmation',
  Alias: 'order-confirmation',
  LayoutTemplate: 'company-layout',
  Subject: 'Your order {{order_id}} is confirmed',
  HtmlBody: `
    <h1>Order Confirmed!</h1>
    <p>Hi {{name}}, your order <strong>{{order_id}}</strong> is confirmed.</p>
    <table>
      {{#each items}}
      <tr>
        <td>{{this.name}}</td>
        <td>{{this.price}}</td>
      </tr>
      {{/each}}
    </table>
    <p>Total: {{order_total}}</p>
  `,
  TextBody: `Order Confirmed!\n\nHi {{name}}, your order {{order_id}} is confirmed.\n\nTotal: {{order_total}}`,
  TemplateType: 'Standard'
});
```

The Standard template's `HtmlBody` replaces `{{{@content}}}` in the Layout automatically.

## Variables Flow Through Both

Variables in `TemplateModel` are accessible in both the Layout and the Standard template:

```javascript
await client.sendEmailWithTemplate({
  From: '[email protected]',
  To: '[email protected]',
  TemplateAlias: 'order-confirmation',
  TemplateModel: {
    // Used by Standard template
    name: 'Jane Doe',
    order_id: 'ORD-12345',
    items: [{ name: 'Widget', price: '$19.99' }],
    order_total: '$19.99',
    // Used by Layout template
    year: '2025',
    unsubscribe_url: null  // null = condition is false, footer link hidden
  },
  MessageStream: 'outbound'
});
```

## Changing or Removing a Layout

```javascript
// Assign a different layout
await client.editTemplate('order-confirmation', {
  LayoutTemplate: 'new-layout'
});

// Remove a layout (set to null)
await client.editTemplate('order-confirmation', {
  LayoutTemplate: null
});
```

## Rules and Constraints

| Rule | Detail |
|------|--------|
| `{{{@content}}}` required | Layout must include this placeholder — triple braces |
| No Subject on Layout | Subject is defined only on Standard templates |
| Layouts are not sendable | You can only send Standard templates, not Layouts directly |
| Delete order | Cannot delete a Layout that has dependent Standard templates |
| No nested layouts | Layouts cannot inherit from other Layouts |
| Variables shared | TemplateModel variables are available to both Layout and Standard |
references/template-api.md Reference
# Template API Reference

## Create a Template

**Endpoint:** `POST /templates`

### Node.js

```javascript
const postmark = require('postmark');
const client = new postmark.ServerClient(process.env.POSTMARK_SERVER_TOKEN);

const template = await client.createTemplate({
  Name: 'Welcome Email',
  Alias: 'welcome-email',
  Subject: 'Welcome to {{product_name}}, {{name}}!',
  HtmlBody: `
    <h1>Welcome, {{name}}!</h1>
    <p>Thanks for joining {{product_name}}.</p>
    {{#if trial}}
      <p>Your trial ends on {{trial_end_date}}.</p>
    {{/if}}
    <a href="{{action_url}}">Get Started</a>
  `,
  TextBody: 'Welcome, {{name}}!\n\nThanks for joining {{product_name}}.\n\nGet started: {{action_url}}',
  TemplateType: 'Standard'
});

console.log('Template ID:', template.TemplateId);
```

### cURL

```bash
curl "https://api.postmarkapp.com/templates" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '{
    "Name": "Welcome Email",
    "Alias": "welcome-email",
    "Subject": "Welcome to {{product_name}}, {{name}}!",
    "HtmlBody": "<h1>Welcome, {{name}}!</h1><p>Thanks for joining {{product_name}}.</p>",
    "TextBody": "Welcome, {{name}}!\n\nThanks for joining {{product_name}}.",
    "TemplateType": "Standard"
  }'
```

### Response

```json
{
  "TemplateId": 12345,
  "Name": "Welcome Email",
  "Alias": "welcome-email",
  "Active": true,
  "TemplateType": "Standard",
  "LayoutTemplate": null
}
```

---

## List Templates

```javascript
const templates = await client.getTemplates({
  count: 100,
  offset: 0,
  templateType: 'Standard' // 'Standard', 'Layout', or 'All'
});

templates.Templates.forEach(t => {
  console.log(`${t.Name} (${t.Alias || t.TemplateId}) — ${t.Active ? 'Active' : 'Inactive'}`);
});
```

---

## Get a Template

```javascript
const template = await client.getTemplate('welcome-email');
// or by ID:
const template = await client.getTemplate(12345);
```

---

## Update a Template

```javascript
await client.editTemplate('welcome-email', {
  Subject: 'Welcome to {{product_name}}!',
  HtmlBody: '<h1>Updated content for {{name}}</h1>'
});

// By ID:
await client.editTemplate(12345, {
  Name: 'Welcome Email v2',
  Subject: 'Welcome aboard, {{name}}!'
});
```

Only fields you include are updated — omitted fields are unchanged.

---

## Delete a Template

```javascript
await client.deleteTemplate('welcome-email');
// or by ID:
await client.deleteTemplate(12345);
```

**Note:** You cannot delete a Layout template that has dependent Standard templates. Remove the `LayoutTemplate` association from all dependents first.

---

## Validate a Template

Test-render without sending. Use in CI/CD to verify templates before deploying.

**Endpoint:** `POST /templates/validate`

### Node.js

```javascript
const validation = await client.validateTemplate({
  Subject: 'Welcome {{name}}',
  HtmlBody: '<h1>Hello {{name}}</h1>{{#if premium}}<p>Premium member</p>{{/if}}',
  TextBody: 'Hello {{name}}',
  TestRenderModel: {
    name: 'Test User',
    premium: true
  }
});

if (validation.AllContentIsValid) {
  console.log('Rendered subject:', validation.Subject.RenderedContent);
  console.log('Rendered HTML:', validation.HtmlBody.RenderedContent);
} else {
  console.error('Errors:', validation.HtmlBody.ValidationErrors);
}
```

### cURL

```bash
curl "https://api.postmarkapp.com/templates/validate" \
  -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Server-Token: $POSTMARK_SERVER_TOKEN" \
  -d '{
    "Subject": "Welcome {{name}}",
    "HtmlBody": "<h1>Hello {{name}}</h1>",
    "TextBody": "Hello {{name}}",
    "TestRenderModel": { "name": "Test User" }
  }'
```

The response includes `SuggestedTemplateModel` — a list of all variables detected in the template. Useful for documentation.

---

## Push Templates Between Servers

Sync templates from staging to production. Requires an **Account Token**, not a Server Token.

**Endpoint:** `PUT /templates/push`

```bash
# Preview first (PerformChanges: false)
curl "https://api.postmarkapp.com/templates/push" \
  -X PUT \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Account-Token: $POSTMARK_ACCOUNT_TOKEN" \
  -d '{
    "SourceServerID": 12345,
    "DestinationServerID": 67890,
    "PerformChanges": false
  }'

# Apply (PerformChanges: true)
curl "https://api.postmarkapp.com/templates/push" \
  -X PUT \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -H "X-Postmark-Account-Token: $POSTMARK_ACCOUNT_TOKEN" \
  -d '{
    "SourceServerID": 12345,
    "DestinationServerID": 67890,
    "PerformChanges": true
  }'
```

Templates are matched by Alias — templates without an Alias cannot be pushed.

---

## Send with a Template

### Single Email (alias recommended)

```javascript
const result = await client.sendEmailWithTemplate({
  From: '[email protected]',
  To: '[email protected]',
  TemplateAlias: 'welcome-email',
  TemplateModel: {
    name: 'Jane Doe',
    product_name: 'Acme App',
    trial: true,
    trial_end_date: 'February 15, 2025',
    action_url: 'https://app.yourdomain.com/start'
  },
  MessageStream: 'outbound'
});
```

### Batch with Templates (up to 500)

```javascript
const results = await client.sendEmailBatchWithTemplates([
  {
    From: '[email protected]',
    To: '[email protected]',
    TemplateAlias: 'welcome-email',
    TemplateModel: { name: 'User 1', product_name: 'Acme App' },
    MessageStream: 'outbound'
  },
  {
    From: '[email protected]',
    To: '[email protected]',
    TemplateAlias: 'welcome-email',
    TemplateModel: { name: 'User 2', product_name: 'Acme App' },
    MessageStream: 'outbound'
  }
]);

results.forEach((result, i) => {
  if (result.ErrorCode === 0) {
    console.log(`Email ${i + 1} sent: ${result.MessageID}`);
  } else {
    console.error(`Email ${i + 1} failed: ${result.Message}`);
  }
});
```

Version History

v1.0.0 Imported from GitHub
1 week ago