Docs / API / Templates

Templates

Store reusable Handlebars templates. Render them with variables. AI-generate them from a prompt with optional voice-and-tone matching.

Why templates?

Handlebars syntax supported

Mailgrid uses a hand-written safe interpreter (Cloudflare Workers blocks new Function). The supported subset:

Variable interpolation

handlebars
Hello {{name}}!
Your order total is ${{order.total}}.
Triple-stash for raw HTML: {{{rawHtml}}}.

Double-stash {{x}} HTML-escapes. Triple-stash {{{x}}} emits raw.

Conditionals

if/else/unless
{{#if isPremium}}
  <p>Premium support available 24/7.</p>
{{else}}
  <p>Upgrade for 24/7 support.</p>
{{/if}}

{{#unless verified}}
  <p>Please verify your email.</p>
{{/unless}}

Loops

each
<ul>
{{#each items}}
  <li>{{this.name}} — ${{this.price}} (#{{@index}})</li>
{{/each}}
</ul>

Inside {{#each}}, the special variables are this (current item), @index (0-based), @first, @last.

Object scoping

with
{{#with user.address}}
  <p>{{street}}, {{city}}, {{country}}</p>
{{/with}}

Built-in helpers

HelperEffect
{{uppercase x}}Returns x.toUpperCase()
{{lowercase x}}Returns x.toLowerCase()
{{capitalize x}}First letter of each word capitalized
{{date x}}Format ISO date as YYYY-MM-DD
{{currency x}}Format number as USD (override locale on send)
{{default x "fallback"}}Use x if truthy, else fallback
{{eq a b}}Equality (for use inside #if)

Not supported (intentionally)

Create a template

POST /api/templates

Scopes: templates:write

FieldTypeRequiredDescription
namestring (64 chars, [A-Za-z0-9_-])YesUnique per tenant. Used as alternative to UUID for templateId.
subjectstring (998)YesHandlebars supported.
htmlstring (≤10 MB)YesHandlebars supported.
textstring (≤10 MB)NoPlain-text body; auto-sent as multipart/alternative.
descriptionstring (500)NoInternal label for your team.

Example

create
curl -X POST https://api.mailgrid.space/api/templates \
  -H "Authorization: Bearer $KEY" \
  -d '{
    "name": "welcome",
    "subject": "Welcome to {{company}}, {{name}}!",
    "html": "<h1>Hi {{name}}</h1><p>Glad you joined {{company}}.</p>{{#if isPro}}<p>Pro tools unlocked.</p>{{/if}}",
    "text": "Hi {{name}}. Glad you joined {{company}}."
  }'

Get a template

GET /api/templates/:id

Where :id is either the UUID or the unique name.

List templates

GET /api/templates

Render pipeline

When you send via a template, here's what happens internally:

  1. D1 lookup by tenant_id + id|name.
  2. Template AST parsed (cached if already in this Worker's memory).
  3. Variables interpolated into subject, html, text.
  4. If personalize: true, AI rewrites with contact metadata + tone.
  5. Tracking pixel + click rewrites injected (if enabled).
  6. Unsubscribe headers stamped.
  7. Sent to SES.

AI-generate a template

POST /api/templates/generate

Scopes: ai:use

FieldTypeRequiredDescription
promptstring (8-2000)YesWhat you want the email to do.
audiencestringNoWho's reading it (e.g. "developers, technical").
toneenumNoformal · friendly · urgent · celebratory
voiceExamplesstring[] (1-3)NoPaste sample emails for style-matched generation.

Voice & Tone Match

Provide 1-3 example emails (max 3000 chars each) and the AI rewrites your prompt in matching style:

voice match
{
  "prompt": "Notify customer their invoice is overdue.",
  "tone":   "friendly",
  "voiceExamples": [
    "Hey there! Just a heads up that we shipped 3.2 this morning...",
    "Quick note: we'll be doing maintenance from 2-3am UTC..."
  ]
}

The AI picks up: contractions, lowercase greetings, "Quick note", "Just a heads up". Without voice examples, you get a generic professional tone.

AI provider chain

  1. Workers AI (default) — Llama 3.1 8B Instruct. Free at the edge. ~800-1500 ms per generation.
  2. Anthropic Claude Haiku (fallback) — engaged automatically if ANTHROPIC_API_KEY is set. ~400-900 ms, higher quality.
  3. If both fail, the endpoint returns UPSTREAM_FAILED.

Auto-injected context

Mailgrid automatically pulls in your knowledge base entries as additional context for generation. Add entries via POST /api/knowledge like:

All entries flow into the system prompt automatically.

Response

200 OK
{
  "success": true,
  "data": {
    "subject": "Quick note about invoice {{invoiceId}}",
    "html":    "<p>Hey {{name}}, just a heads up...",
    "text":    "Hey {{name}}, just a heads up...",
    "variables": ["name", "invoiceId", "amount", "dueDate"]
  }
}

The variables array tells you which Handlebars placeholders the AI used — useful for building UI that asks the user to fill them in.

Persisting a generated template

Generation doesn't auto-save. Call POST /api/templates with the returned fields if you want to reuse it.

Using a template on send

send with template
{
  "from":       "hello@yourdomain.com",
  "to":         "user@example.com",
  "templateId": "welcome",
  "variables":  { "name": "Anna", "company": "Acme", "isPro": true }
}

Subject from the template can be overridden by passing subject on the send. Bodies cannot be overridden when templateId is used — either use a template OR provide html/text inline.

Errors specific to templates

CodeCause
NOT_FOUNDTemplate id/name doesn't exist for this tenant.
BAD_REQUESTTemplate syntax error (e.g. unclosed {{#if}}).
CONFLICTTemplate name already exists.
UPSTREAM_FAILEDAI generation provider failed.
Template safety

Variables passed into {{double}} stash are auto HTML-escaped. Be careful with {{{triple}}} stash — only use when the variable is trusted (e.g. another tenant-owned template render, not user input).