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

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# Message Content and Substitutions in the CLI

> Write the content of emails sent with the AhaSend CLI: text, HTML and AMP files, global and per-recipient substitutions, conditions, loops and defaults.

This guide shows how to give `ahasend messages send` its content, from flags or files, and how to personalize it with substitutions.

> [!NOTE]
>
> To send a transactional template stored in your AhaSend account instead, see [Stored Templates](#stored-templates) and the [`ahasend templates` commands](https://ahasend.com/docs/cli/commands/templates).

Before running the examples, [log in with a full API key](https://ahasend.com/docs/cli/authentication.md), use your verified sender domain, and replace `YOUR_RESOURCE_ID` with the ID of a published template in your test account. Create any named content, recipient, or JSON files first. Shell examples use Bash. Sends use sandbox mode.

## Content Flags

Give each part of the message inline or from a file, not both:

| Part | Inline | From a file |
| - | - | - |
| Plain text | `--text` | `--text-file` |
| HTML | `--html` | `--html-file` |
| AMP HTML | `--amp` | `--amp-file` |

* Text or HTML content is required. AMP content is an extra part that goes with one of them.
* Several parts together make a multipart email.
* `--text` with `--text-file`, `--html` with `--html-file`, or `--amp` with `--amp-file` fails with exit code 4.

```bash theme={null}
ahasend messages send --sandbox \
  --from sender@example.com \
  --to recipient@example.com \
  --subject "Your order" \
  --html-file order.html \
  --text-file order.txt
```

## Substitution Basics

The subject and the content can hold placeholders in double curly braces. The AhaSend API fills them for each recipient with [MiniJinja](https://github.com/mitsuhiko/minijinja), which uses Jinja2-compatible syntax.

```html welcome.html theme={null}
<h1>Hello {{ first_name }}!</h1>
<p>Welcome to {{ company_name }}.</p>
<p>Your account type is: {{ account_type }}</p>
```

The values come from two places:

| Flag | Values |
| - | - |
| `--global-substitutions` | A JSON file with one object, shared by every recipient |
| `--recipients` | A CSV or JSON file with values for each recipient |

A recipient's own value for a key wins over the global value of the same key. The API HTML-escapes values in the HTML and AMP content, so send them as plain text. The subject, custom headers and the text content get values as they are. Use `{{ name|safe }}` only for HTML that your own code builds.

## Substitution Types

### Global Substitutions

Use global substitutions for values that are the same for every recipient. They work with `--to` and with `--recipients`.

```json company-data.json theme={null}
{
  "company_name": "Example Inc.",
  "support_email": "support@example.com",
  "year": 2026
}
```

```bash theme={null}
ahasend messages send --sandbox \
  --from sender@example.com \
  --to recipient@example.com \
  --subject "News from {{ company_name }}" \
  --html-file welcome.html \
  --global-substitutions company-data.json
```

### Recipient Substitutions

Use a recipients file for values that differ per recipient. The file's extension, `.csv` or `.json`, sets its format.

> [tab] CSV Format
>
> ```csv users.csv theme={null}
> email,name,first_name,account_type
> john@example.com,John Doe,John,premium
> jane@example.com,Jane Smith,Jane,basic
> ```
>
> The `email` column is required, and `name` sets the display name. The two headers can be in any case. Every other column is a substitution, and every value is trimmed. An empty cell sets no value, so the global value of the same name, if any, applies to that recipient.

> [tab] JSON Format
>
> ```json users.json theme={null}
> [
>   {
>     "email": "john@example.com",
>     "name": "John Doe",
>     "substitution_data": {
>       "first_name": "John",
>       "account_type": "premium",
>       "balance": 1234.56
>     }
>   }
> ]
> ```
>
> Each recipient has an `email`, an optional `name`, and its values in `substitution_data`. Values can be strings, numbers, booleans, lists or objects, and numbers are sent as written.

```bash theme={null}
ahasend messages send --sandbox \
  --from sender@example.com \
  --recipients users.csv \
  --subject "Hello {{ first_name }}" \
  --html-file welcome.html \
  --global-substitutions company-data.json
```

A file with no recipients, an invalid address or an unknown JSON key fails with exit code 4 before anything is sent. For a bad entry, the error names its CSV row or JSON index. To send to many recipients, see [batch processing](https://ahasend.com/docs/cli/batch-processing.md).

## Template Syntax Examples

### Variables and Defaults

`default` gives a value to use when a variable is missing:

```html greeting.html theme={null}
<p>Hello {{ first_name|default("there") }},</p>
<p>Questions? Contact {{ support_email }}.</p>
```

### Conditional Content

```html conditional.html theme={null}
<h1>Hello {{ first_name }}!</h1>

{% if account_type == "premium" %}
    <p>Thank you for being a Premium member!</p>
{% else %}
    <p>Consider upgrading to Premium!</p>
{% endif %}
```

### Lists and Loops

A loop needs a list, so its values come from a JSON recipients file or the global substitutions file. CSV cells hold text only.

```html order-confirmation.html theme={null}
<h1>Order Confirmation</h1>
<p>Thank you, {{ customer_name }}!</p>

<table>
    {% for item in items %}
    <tr>
        <td>{{ item.name }}</td>
        <td>{{ item.quantity }}</td>
        <td>{{ item.price }}</td>
    </tr>
    {% endfor %}
</table>
```

```json orders.json theme={null}
[
  {
    "email": "john@example.com",
    "substitution_data": {
      "customer_name": "John",
      "items": [
        {"name": "Notebook", "quantity": 2, "price": "$8.00"},
        {"name": "Pen", "quantity": 5, "price": "$1.50"}
      ]
    }
  }
]
```

```bash theme={null}
ahasend messages send --sandbox \
  --from orders@example.com \
  --recipients orders.json \
  --subject "Your order, {{ customer_name }}" \
  --html-file order-confirmation.html
```

## Stored Templates

`--template-id` sends a transactional template that is stored in your AhaSend account, instead of content from flags or files. Create, publish and inspect stored templates with the [`ahasend templates` commands](https://ahasend.com/docs/cli/commands/templates). `ahasend templates get` lists the variables that a send must supply.

```bash theme={null}
ahasend messages send --sandbox \
  --template-id YOUR_RESOURCE_ID \
  --recipients users.csv \
  --global-substitutions company-data.json
```

* The template supplies the content, so the content flags on this page cannot be used with `--template-id`.
* `--from` and `--subject` are optional. Without them, the template's sender and subject apply.
* Substitutions work as on this page: the values of `--global-substitutions` are copied into each recipient's substitutions, and a recipient's own value wins.

## Testing Content

Send to a test address in sandbox mode, which delivers nothing:

```bash theme={null}
ahasend messages send --sandbox \
  --from test@example.com \
  --to test@example.com \
  --subject "Content test" \
  --html-file welcome.html \
  --global-substitutions test-data.json
```

Then open the message in the AhaSend dashboard, or run `ahasend messages get` with its Message-ID to see its content. See [`messages get`](https://ahasend.com/docs/cli/commands/messages#get-a-message).

## Best Practices

1. **Use meaningful variable names**: `{{ customer_first_name }}`, not `{{ fn }}`.
2. **Provide default values**: `{{ name|default("Friend") }}`.
3. **Test in sandbox mode**: Send to a test address before a real send.
4. **Keep templates simple**: Complex logic belongs in your application.
5. **Split global and recipient data**: Company details go in the global file, personal details in the recipients file.

## Next Steps

* [Learn about batch processing](https://ahasend.com/docs/cli/batch-processing.md)
* [Manage stored templates](https://ahasend.com/docs/cli/commands/templates.md)
* [Explore the MiniJinja documentation](https://github.com/mitsuhiko/minijinja)
* [View CLI examples](https://ahasend.com/docs/cli/examples.md)

## Related Guides

* [Install the CLI](https://ahasend.com/docs/cli/installation.md), [CLI authentication](https://ahasend.com/docs/cli/authentication.md), [REST sending guide](https://ahasend.com/docs/send-api/send-email.md).
* [SMTP settings](https://ahasend.com/docs/smtp.md), [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md), [Go SDK](https://ahasend.com/docs/guides/go-sdk.md).
* [CLI configuration and output](https://ahasend.com/docs/cli/configuration.md), [API authentication](https://ahasend.com/docs/api-reference/authentication.md), [Permission scopes](https://ahasend.com/docs/api-reference/scopes.md).
* [Domain setup](https://ahasend.com/docs/domains.md), [Sandbox behavior](https://ahasend.com/docs/send-api/sandbox.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.