# Templates

A template is an email your team writes once and sends many times: an order confirmation, a password reset, a welcome email. You write the HTML, declare its variables, publish it, and then send it with just its id or alias and the variable values. The API works like Resend Templates.

## Before you start

- Managing templates through the API needs a key with `email:write`. For a key that only sends with templates, grant `email:send`.
- The template's `from` address, or the one you pass when sending, must be on a verified domain.

## Write and publish

Open **Email → Templates → New template**. The editor has:

- **Defaults**: sender, subject, Reply-To and folder. A send can override each of them.
- **HTML** with syntax highlighting, a **Preview** tab filled with the fallback values, and a **Plain text** tab. Leave plain text empty and Ha Tang generates it from the HTML.
- **Variables**: declare every variable the subject and body use, as `string` or `number`, with an optional fallback. **Declare all** adds the ones Ha Tang finds in the content.
- **Check links** reports links that are empty, only `#`, on a placeholder domain (`example.com`, `yourdomain.com`, `localhost`…), not `http(s)`, unreachable, or answering with an error page. Link problems hold back publishing until you choose **Publish anyway**.
- **Send test** to your own address, **Duplicate** and **Publish**.

Only a published version can be sent. You can keep editing the draft: sends keep using the published version until you publish again. The list shows **Unpublished changes** when the draft differs from what is published.

## Variables

Write variables as `{{{NAME}}}`, or `{{{NAME|default}}}` with an inline default:

```html
<p>Hi {{{NAME|there}}}, order {{{ORDER}}} for {{{TOTAL}}} VND is confirmed.</p>
```

- Keys use letters, digits and `_`, up to 50 characters. A template declares at most 50 variables, and each value is at most 2,000 characters.
- Values are HTML-escaped when they go into the HTML part.
- A `number` variable refuses values that are not numbers.
- The value comes from the send; if there is none, from the inline default; if there is none, from the declared fallback. A variable with none of these makes the send fail with `422` and nothing is sent.
- Publishing fails while the content uses a variable that is not declared.
- These names are reserved and filled for each recipient by broadcasts and automations: `{{{FIRST_NAME}}}`, `{{{LAST_NAME}}}`, `{{{EMAIL}}}`, `{{{UNSUBSCRIBE_URL}}}` (and its alias `{{{RESEND_UNSUBSCRIBE_URL}}}`). Contact properties are available as `{{{contact.plan}}}`; see [Contacts, segments and topics](https://hatang.dev/docs/guides/email/audiences.md). You do not declare these.

## Send with a template

**cURL**

```bash
curl -X POST https://api.hatang.dev/emails \
  -H "Authorization: Bearer $HATANG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["lan@example.net"],
    "template": {
      "id": "order-confirmation",
      "variables": { "NAME": "Lan", "ORDER": "DH1024", "TOTAL": 450000 }
    }
  }'
```

**Node.js**

```ts
const { data, error } = await hatang.emails.send({
  to: "lan@example.net",
  template: { id: "order-confirmation", variables: { NAME: "Lan", ORDER: "DH1024", TOTAL: 450000 } },
});
```

**Python**

```python
hatang.emails.send({
    "to": ["lan@example.net"],
    "template": {"id": "order-confirmation", "variables": {"NAME": "Lan", "ORDER": "DH1024", "TOTAL": 450000}},
})
```

**PHP**

```php
$res = $client->post('/emails', [
    'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
    'json' => [
        'to' => ['lan@example.net'],
        'template' => ['id' => 'order-confirmation', 'variables' => ['NAME' => 'Lan', 'ORDER' => 'DH1024', 'TOTAL' => 450000]],
    ],
]);
```

- `template.id` is the template id (`tpl_…`) or its alias.
- `from`, `subject` and `reply_to` in the request override the template's defaults. They are required when the template has none.
- Do not send `html` or `text` together with `template`.
- `POST /emails/batch` accepts `template` in each item.
- To start a broadcast from a template, pass `template_id` to `POST /broadcasts`, or choose **Start from a template** in the broadcast editor. See [Broadcasts](https://hatang.dev/docs/guides/email/broadcasts.md).

| Error | Cause |
|---|---|
| `404 not_found` | No template has this id or alias. |
| `422 validation_error` "is not published yet" | Publish the template first. |
| `422 validation_error` "Missing value for template variable" | A variable has no value, no inline default and no fallback. |
| `422 validation_error` "The template has no `from`" | Pass `from` in the request or set it on the template. |

## Manage templates with the API

```bash
curl -X POST https://api.hatang.dev/templates \
  -H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "name": "Order confirmation",
    "alias": "order-confirmation",
    "from": "Acme <orders@example.com>",
    "subject": "Order {{{ORDER}}} is confirmed",
    "html": "<p>Hi {{{NAME|there}}}, order {{{ORDER}}} for {{{TOTAL}}} VND is confirmed.</p>",
    "variables": [
      { "key": "NAME", "type": "string" },
      { "key": "ORDER", "type": "string" },
      { "key": "TOTAL", "type": "number", "fallback_value": 0 }
    ]
  }'
# { "object": "template", "id": "tpl_…" }

curl -X POST https://api.hatang.dev/templates/order-confirmation/publish -H "Authorization: Bearer $HATANG_API_KEY"
# { "object": "template", "id": "tpl_…", "current_version_id": "tplv_…" }
```

| Endpoint | Description |
|---|---|
| `POST /templates` | Create a draft: `name`, `html` (required), `alias`, `from`, `subject`, `reply_to`, `text`, `variables`, `folder`. |
| `GET /templates` | List templates. Filters: `q` (name or alias), `folder`, `status` (`draft` or `published`), plus `limit` and `after`. |
| `GET /templates/{id or alias}` | One template, with `status`, `current_version_id`, `published_at` and `has_unpublished_versions`. |
| `PATCH /templates/{id or alias}` | Change the draft. |
| `POST /templates/{id or alias}/publish` | Publish the draft. |
| `POST /templates/{id or alias}/duplicate` | Copy it into a new draft named "(copy)". |
| `DELETE /templates/{id or alias}` | Delete it. |

An alias uses lowercase letters, digits, `-` and `_`, up to 64 characters, and is unique in your business. The Node.js SDK has the same calls under `hatang.templates`: `create`, `get`, `list`, `update`, `publish`, `duplicate` and `remove`.

## Next steps

- [Send email](https://hatang.dev/docs/guides/email/sending.md)
- [Broadcasts](https://hatang.dev/docs/guides/email/broadcasts.md)
- [Automations](https://hatang.dev/docs/guides/email/automations.md)

Source: https://hatang.dev/docs/guides/email/templates
