# Templates

Reusable email content with typed variables. Write it once, publish it, and send it by ID or alias with the values filled in.

## Versions

A template is a name, an optional alias, and a series of versions of its content — `from`, `subject`, `reply_to`, `html`, `text` and the variables they use. Creating a template writes version 1 as a draft. `publish` makes the current version the one sends use; updating a published template opens the next draft without changing what is sent, until you publish again.

- Retrieving a template shows the **current** version. When that is an open draft, `has_unpublished_versions` is `true`.
- A send records the exact version it used, so a later edit never changes what an already-sent email said.
- Only a published template can be sent. Naming a draft in `POST /emails` is `422 invalid_parameter`.
- A send may leave out `from` and `subject` when the published version sets them. Values in the request win. If neither has one, the send is `422 missing_required_field`.

## Variables and placeholders

A placeholder is `{{key}}` — or `{{ key }}` — with the key matching `^[A-Za-z0-9_]+$`. There is no logic, no filter and no nesting: a value that contains `{{other}}` is inserted as that literal text, never expanded. Every placeholder the bodies use must be declared in `variables`, and every declared variable must be used, so an editor can point at exactly what is wrong.

Sends supply values as strings or numbers. A value is checked against the declared type; a key the send omits takes the variable's `fallback_value`, and a declared key with neither is `422 missing_required_field`.

| Type | Rendered as |
| --- | --- |
| `string` | Inserted as written: HTML-escaped in `html`, raw in `subject` and `text`. |
| `number` | Written out as a number. |
| `boolean` | `true` or `false`. |
| `object` | Compact JSON, HTML-escaped in `html`. |
| `list` | Compact JSON, HTML-escaped in `html`. |

## Addressing a template

- Every `{id}` below accepts the template's UUID or its alias. An alias is lower-case, may not be a UUID, and is unique among the team's live templates.
- Reading and listing work with either kind of key. Creating, updating, publishing, duplicating and deleting need a `full_access` key; a `sending_access` key gets `401 restricted_api_key`.
- A template in another team is `404 not_found`, indistinguishable from one that does not exist.

## Endpoints

### `POST /templates`

A new draft, holding version 1 of its content.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | A name for the dashboard. |
| `alias` | string | A short name to address the template by instead of its ID: lower-case letters, digits, `-` and `_`, starting with a letter or digit, at most 63 characters, and never a UUID. Unique among the team's templates. |
| `from` | string | The sender, as `Name <address@domain>` or a bare address. A send that names its own `from` overrides it. |
| `subject` | string | The subject line. May carry `{{key}}` placeholders. |
| `html` | string | The HTML body. May carry `{{key}}` placeholders. On create, send this or `starter` — exactly one. |
| `text` | string | The plain-text body. May carry `{{key}}` placeholders. |
| `variables` | object[] | The variables the bodies use, at most 200: `{ key, type, fallback_value }`, with `type` one of `string`, `number`, `boolean`, `object` or `list`. Every `{{key}}` must be declared here, and every declared key must be used. |
| `starter` | string | The slug of a starter template to copy instead of writing `html`. Its subject, layout and variables become version 1: the copy opens in the dashboard's visual editor, and its HTML and plain text are compiled from the layout. Send this or `html`, never both. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `reply_to` | string[] | Reply-to addresses. |
| `brand_overrides` | object \| null | This template's own values for your brand fields, for a one-off that should not wear the project brand: `logo_url`, `primary_color`, `accent_color`, `font`, `button_style`, `company_name`, `address`, `footer_text`. A field you leave out follows the project brand; a field set to `null` uses our plain default even when your brand sets one. `null` for the whole object means this template pins nothing. |

Create a template:

```sh
curl -X POST "https://api.rasket.com/templates" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Welcome",
  "alias": "welcome",
  "subject": "Welcome, {{name}}",
  "html": "<p>Hello {{name}}, thanks for joining.</p>",
  "variables": [
    {
      "key": "name",
      "type": "string",
      "fallback_value": "there"
    }
  ]
}'
```

```ts
const response = await fetch("https://api.rasket.com/templates", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Welcome",
    alias: "welcome",
    subject: "Welcome, {{name}}",
    html: "<p>Hello {{name}}, thanks for joining.</p>",
    variables: [
      {
        key: "name",
        type: "string",
        fallback_value: "there"
      }
    ]
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/templates",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "Welcome",
    "alias": "welcome",
    "subject": "Welcome, {{name}}",
    "html": "<p>Hello {{name}}, thanks for joining.</p>",
    "variables": [
      {
        "key": "name",
        "type": "string",
        "fallback_value": "there"
      }
    ]
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "object": "template"
}
```

- A new template is a draft. Nothing can be sent from it until you publish it.
- Send exactly one of `html` and `starter`. Neither is `422 missing_required_field`; both is `422 invalid_parameter`. The schema cannot express "exactly one", so a client will not catch it for you.
- `starter` is one of the slugs `GET /starters` lists. Any other value is `400 validation_error`.
- With `starter`, anything else you send wins over the starter's: `subject`, `text` and `variables` override it, and `alias`, `from` and `reply_to` are yours to set. A `text` you send is kept as the plain text instead of the one generated from the layout. The copy keeps no link to the library, so improving a starter never changes it.
- This route takes no `Idempotency-Key`. A retry with no `alias` creates a second template; set an alias, or check the list first.
- A `{{key}}` the bodies use but `variables` does not declare — or a declared key the bodies never use — is `422 validation_error`, with one entry in `errors[]` per key.
- An alias another live template holds is `422 validation_error`. Deleting a template frees its alias.
- `brand_overrides` is not a send variable and declares nothing: a brand field is your project's and a `{{key}}` is the recipient's, so the declaration rule never sees one.
- An unknown field name inside `brand_overrides` is `400 validation_error` rather than ignored, and a `font` or `button_style` outside its list is too. Everything else it refuses is `422 invalid_parameter`.

### `GET /templates`

Every template, most recently changed first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `search` | string | Only templates whose name or alias contains this, ignoring case. At most 200 characters. |
| `status` | string | Only templates in this status: `draft` or `published`. |

List templates:

```sh
curl -X GET "https://api.rasket.com/templates?limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/templates?limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/templates?limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
      "name": "Welcome",
      "status": "published",
      "published_at": "2026-09-11T12:00:00.000Z",
      "created_at": "2026-09-11T12:00:00.000Z",
      "updated_at": "2026-09-11T12:00:00.000Z",
      "alias": "welcome"
    }
  ]
}
```

- Items carry no content. Retrieve one template for its bodies and variables.
- `alias` is present only on templates that have one.

### `GET /templates/{id}`

By ID or by alias, with the current version's content.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

Retrieve a template:

```sh
curl -X GET "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "template",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "current_version_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d18",
  "name": "Welcome",
  "alias": "welcome",
  "from": "Acme <hello@acme.com>",
  "subject": "Welcome, {{name}}",
  "reply_to": null,
  "html": "<p>Hello {{name}}, thanks for joining.</p>",
  "text": "Hello {{name}}, thanks for joining.",
  "variables": [
    {
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d19",
      "key": "name",
      "type": "string",
      "fallback_value": "there",
      "created_at": "2026-09-11T12:00:00.000Z",
      "updated_at": "2026-09-11T12:00:00.000Z"
    }
  ],
  "brand_overrides": null,
  "created_at": "2026-09-11T12:00:00.000Z",
  "updated_at": "2026-09-11T12:00:00.000Z",
  "status": "published",
  "published_at": "2026-09-11T12:00:00.000Z",
  "has_unpublished_versions": false
}
```

- The content shown is the **current** version — the open draft, if there is one. `has_unpublished_versions: true` means it differs from what sends use.
- `alias`, `from`, `subject` and `text` are absent, not `null`, when unset.
- `brand_overrides` is `null` when the template pins nothing, and carries only the fields it does pin — the rest come from your project brand.
- A template that belongs to another team, or that has been deleted, is `404 not_found`.

### `PATCH /templates/{id}`

Write the draft. Sends keep using the published version.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | A new name. |
| `alias` | string \| null | A new alias, or `null` to remove the current one. |
| `from` | string \| null | The sender, as `Name <address@domain>` or a bare address. A send that names its own `from` overrides it. `null` clears it. |
| `subject` | string \| null | The subject line. May carry `{{key}}` placeholders. `null` clears it. |
| `html` | string | The HTML body. May carry `{{key}}` placeholders. On create, send this or `starter` — exactly one. |
| `text` | string | The plain-text body. May carry `{{key}}` placeholders. |
| `variables` | object[] | The variables the bodies use, at most 200: `{ key, type, fallback_value }`, with `type` one of `string`, `number`, `boolean`, `object` or `list`. Every `{{key}}` must be declared here, and every declared key must be used. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `reply_to` | string[] | Reply-to addresses. |
| `brand_overrides` | object \| null | This template's own values for your brand fields, for a one-off that should not wear the project brand: `logo_url`, `primary_color`, `accent_color`, `font`, `button_style`, `company_name`, `address`, `footer_text`. A field you leave out follows the project brand; a field set to `null` uses our plain default even when your brand sets one. `null` for the whole object means this template pins nothing. |

Update a template:

```sh
curl -X PATCH "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "subject": "Welcome aboard, {{name}}"
}'
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    subject: "Welcome aboard, {{name}}"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "subject": "Welcome aboard, {{name}}"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "object": "template"
}
```

- On a published template with no open draft, the first update starts a new version; later updates edit that draft in place. Publish it when it is ready.
- Only the fields you send change. The declaration rule applies to the merged result: the bodies after your change must still declare and use exactly the same keys as `variables`.
- An empty body is `422 invalid_parameter`.
- `brand_overrides` replaces what the template pinned rather than merging into it: send the whole set you want, or `null` to stop pinning anything. A per-field merge could not tell "stop pinning the accent colour" from "leave the accent colour alone".
- On a template built in the dashboard's visual editor — which every copy of a starter is — an `html` that differs from the stored one switches it to code editing: the update starts a new version holding your HTML and no layout — even over an open draft — and keeps the plain text unless you send `text` too. Sending back the unchanged `html` switches nothing, and restoring an earlier version from the history brings the visual editor back.

### `DELETE /templates/{id}`

Remove it and free its alias.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

Delete a template:

```sh
curl -X DELETE "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "template",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "deleted": true
}
```

- The template answers `404 not_found` from this moment, and a send that names it is `422 invalid_parameter`.
- Emails already sent from it keep their record of which version they used.

### `POST /templates/{id}/publish`

Make the current version the one sends use.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

Publish a template:

```sh
curl -X POST "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/publish" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/publish", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/publish",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "object": "template"
}
```

- Publishing takes effect for the next send. Emails already queued keep the version they resolved.
- Publishing a template whose current version is already published changes nothing and still answers `200`.

### `POST /templates/{id}/duplicate`

A new draft holding a copy of the current version.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

Duplicate a template:

```sh
curl -X POST "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/duplicate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/duplicate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/duplicate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d2a",
  "object": "template"
}
```

- The copy is named `<name> (copy)`, has no alias, and is a draft. The source is untouched.
- What is copied is the current version — the open draft, if there is one — not the published one.

### `POST /templates/{id}/preview`

Render a version with values filled in, the way a send would. Nothing is stored.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `variables` | object | Values by key, strings or numbers, as a send takes them. An undeclared key or a value of the wrong type is `422 invalid_parameter`. |
| `version` | integer | The version number to render. The current version when absent. |

Preview a template:

```sh
curl -X POST "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/preview" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "variables": {
    "name": "Ronald"
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/preview", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    variables: {
      name: "Ronald"
    }
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/preview",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "variables": {
      "name": "Ronald"
    }
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "template_preview",
  "template_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "version_number": 1,
  "subject": "Welcome, Ronald",
  "html": "<p>Hello Ronald, thanks for joining.</p>",
  "text": "Hello Ronald, thanks for joining.",
  "missing_variables": []
}
```

- A declared variable with neither a value nor a fallback is listed in `missing_variables`, and its placeholder is left in place.
- `html` has been sanitised, with remote images withheld. Show it in a sandboxed iframe, never straight in a page.

### `POST /templates/{id}/test`

Send the current version to up to five inboxes.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

#### Headers, less common

| Field | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | string | Retries with the same key send the test once. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `to` (required) | string[] | One to five addresses. |
| `from` | string | The sender for the test. Needed when the template has none. |
| `variables` | object | Values by key, strings or numbers. A variable you leave out uses its fallback; one without a fallback must be given. |

Send a test of a template:

```sh
curl -X POST "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/test" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "to": ["you@acme.example"],
  "variables": {
    "name": "Ronald"
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/test", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: ["you@acme.example"],
    variables: {
      name: "Ronald"
    }
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/test",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "to": ["you@acme.example"],
    "variables": {
      "name": "Ronald"
    }
  },
)

print(response.json())
```

#### Response `202`

```json
{
  "object": "template_test",
  "template_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "version_number": 2,
  "data": [
    {
      "id": "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
      "to": "you@acme.example"
    }
  ]
}
```

- The current version is sent: the open draft, if there is one. The subject starts with `[Test] `.
- Each test counts as a send. The token also needs permission to send email.

### `GET /templates/{id}/versions`

Every version of a template, newest first.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List template versions:

```sh
curl -X GET "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/versions?limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/versions?limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/versions?limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "template_version",
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d18",
      "version_number": 1,
      "current": true,
      "published": true,
      "published_at": "2026-09-11T12:00:00.000Z",
      "created_at": "2026-09-11T12:00:00.000Z",
      "subject": "Welcome, {{name}}",
      "variables": []
    }
  ]
}
```

- `current` is the version you read and edit; `published` is the one sends use.
- Bodies are left out. Preview a template with `version` to see an older version's content.

### `POST /templates/{id}/versions/{version_number}/restore`

Make a new draft from an earlier version.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The template's ID or its alias. An alias is matched case-insensitively. |
| `version_number` (required) | integer | The version to copy. |

Restore a template version:

```sh
curl -X POST "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/versions/{version_number}/restore" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/versions/{version_number}/restore", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/versions/{version_number}/restore",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "template",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
  "version_number": 3
}
```

- Sends keep using the published version until you publish the new draft.
- An unpublished draft you had open stays in the version list.

### `GET /starters`

The curated library a template can be started from.

List the starter templates:

```sh
curl -X GET "https://api.rasket.com/starters" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/starters", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/starters",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "starter",
      "slug": "receipt",
      "name": "Receipt",
      "summary": "What was paid, for what, and where to find the invoice.",
      "category": "Billing",
      "subject": "Your receipt from {{brand.company_name}}",
      "variables": [
        {
          "key": "amount",
          "type": "string"
        }
      ]
    }
  ]
}
```

- The same catalogue for every project, in the order the dashboard shows it. It is not paginated.
- Items carry no bodies. Retrieve one starter for its HTML and plain text.
- Pass a `slug` to create a template as `starter` to copy it.

### `GET /starters/{slug}`

One starter in full, with both bodies and sample values.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `slug` (required) | string | The starter's slug. |

Retrieve a starter template:

```sh
curl -X GET "https://api.rasket.com/starters/{slug}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/starters/{slug}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/starters/{slug}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "starter",
  "slug": "receipt",
  "name": "Receipt",
  "summary": "What was paid, for what, and where to find the invoice.",
  "category": "Billing",
  "subject": "Your receipt from {{brand.company_name}}",
  "variables": [
    {
      "key": "amount",
      "type": "string"
    }
  ],
  "html": "<p>You paid {{amount}}.</p>",
  "text": "You paid {{amount}}.",
  "sample_variables": {
    "amount": "$42.00"
  }
}
```

- `{{brand.*}}` is your project's brand settings and resolves when the copy is rendered; `{{key}}` is a send variable and a copy declares every one of them.
- `sample_variables` is for previewing only. It is never sent and never stored.
- A slug that is not one of ours is `404 not_found`.
