← Back to docs

Templates API

Templates API

Create reusable email templates with Handlebars variable substitution. Templates can be referenced by slug or UUID when sending emails.

Create Template

POST /v1/templates

Request Body

FieldTypeRequiredDescription
namestringYesDisplay name (max 191 characters)
slugstringYesUnique identifier (lowercase alphanumeric + hyphens, 2-100 chars)
descriptionstringNoOptional description
subjectstringNoSubject line template (supports {{variables}})
html_bodystringNoHTML body template
text_bodystringNoPlain text body template

Example Request

curl -X POST https://api.postscale.io/v1/templates \
  -H "Authorization: Bearer ps_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome Email",
    "slug": "welcome",
    "description": "Sent to new users after signup",
    "subject": "Welcome to {{company_name}}, {{first_name}}!",
    "html_body": "<h1>Hello {{first_name}}</h1><p>Welcome to {{company_name}}!</p>",
    "text_body": "Hello {{first_name}}, Welcome to {{company_name}}!"
  }'

Response 201

{
  "template": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "org_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
    "name": "Welcome Email",
    "slug": "welcome",
    "description": "Sent to new users after signup",
    "subject": "Welcome to {{company_name}}, {{first_name}}!",
    "html_body": "<h1>Hello {{first_name}}</h1><p>Welcome to {{company_name}}!</p>",
    "text_body": "Hello {{first_name}}, Welcome to {{company_name}}!",
    "active": true,
    "created_at": "2026-03-16T12:00:00Z",
    "updated_at": "2026-03-16T12:00:00Z"
  }
}

Errors

StatusErrorDescription
400Invalid slugSlug doesn't match ^[a-z0-9][a-z0-9-]*[a-z0-9]$
400Invalid template syntaxSubject, html_body, or text_body has a Handlebars parse error
403Template limit reachedPlan limit exceeded
409Slug already existsAnother template in this org uses the same slug

List Templates

GET /v1/templates

Query Parameters

ParameterTypeDescription
limitintegerNumber of results (default: 50, max: 100)
offsetintegerNumber of results to skip

Example Request

curl https://api.postscale.io/v1/templates?limit=50 \
  -H "Authorization: Bearer ps_live_your_api_key"

Response 200

{
  "templates": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "org_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
      "name": "Welcome Email",
      "slug": "welcome",
      "description": "Sent to new users after signup",
      "subject": "Welcome to {{company_name}}, {{first_name}}!",
      "html_body": "...",
      "text_body": "...",
      "active": true,
      "created_at": "2026-03-16T12:00:00Z",
      "updated_at": "2026-03-16T12:00:00Z"
    }
  ],
  "total": 1
}

Get Template

GET /v1/templates/:id

The :id parameter accepts either a UUID or a slug.

Example Requests

# By UUID
curl https://api.postscale.io/v1/templates/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
  -H "Authorization: Bearer ps_live_your_api_key"

# By slug
curl https://api.postscale.io/v1/templates/welcome \
  -H "Authorization: Bearer ps_live_your_api_key"

Response 200

{
  "template": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "org_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
    "name": "Welcome Email",
    "slug": "welcome",
    "subject": "Welcome to {{company_name}}, {{first_name}}!",
    "html_body": "<h1>Hello {{first_name}}</h1><p>Welcome to {{company_name}}!</p>",
    "text_body": "Hello {{first_name}}, Welcome to {{company_name}}!",
    "active": true,
    "created_at": "2026-03-16T12:00:00Z",
    "updated_at": "2026-03-16T12:00:00Z"
  }
}

Update Template

PUT /v1/templates/:id

Partially update a template. Only provided fields are changed. The :id parameter accepts either a UUID or a slug.

Request Body

FieldTypeDescription
namestringDisplay name
slugstringUnique slug (validated for format and uniqueness)
descriptionstringDescription
subjectstringSubject line template
html_bodystringHTML body template
text_bodystringPlain text body template
activebooleanWhether the template can be used for sending

All fields are optional. Template syntax is validated on update.

Example Request

curl -X PUT https://api.postscale.io/v1/templates/welcome \
  -H "Authorization: Bearer ps_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Welcome aboard, {{first_name}}!",
    "active": true
  }'

Response 200

Returns the full updated template object.


Delete Template

DELETE /v1/templates/:id

Permanently delete a template. The :id parameter accepts either a UUID or a slug.

Example Request

curl -X DELETE https://api.postscale.io/v1/templates/welcome \
  -H "Authorization: Bearer ps_live_your_api_key"

Response 200

{
  "status": "deleted"
}

Preview Template

POST /v1/templates/:id/preview

Render a template with test variables and return the result. Useful for testing templates before sending. The :id parameter accepts either a UUID or a slug.

Request Body

FieldTypeRequiredDescription
variablesobjectNoKey-value pairs to substitute into the template

Example Request

curl -X POST https://api.postscale.io/v1/templates/welcome/preview \
  -H "Authorization: Bearer ps_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "variables": {
      "first_name": "Jane",
      "company_name": "Acme Corp"
    }
  }'

Response 200

{
  "subject": "Welcome to Acme Corp, Jane!",
  "html": "<h1>Hello Jane</h1><p>Welcome to Acme Corp!</p>",
  "text": "Hello Jane, Welcome to Acme Corp!"
}

Template Object

FieldTypeDescription
idstring (UUID)Unique template identifier
org_idstring (UUID)Organization that owns this template
namestringDisplay name
slugstringUnique slug used to reference the template when sending
descriptionstring | nullOptional description
subjectstringSubject line template with {{variable}} placeholders
html_bodystringHTML body template
text_bodystringPlain text body template
activebooleanWhether the template can be used for sending
created_atstring (ISO 8601)Creation timestamp
updated_atstring (ISO 8601)Last update timestamp

Built-in Variables

These variables are automatically injected when rendering templates (they won't overwrite user-provided values):

VariableDescription
{{current_year}}Current year (e.g., 2026)
{{current_date}}Current date in YYYY-MM-DD format
{{unsubscribe_url}}One-click unsubscribe URL (populated at send time)

Template Syntax

Templates use Handlebars syntax:

  • Variables: {{variable_name}}
  • Nested access: {{user.first_name}}
  • Conditionals: {{#if paid}}...{{else}}...{{/if}}
  • Loops: {{#each items}}{{this.name}}{{/each}}
  • Defaults: {{default first_name "there"}} — returns "there" if first_name is empty or missing

Plan Limits

PlanMax Templates
Free5
Starter25
Plus50
Pro100
Scale500