Docs/Sending email

Variables & personalization

Pass a variables object on a send and Anypost fills its values into the subject, body, and headers before delivery.

The variables field

variables is an optional object on POST /v1/email. Write {{ key }} markers anywhere in the message, and Anypost replaces each one with the matching value from variables:

curl https://api.anypost.com/v1/email \
  -H "Authorization: Bearer $ANYPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your {{plan}} plan is ready",
    "html": "<p>Welcome to the {{plan}} plan. You have {{seats}} seats.</p>",
    "variables": { "plan": "Pro", "seats": 5 }
  }'

That send arrives with the subject "Your Pro plan is ready". variables works the same way whether the body is inline, as above, or comes from a template; see Sending with templates.

Placeholder syntax

Markers use Handlebars: a key wrapped in double braces.

  • Simple value: {{plan}} resolves to variables.plan.
  • Nested value: {{order.total}} resolves to variables.order.total. Objects may nest up to four levels deep.
  • Missing key: a marker whose key is absent from variables renders as an empty string. It is not an error, and the marker is not left in the message.

A value is HTML-escaped by default, so one containing < or & is safe to place in an HTML body. To insert a value as raw markup, use triple braces: {{{ banner_html }}}. Reach for the triple-brace form only when the value is trusted HTML you intend to render.

Conditionals and loops

The subject and body support Handlebars block helpers, so a message can show a section conditionally or repeat one over a list. The data still comes entirely from variables; the blocks only decide which parts of the message render.

Include a section conditionally with {{#if}}, optionally with an {{else}}:

{{#if order.gift}}
  <p>A gift receipt is enclosed.</p>
{{else}}
  <p>Thanks for your order.</p>
{{/if}}

{{#unless}} is the inverse: it renders only when the value is falsy or absent.

Iterate over an array with {{#each}}. Inside the block, {{this}} is the current item and {{@index}} is its zero-based position:

<ul>
{{#each items}}
  <li>{{@index}}. {{this}}</li>
{{/each}}
</ul>

When the array holds objects, reference each object's keys directly inside the block: {{#each items}}{{name}}: {{price}}{{/each}}.

{{#with}} shifts the context into a nested object so you can drop the prefix:

{{#with order}}Order {{id}}, {{total}} total.{{/with}}

To branch on a comparison rather than on presence alone, comparison helpers such as eq and gt are available as sub-expressions: {{#if (eq plan "Pro")}}...{{/if}}.

Per-recipient values: name and email

Two markers are filled from the recipient and need no entry in variables:

MarkerValue
{{name}}The recipient's display name, or an empty string if the address has none.
{{email}}The recipient's email address.

They differ per recipient within a single send, so one {{name}} in the body greets everyone in to by their own name. If variables also defines name or email, the recipient binding takes precedence.

The reserved unsubscribe_url variable

A send with unsubscribe.mode: "generate" fills one more marker, {{unsubscribe_url}}, with that recipient's one-click unsubscribe link — the same URL Anypost puts in the List-Unsubscribe header. You do not add unsubscribe_url to variables; if you do, your value overrides the generated link.

It is the one marker that renders even on a send with no variables object (see Substitution is off unless you opt in below). See Unsubscribe handling for the full one-click setup.

Where substitution applies

When variables is present, Anypost renders markers in:

  • the subject,
  • the html body,
  • the text body,
  • the values of any custom headers.

Marker-like text in attachments or in addresses is left alone.

Substitution is off unless you opt in

Anypost substitutes only when a request carries a non-empty variables object. Send with no variables, or with variables: {}, and the message goes out exactly as written: any {{ ... }} in the body stays as literal text.

This protects a body that carries its own braces, such as a code sample or output from another templating system. It also means the {{name}} and {{email}} bindings do nothing unless the request includes a non-empty variables object.

One case overrides this: a mode: "generate" unsubscribe send. Anypost has to render it to fill {{unsubscribe_url}}, so the templating step runs even with no variables — and any other {{ ... }} in that message is evaluated along with it. See Unsubscribe handling.

The opposite case is the one to watch. Once variables is non-empty, every {{ ... }} in the rendered fields is treated as a marker, including any you did not intend as one.

Limits

LimitValue
Total size64,000 bytes, JSON-encoded
Keys per object level100
Nesting depth4
Value typesAny JSON-serializable value

A variables object that exceeds a limit is rejected with 422 validation_error; see API conventions.

Different values for each recipient

Within one POST /v1/email, every recipient shares the same variables object. Only {{name}} and {{email}} change from one recipient to the next.

To give each recipient genuinely different values, such as an order number or an account balance, send a batch. Every entry in a batch request carries its own variables. See Batch sending.

Where to go next