> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Personalize a message

> Fill one template with each recipient's data using variables, and handle missing values.

export const Tags = ({items}) => {
  const routes = {
    Email: "/integrations/email/overview",
    SMS: "/integrations/sms/overview",
    Push: "/integrations/push/overview",
    Inbox: "/in-app/overview",
    Chat: "/integrations/direct-message/overview",
    Templates: "/design/templates/overview",
    Variables: "/design/templates/variables",
    Elemental: "/design/elemental/overview",
    Brands: "/design/brands",
    Translations: "/design/elemental/locales",
    Routing: "/send/routing",
    Preferences: "/recipients/preferences/overview",
    Journeys: "/journeys/overview",
    Broadcasts: "/broadcasts/overview",
    Tenants: "/tenants/overview",
    Logs: "/monitor/overview",
    Webhooks: "/monitor/webhooks/outbound",
    Lists: "/recipients/lists-and-audiences/overview",
    Users: "/recipients/overview",
    Digests: "/journeys/nodes/digest",
    Environments: "/workspaces/overview",
    MCP: "/resources/mcp"
  };
  const icons = {
    Email: "envelope",
    SMS: "comment",
    Push: "mobile",
    Inbox: "inbox",
    Chat: "comments",
    Templates: "pen-ruler",
    Variables: "pen-ruler",
    Elemental: "pen-ruler",
    Brands: "pen-ruler",
    Translations: "pen-ruler",
    Routing: "paper-plane",
    Preferences: "users",
    Journeys: "route",
    Broadcasts: "bullhorn",
    Tenants: "building",
    Logs: "chart-simple",
    Webhooks: "chart-simple",
    Lists: "users",
    Users: "users",
    Digests: "route",
    Environments: "briefcase",
    MCP: "toolbox"
  };
  const base = "https://d3gk2c5xim1je2.cloudfront.net/fontawesome/v7.2.0/regular/";
  const names = String(items || "").split(",").map(entry => entry.trim()).filter(Boolean);
  return <div className="cx-tags">
      {names.map(name => {
    const href = routes[name];
    const icon = icons[name];
    const url = icon ? "url(" + base + icon + ".svg)" : null;
    const style = url ? {
      "--cx-tag-icon": url
    } : null;
    if (!href) {
      return <span className="cx-tag" data-icon={icon} style={style} key={name}>
              {name}
            </span>;
    }
    return <a className="cx-tag" data-icon={icon} style={style} href={href} key={name}>
            {name}
          </a>;
  })}
    </div>;
};

export const AppLink = ({href, children, name, bare}) => {
  const label = children || name || "Open in Courier";
  if (bare) {
    return <a href={href} target="_blank" rel="noreferrer">{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="app" href={href} target="_blank" rel="noreferrer">
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method" aria-hidden="true">↗</span>
    </a>;
};

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

<Tags items="Templates, Variables" />

Write one order-confirmation template, then let every send fill it with that customer's name, items, and total.

## Prerequisites

* <AppLink href="https://app.courier.com/settings/api-keys">A Courier API key</AppLink>
* <Doc href="/docs/design/templates/overview">A template with an email channel</Doc>
* <Doc href="/docs/recipients/overview">A profile holding `first_name` and `timezone`</Doc>

## Personalize the message

<Steps>
  <Step title="Write the template">
    Put this in an email block in <Doc href="/docs/design/templates/design-studio">Design Studio</Doc>, or create the template in code with the <Doc href="/docs/design/templates/api">Templates API</Doc>:

    ```handlebars theme={null}
    Hi {{default profile.first_name "there"}},

    {{#each items}}
    {{capitalize this.name}}: {{format "$%.2f" this.price}}
    {{/each}}

    Total: {{format "$%.2f" (add (default subtotal 0) (default tax 0))}}
    {{#if placed_at}}Placed {{datetime-format placed_at "%b %d, %Y" profile.timezone}}{{/if}}
    ```

    Four helpers are doing the work:

    | Helper    | What it handles                                                                                                                                                                        |
    | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `default` | A new customer has no `first_name` yet, so this prints `there` instead of nothing                                                                                                      |
    | `each`    | Walks the `items` array. Inside the loop, `this` is the current item                                                                                                                   |
    | `format`  | [sprintf](https://en.wikipedia.org/wiki/Printf), so the `$` goes in the format string. There is no currency helper                                                                     |
    | `add`     | Send the parts and let the template add them up, so a change is an edit and not a deploy. Wrap each one in `default`, because `add` on a missing value yields `NaN` and fails the send |

    `datetime-format` takes the timestamp, a [strftime](https://en.wikipedia.org/wiki/C_date_and_time_functions#strftime) format, and a timezone. Passing `profile.timezone` means every customer reads the time in their own zone. Guard it with `#if` rather than `default`, since an empty string is not a date either.

    Anything prefixed with `profile.` comes from the stored profile. Everything else comes from `data` on the send.
  </Step>

  <Step title="Send the data it fills in">
    <CodeGroup>
      ```javascript Node.js highlight={5-13} theme={null}
      const { requestId } = await client.send.message({
        message: {
          to: { user_id: 'user_123' },
          template: 'nt_01kx4h2jdafq8bk9aftxak4b40',
          data: {
            placed_at: '2026-05-15T18:30:00Z',
            subtotal: 74.98,
            tax: 6.19,
            items: [
              { name: 'wireless mouse', price: 29.99 },
              { name: 'usb-c hub', price: 44.99 },
            ],
          },
        },
      });
      ```

      ```python Python highlight={5-13} theme={null}
      response = client.send.message(
          message={
              "to": {"user_id": "user_123"},
              "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
              "data": {
                  "placed_at": "2026-05-15T18:30:00Z",
                  "subtotal": 74.98,
                  "tax": 6.19,
                  "items": [
                      {"name": "wireless mouse", "price": 29.99},
                      {"name": "usb-c hub", "price": 44.99},
                  ],
              },
          },
      )
      ```

      ```bash cURL highlight={8-16} theme={null}
      curl -X POST https://api.courier.com/send \
        -H "Authorization: Bearer $COURIER_API_KEY" \
        -H "Content-Type: application/json" \
        -d '{
          "message": {
            "to": { "user_id": "user_123" },
            "template": "nt_01kx4h2jdafq8bk9aftxak4b40",
            "data": {
              "placed_at": "2026-05-15T18:30:00Z",
              "subtotal": 74.98,
              "tax": 6.19,
              "items": [
                { "name": "wireless mouse", "price": 29.99 },
                { "name": "usb-c hub", "price": 44.99 }
              ]
            }
          }
        }'
      ```

      ```ruby Ruby highlight={5-13} theme={null}
      response = courier.send_.message(
        message: {
          to: { user_id: "user_123" },
          template: "nt_01kx4h2jdafq8bk9aftxak4b40",
          data: {
            placed_at: "2026-05-15T18:30:00Z",
            subtotal: 74.98,
            tax: 6.19,
            items: [
              { name: "wireless mouse", price: 29.99 },
              { name: "usb-c hub", price: 44.99 }
            ]
          }
        }
      )
      ```

      ```go Go highlight={9-17} theme={null}
      response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
      	Message: courier.SendMessageParamsMessage{
      		To: courier.SendMessageParamsMessageToUnion{
      			OfUserRecipient: &shared.UserRecipientParam{
      				UserID: courier.String("user_123"),
      			},
      		},
      		Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
      		Data: map[string]any{
      			"placed_at": "2026-05-15T18:30:00Z",
      			"subtotal":  74.98,
      			"tax":       6.19,
      			"items": []any{
      				map[string]any{"name": "wireless mouse", "price": 29.99},
      				map[string]any{"name": "usb-c hub", "price": 44.99},
      			},
      		},
      	},
      })
      ```

      ```java Java highlight={5-11} theme={null}
      SendMessageParams params = SendMessageParams.builder()
          .message(SendMessageParams.Message.builder()
              .to(UserRecipient.builder().userId("user_123").build())
              .template("nt_01kx4h2jdafq8bk9aftxak4b40")
              .data(JsonValue.from(java.util.Map.of(
                  "placed_at", "2026-05-15T18:30:00Z",
                  "subtotal", 74.98,
                  "tax", 6.19,
                  "items", java.util.List.of(
                      java.util.Map.of("name", "wireless mouse", "price", 29.99),
                      java.util.Map.of("name", "usb-c hub", "price", 44.99)))))
              .build())
          .build();
      SendMessageResponse response = client.send().message(params);
      ```

      ```php PHP highlight={5-13} theme={null}
      $response = $client->send->message(
        message: [
          'to' => ['user_id' => 'user_123'],
          'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
          'data' => [
            'placed_at' => '2026-05-15T18:30:00Z',
            'subtotal' => 74.98,
            'tax' => 6.19,
            'items' => [
              ['name' => 'wireless mouse', 'price' => 29.99],
              ['name' => 'usb-c hub', 'price' => 44.99],
            ],
          ],
        ],
      );
      ```

      ```csharp C# highlight={7-18} theme={null}
      SendMessageParams parameters = new()
      {
          Message = new()
          {
              To = new UserRecipient { UserID = "user_123" },
              Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
              Data = new Dictionary<string, JsonElement>()
              {
                  { "placed_at", JsonSerializer.SerializeToElement("2026-05-15T18:30:00Z") },
                  { "subtotal", JsonSerializer.SerializeToElement(74.98) },
                  { "tax", JsonSerializer.SerializeToElement(6.19) },
                  { "items", JsonSerializer.SerializeToElement(new[]
                      {
                          new { name = "wireless mouse", price = 29.99 },
                          new { name = "usb-c hub", price = 44.99 },
                      })
                  },
              },
          },
      };

      var response = await client.Send.Message(parameters);
      ```

      ```bash CLI highlight={5} theme={null}
      courier send message \
        --api-key "$COURIER_API_KEY" \
        --message.to '{"user_id": "user_123"}' \
        --message.template "nt_01kx4h2jdafq8bk9aftxak4b40" \
        --message.data '{"placed_at": "2026-05-15T18:30:00Z", "subtotal": 74.98, "tax": 6.19, "items": [{"name": "wireless mouse", "price": 29.99}, {"name": "usb-c hub", "price": 44.99}]}'
      ```

      ```text MCP theme={null}
      With Courier MCP, send my nt_01kx4h2jdafq8bk9aftxak4b40 template to user_123 with a wireless mouse and a usb-c hub.
      ```
    </CodeGroup>
  </Step>
</Steps>

## Verify

<Steps>
  <Step title="Read what arrived">
    ```text theme={null}
    Hi Sarah,

    Wireless mouse: $29.99
    Usb-c hub: $44.99

    Total: $81.17
    Placed May 15, 2026
    ```
  </Step>

  <Step title="Compare it against the data you sent">
    Open <AppLink href="https://app.courier.com/logs">Logs</AppLink>, find the `requestId`, and read the rendered message next to the `data` that produced it.

    <Warning>
      Check the delivered message, not the designer's preview. The preview substitutes variables but does not run helpers, so `format` and `datetime-format` can look wrong there and render correctly when sent.
    </Warning>
  </Step>
</Steps>

## Handle a missing value

A variable you only print is safe to omit: a missing single-brace variable renders as the literal `{tax}`, and a missing Handlebars `var` renders empty. That silence is why the `default` on the greeting matters.

A variable a **helper computes with** is not safe. Send the template above without `tax` and `add` produces `NaN`, `format` throws, and the whole message fails with `undefined is NaN`. The status is `UNDELIVERABLE`, and nothing is delivered on any channel. The send still returns `202` with a `requestId`, so the only place this surfaces is the message status.

Two helpers in this template need guarding, and they need different guards:

| Helper                            | Missing value                                | Guard                 |
| --------------------------------- | -------------------------------------------- | --------------------- |
| `add`, and any `format` fed by it | yields `NaN`, then throws                    | `default` to `0`      |
| `datetime-format`                 | throws `Cannot read properties of undefined` | `#if` around the line |
| `each`                            | renders nothing, safe                        | none needed           |

So guard every variable a helper computes with, and reserve the bare form for text you are only printing. That is why the total above is written:

```handlebars theme={null}
Total: {{format "$%.2f" (add (default subtotal 0) (default tax 0))}}
```

To fail loudly on the printed kind too, turn on **Throw on variable not found** in the template's Advanced settings. It catches a single-brace `{tax}` left in the rendered output, not a `{{tax}}` that rendered empty, so the `default` above is still what protects you. <Doc href="/docs/design/templates/variables#when-a-variable-is-missing">When a variable is missing</Doc> has the full behavior.

<Tip>
  Branch on what you know with `condition`, which takes its operator as a string: `{{#if (condition plan "==" "pro")}}Your Pro order ships free.{{/if}}`
</Tip>
