# Templates

A template is a subject and a body with holes in it. You store it once, and each send
fills the holes:

**curl**

```bash
curl -X POST https://api.avelto.dev/v1/emails \
  -H "Authorization: Bearer av_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@mail.acme.com>",
    "to": "ada@example.com",
    "template_slug": "welcome",
    "variables": {
      "name": "Ada",
      "plan": "Growth"
    }
  }'
```

**Node**

```ts
import { Avelto } from "@avelto/sdk";

const avelto = new Avelto(process.env.AVELTO_API_KEY!);

const { id } = await avelto.emails.send({
  from: "Acme <hello@mail.acme.com>",
  to: "ada@example.com",
  template_slug: "welcome",
  variables: { name: "Ada", plan: "Growth" },
});
```

**Python**

```python
import os, requests

r = requests.post(
    "https://api.avelto.dev/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['AVELTO_API_KEY']}"},
    json={
      "from": "Acme <hello@mail.acme.com>",
      "to": "ada@example.com",
      "template_slug": "welcome",
      "variables": {
        "name": "Ada",
        "plan": "Growth"
      }
    },
)
r.raise_for_status()
print(r.json())
```

**Go**

```go
package main

import (
	"bytes"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body := []byte(`{"from":"Acme <hello@mail.acme.com>","to":"ada@example.com","template_slug":"welcome","variables":{"name":"Ada","plan":"Growth"}}`)
	req, _ := http.NewRequest("POST", "https://api.avelto.dev/v1/emails", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+os.Getenv("AVELTO_API_KEY"))

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	fmt.Println(res.Status)
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://api.avelto.dev/v1/emails")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV["AVELTO_API_KEY"]}"
req["Content-Type"] = "application/json"
req.body = JSON.generate({
  from: "Acme <hello@mail.acme.com>",
  to: "ada@example.com",
  template_slug: "welcome",
  variables: {
    name: "Ada",
    plan: "Growth"
  }
})

res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }
puts res.code, res.body
```

**PHP**

```php
<?php

require "vendor/autoload.php";

$client = new GuzzleHttp\Client(["base_uri" => "https://api.avelto.dev"]);

$res = $client->request("POST", "/v1/emails", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("AVELTO_API_KEY"),
    ],
    "json" => [
        "from" => "Acme <hello@mail.acme.com>",
        "to" => "ada@example.com",
        "template_slug" => "welcome",
        "variables" => [
            "name" => "Ada",
            "plan" => "Growth"
        ]
    ],
]);

echo $res->getStatusCode(), "\n", $res->getBody();
```

**C#**

```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AVELTO_API_KEY"));

var res = await client.PostAsJsonAsync("https://api.avelto.dev/v1/emails", new
{
    from = "Acme <hello@mail.acme.com>",
    to = "ada@example.com",
    template_slug = "welcome",
    variables = new { name = "Ada", plan = "Growth" }
});
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
```

No `subject`, no `html`, no `text` — the template owns those. Passing one alongside a
template is an error rather than an override, because two places deciding what a message
says is a support question waiting to happen. The reverse holds too: `variables` without
a template is refused.

## The template language, in full

There isn't one. The entire grammar is a name between braces:

| Form | What it does |
| --- | --- |
| `{{name}}` | The value, HTML-escaped in the HTML part |
| `{{{name}}}` | The value exactly as given |

A name is letters, digits and underscores, not starting with a digit; `{{ name }}` with
spaces is fine. Anything else between braces is left as written. String values may be up
to 10,000 characters.

No loops, no conditionals, no partials, no property access, no expressions, no filters.
That is a deliberate ceiling rather than a roadmap: every one of those turns your
template into a program that runs on our servers, and the history of template engines is
a history of that program escaping.

If you need a list of order lines, build the HTML for it yourself and pass it as one
variable with `{{{lines}}}`. You keep the loop, in your own language, where you can test
it.

## Escaping

`{{name}}` is escaped in the HTML part and left alone in the subject and the plain text
part, so the same variable is safe in both. A customer called `Ada <ada@example.com>` renders as text, not
as a broken tag.

`{{{name}}}` skips the escaping. Use it when you are passing markup you built, and not
when you are passing something a user typed.

## Missing variables are an error

A template that refers to `{{name}}` and a send that does not pass one is refused with
`422`, listing every variable that was missing:

```json
{
  "error": {
    "code": "validation_error",
    "message": "The template needs values for these variables: company, name. Pass them in variables.",
    "details": { "missing_variables": ["company", "name"] }
  }
}
```

Not an empty string. "Hi ," sent to a hundred thousand people is worse in every way than
a send that did not happen, and the error names all of them at once so it is one fix
rather than a guessing game.

## Values

Strings, numbers, booleans and `null`. `null` renders as nothing, which is what an absent
middle name should do. Objects and arrays are refused — see the ceiling above.

## Managing templates

`POST /v1/templates` with a `name`, a `subject`, and `html`, `text` or both. The `slug`
is derived from the name unless you set one, and it is what you pass as `template_slug`.
Slugs are lower-case letters, digits and hyphens (`welcome-email`). A derived slug that is
already taken gets a numeric suffix; a slug you set yourself must be free or the request
is `409 conflict`. On a send, name the template with `template_slug` or `template_id`,
not both. An unknown slug or id is `404 not_found`.

```ts

const avelto = new Avelto(process.env.AVELTO_API_KEY);

const template = await avelto.templates.create({
  name: "Welcome",
  subject: "Welcome, {{name}}",
  html: "<p>Hello {{name}}</p>",
});

await avelto.emails.send({
  from: "hello@mail.acme.com",
  to: "ada@example.com",
  template_slug: template.slug,
  variables: { name: "Ada" },
});
```

`GET`, `PATCH` and `DELETE /v1/templates/{id}` do what you expect. A key needs the
`templates:manage` scope for any of them.

## Versions

Every save adds a version; version 1 is the template as you first created it, and
`version` on the template is the current one. `GET /v1/templates/{id}/versions` lists them, and
`POST /v1/templates/{id}/restore` with a `version` puts one back.

Restoring writes the old version forward as a new one rather than rewinding, so the
history is append-only and a restore is itself something you can undo. The mistake you
restored away from is still there.

## What a send keeps

The rendered subject and body are copied onto the email when it is accepted. Editing a
template afterwards does not change what was already sent, and deleting one does not
orphan it — the email log still shows exactly what went out.

## In the dashboard

[Templates](/dashboard/templates) has an editor with the same preview the email log uses,
a panel listing the variables it found in your body, and a button that sends the template
to the account owner's address in test mode so you can look at it in a real mail client.

---

Rendered page: https://avelto.dev/docs/templates
