# 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`. |

## Show-when rules, repeats and event data

A visual template can show a block or a section only to some contacts. Its rule is a list of conditions that must all, or any, hold: a contact field or property (the same `{ field, op, value }` a segment filter uses), a topic subscription, membership of a segment, or a property of the flow's trigger event. A rule that names a deleted property, topic or segment hides the block; the send still goes.

A template made for a flow names its trigger event. It can then print the event's data with `{{{event.order_id}}}` and repeat a group of blocks once per item of a list in it. Inside a repeat, each item is `{{{item.name}}}`. A missing or empty list draws nothing, and a repeat draws at most 50 items.

An event or item value can end with a fallback and a format: `{{{item.total|0|currency:USD}}}`, `{{{event.placed_at|date:short}}}`, `{{{item.quantity|number:0}}}`. Dates take `short`, `medium` or `long` and print in UTC; currencies take a three-letter code; numbers take 0 to 6 decimals. Output is in US English. A format a save cannot read is `422 validation_error`.

A template for a store flow names one of the eight `store:` events as its trigger (`store:checkout.started`, `store:order.placed`, …). Their payload keys are fixed: `value` and `currency`, `checkout_url`, `order_id`, `product_id` and `items`, each item with `title`, `variant`, `quantity`, `price`, `image_url`, `url` and `product_id`. The `cart` block draws the event's items as lines with a subtotal and a button to `checkout_url`, in the event's own currency; with no items it draws nothing at all. The `product` block draws one product picked from the connected store's catalogue, read again when the email is sent so the price and picture are current; the `recommended` block draws up to six products chosen for each recipient when the email is sent: the project's best sellers (what its store events have seen ordered most), products in the same collections as the event's items, or what that person looked at last. Nothing is fetched from the store during a send.

Rules are decided per contact when a campaign or a flow sends. A send through `POST /emails`, a preview and a test have no contact to decide for: they show every block, draw no repeated items and no cart or recommendation, and draw a picked product as it was when it was picked.

## 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.

## Blocks and saved blocks

A template made in the dashboard's visual editor is a layout of sections, each one to three columns, holding blocks: `heading`, `text`, `image`, `button`, `divider`, `spacer`, `social`, `footer`, `card`, `video`, `menu`, `countdown`, `html`, `preheader`, and the three store blocks `cart`, `product` and `recommended`. Rasket compiles the layout into the version's `html` and `text` each time it is saved.

- A `video` is its thumbnail and a caption, both linking to where it plays: no inbox plays video. A `countdown` names its end date in words, and can show a timer picture a countdown service draws each time the email is opened.
- An `html` block is your own markup. Scripts, event handlers, style sheets, `javascript:` links and embeds are removed before it is sent.
- A `preheader` is the line most inboxes show after the subject. An email has one, and it comes first.
- A **saved block** is a header, a footer or any block or section saved once and placed in many templates. Editing it publishes the change to every template whose published version uses it; deleting it leaves each template sending the copy it last had. A template created from a starter wears your saved header and footer.

## Before you send

`POST /templates/{id}/checks` runs the pre-send checks over a version, rendered as the preview renders it: links, variables with no value, the subject and preview text, alt text, image weight, text beside images, and the unsubscribe link. Each is `pass`, `warn` or `fail`. Nothing is gated on a template; a campaign made from one is checked again when it is sent. Links are read, never visited.

## 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}/checks`

The pre-send checks over a version, rendered as the preview renders it. 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, as the preview takes them. The body may be omitted. |
| `version` | integer | The version number to check. The current version when absent. |

Check a template:

```sh
curl -X POST "https://api.rasket.com/templates/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17/checks" \
  -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/checks", {
  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/checks",
    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": "email_checks",
  "checks": [
    {
      "code": "alt_text",
      "status": "warn",
      "title": "Alt text",
      "message": "1 image has no alt text. Readers with images off, or using a screen reader, get nothing in its place."
    },
    {
      "code": "links",
      "status": "pass",
      "title": "Links",
      "message": "2 links, all well-formed. Checked as written, without visiting them."
    }
  ]
}
```

- Advisory: nothing is gated on a template. A campaign made from it is checked again when it is sent.
- A visual template's findings can name the block they are about, in `block_id`.

### `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.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `goal` | string | Only the starters for one goal: `welcome`, `launch`, `newsletter`, `winback`, `receipts`, `event`, `promotion`, `digest` or `basics`. |

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",
      "goal": "basics",
      "kind": "transactional",
      "tags": ["payment", "billing", "plain"],
      "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.
- `goal` is what a starter is for. `kind` says how a copy is usually sent: `campaign`, `flow` or `transactional`.
- 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",
  "goal": "basics",
  "kind": "transactional",
  "tags": ["payment", "billing", "plain"],
  "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`.

### `POST /saved-blocks`

A header, a footer or any block your visual templates place by reference.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | At most 100 characters. |
| `kind` (required) | string | `header`, `footer` or `block`. A header and a footer are whole sections, and a project has one of each. |
| `document` (required) | object | The block or section, exactly as the visual editor writes it: a `section`, or one of the content blocks (`heading`, `text`, `image`, `button`, `divider`, `spacer`, `social`, `footer`, `card`, `video`, `menu`, `countdown`, `html`). It may not place another saved block or hold a `preheader`. |

Create a saved block:

```sh
curl -X POST "https://api.rasket.com/saved-blocks" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Shared footer",
  "kind": "footer",
  "document": {
    "id": "s-footer",
    "type": "section",
    "layout": "1",
    "style": {
      "background_color": null,
      "padding_top": 16,
      "padding_bottom": 24,
      "full_width": false
    },
    "columns": [
      {
        "id": "c-footer",
        "type": "column",
        "style": {
          "vertical_align": "top"
        },
        "blocks": [
          {
            "id": "b-footer",
            "type": "footer",
            "company_name": true,
            "address": true,
            "footer_text": true,
            "unsubscribe_label": "Unsubscribe",
            "style": {
              "align": "center",
              "color": "#6b7280",
              "font_family": "{{brand.font}}",
              "font_size": 12,
              "line_height": 1.5,
              "padding_x": 24,
              "padding_y": 16,
              "link_color": "#6b7280"
            }
          }
        ]
      }
    ]
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/saved-blocks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Shared footer",
    kind: "footer",
    document: {
      id: "s-footer",
      type: "section",
      layout: "1",
      style: {
        background_color: null,
        padding_top: 16,
        padding_bottom: 24,
        full_width: false
      },
      columns: [
        {
          id: "c-footer",
          type: "column",
          style: {
            vertical_align: "top"
          },
          blocks: [
            {
              id: "b-footer",
              type: "footer",
              company_name: true,
              address: true,
              footer_text: true,
              unsubscribe_label: "Unsubscribe",
              style: {
                align: "center",
                color: "#6b7280",
                font_family: "{{brand.font}}",
                font_size: 12,
                line_height: 1.5,
                padding_x: 24,
                padding_y: 16,
                link_color: "#6b7280"
              }
            }
          ]
        }
      ]
    }
  }),
});

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

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/saved-blocks",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "Shared footer",
    "kind": "footer",
    "document": {
      "id": "s-footer",
      "type": "section",
      "layout": "1",
      "style": {
        "background_color": None,
        "padding_top": 16,
        "padding_bottom": 24,
        "full_width": False
      },
      "columns": [
        {
          "id": "c-footer",
          "type": "column",
          "style": {
            "vertical_align": "top"
          },
          "blocks": [
            {
              "id": "b-footer",
              "type": "footer",
              "company_name": True,
              "address": True,
              "footer_text": True,
              "unsubscribe_label": "Unsubscribe",
              "style": {
                "align": "center",
                "color": "#6b7280",
                "font_family": "{{brand.font}}",
                "font_size": 12,
                "line_height": 1.5,
                "padding_x": 24,
                "padding_y": 16,
                "link_color": "#6b7280"
              }
            }
          ]
        }
      ]
    }
  },
)

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

#### Response `201`

```json
{
  "object": "saved_block",
  "id": "0192f6f0-5c3a-7b1e-9d2a-4f1e8c7b6a01",
  "name": "Shared footer",
  "kind": "footer",
  "shape": "section",
  "document": {
    "id": "s-footer",
    "type": "section",
    "layout": "1",
    "style": {
      "background_color": null,
      "padding_top": 16,
      "padding_bottom": 24,
      "full_width": false
    },
    "columns": [
      {
        "id": "c-footer",
        "type": "column",
        "style": {
          "vertical_align": "top"
        },
        "blocks": [
          {
            "id": "b-footer",
            "type": "footer",
            "company_name": true,
            "address": true,
            "footer_text": true,
            "unsubscribe_label": "Unsubscribe",
            "style": {
              "align": "center",
              "color": "#6b7280",
              "font_family": "{{brand.font}}",
              "font_size": 12,
              "line_height": 1.5,
              "padding_x": 24,
              "padding_y": 16,
              "link_color": "#6b7280"
            }
          }
        ]
      }
    ]
  },
  "used_in": 0,
  "created_at": "2026-09-11T12:00:00.000Z",
  "updated_at": "2026-09-11T12:00:00.000Z"
}
```

- A second live `header` or `footer` is `422 invalid_parameter`: edit the one you have, or delete it first.
- Once you have a header or a footer, a template created from a starter wears yours instead of its own.
- In the dashboard, select a block or a section and save it from the editor's Saved blocks tab.

### `GET /saved-blocks`

Every saved block, most recently edited first.

List saved blocks:

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

```ts
const response = await fetch("https://api.rasket.com/saved-blocks", {
  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/saved-blocks",
    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": "saved_block",
      "id": "0192f6f0-5c3a-7b1e-9d2a-4f1e8c7b6a01",
      "name": "Shared footer",
      "kind": "footer",
      "shape": "section",
      "document": {
        "id": "s-footer",
        "type": "section",
        "layout": "1",
        "style": {
          "background_color": null,
          "padding_top": 16,
          "padding_bottom": 24,
          "full_width": false
        },
        "columns": [
          {
            "id": "c-footer",
            "type": "column",
            "style": {
              "vertical_align": "top"
            },
            "blocks": [
              {
                "id": "b-footer",
                "type": "footer",
                "company_name": true,
                "address": true,
                "footer_text": true,
                "unsubscribe_label": "Unsubscribe",
                "style": {
                  "align": "center",
                  "color": "#6b7280",
                  "font_family": "{{brand.font}}",
                  "font_size": 12,
                  "line_height": 1.5,
                  "padding_x": 24,
                  "padding_y": 16,
                  "link_color": "#6b7280"
                }
              }
            ]
          }
        ]
      },
      "used_in": 4,
      "created_at": "2026-09-11T12:00:00.000Z",
      "updated_at": "2026-09-11T12:00:00.000Z"
    }
  ]
}
```

- `used_in` counts the live templates whose current or published version places the block.
- It is not paginated.

### `GET /saved-blocks/{id}`

One saved block, with its content.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The saved block's ID. |

Retrieve a saved block:

```sh
curl -X GET "https://api.rasket.com/saved-blocks/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/saved-blocks/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/saved-blocks/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": "saved_block",
  "id": "0192f6f0-5c3a-7b1e-9d2a-4f1e8c7b6a01",
  "name": "Shared footer",
  "kind": "footer",
  "shape": "section",
  "document": {
    "id": "s-footer",
    "type": "section",
    "layout": "1",
    "style": {
      "background_color": null,
      "padding_top": 16,
      "padding_bottom": 24,
      "full_width": false
    },
    "columns": [
      {
        "id": "c-footer",
        "type": "column",
        "style": {
          "vertical_align": "top"
        },
        "blocks": [
          {
            "id": "b-footer",
            "type": "footer",
            "company_name": true,
            "address": true,
            "footer_text": true,
            "unsubscribe_label": "Unsubscribe",
            "style": {
              "align": "center",
              "color": "#6b7280",
              "font_family": "{{brand.font}}",
              "font_size": 12,
              "line_height": 1.5,
              "padding_x": 24,
              "padding_y": 16,
              "link_color": "#6b7280"
            }
          }
        ]
      }
    ]
  },
  "used_in": 4,
  "created_at": "2026-09-11T12:00:00.000Z",
  "updated_at": "2026-09-11T12:00:00.000Z"
}
```

### `PATCH /saved-blocks/{id}`

Rename it or change its content, and publish the change to the templates that use it.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The saved block's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | At most 100 characters. |
| `document` | object | The block or section, exactly as the visual editor writes it: a `section`, or one of the content blocks (`heading`, `text`, `image`, `button`, `divider`, `spacer`, `social`, `footer`, `card`, `video`, `menu`, `countdown`, `html`). It may not place another saved block or hold a `preheader`. A section stays a section, and a block a block. |

Update a saved block:

```sh
curl -X PATCH "https://api.rasket.com/saved-blocks/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "document": {
    "id": "s-footer",
    "type": "section",
    "layout": "1",
    "style": {
      "background_color": null,
      "padding_top": 16,
      "padding_bottom": 24,
      "full_width": false
    },
    "columns": [
      {
        "id": "c-footer",
        "type": "column",
        "style": {
          "vertical_align": "top"
        },
        "blocks": [
          {
            "id": "b-footer",
            "type": "footer",
            "company_name": true,
            "address": true,
            "footer_text": true,
            "unsubscribe_label": "Unsubscribe",
            "style": {
              "align": "center",
              "color": "#6b7280",
              "font_family": "{{brand.font}}",
              "font_size": 12,
              "line_height": 1.5,
              "padding_x": 24,
              "padding_y": 16,
              "link_color": "#6b7280"
            }
          }
        ]
      }
    ]
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/saved-blocks/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({
    document: {
      id: "s-footer",
      type: "section",
      layout: "1",
      style: {
        background_color: null,
        padding_top: 16,
        padding_bottom: 24,
        full_width: false
      },
      columns: [
        {
          id: "c-footer",
          type: "column",
          style: {
            vertical_align: "top"
          },
          blocks: [
            {
              id: "b-footer",
              type: "footer",
              company_name: true,
              address: true,
              footer_text: true,
              unsubscribe_label: "Unsubscribe",
              style: {
                align: "center",
                color: "#6b7280",
                font_family: "{{brand.font}}",
                font_size: 12,
                line_height: 1.5,
                padding_x: 24,
                padding_y: 16,
                link_color: "#6b7280"
              }
            }
          ]
        }
      ]
    }
  }),
});

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

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/saved-blocks/0199d2f1-4c4e-7a20-9c31-6f2b8a0e5d17",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "document": {
      "id": "s-footer",
      "type": "section",
      "layout": "1",
      "style": {
        "background_color": None,
        "padding_top": 16,
        "padding_bottom": 24,
        "full_width": False
      },
      "columns": [
        {
          "id": "c-footer",
          "type": "column",
          "style": {
            "vertical_align": "top"
          },
          "blocks": [
            {
              "id": "b-footer",
              "type": "footer",
              "company_name": True,
              "address": True,
              "footer_text": True,
              "unsubscribe_label": "Unsubscribe",
              "style": {
                "align": "center",
                "color": "#6b7280",
                "font_family": "{{brand.font}}",
                "font_size": 12,
                "line_height": 1.5,
                "padding_x": 24,
                "padding_y": 16,
                "link_color": "#6b7280"
              }
            }
          ]
        }
      ]
    }
  },
)

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

#### Response `200`

```json
{
  "object": "saved_block",
  "id": "0192f6f0-5c3a-7b1e-9d2a-4f1e8c7b6a01",
  "name": "Shared footer",
  "kind": "footer",
  "shape": "section",
  "document": {
    "id": "s-footer",
    "type": "section",
    "layout": "1",
    "style": {
      "background_color": null,
      "padding_top": 16,
      "padding_bottom": 24,
      "full_width": false
    },
    "columns": [
      {
        "id": "c-footer",
        "type": "column",
        "style": {
          "vertical_align": "top"
        },
        "blocks": [
          {
            "id": "b-footer",
            "type": "footer",
            "company_name": true,
            "address": true,
            "footer_text": true,
            "unsubscribe_label": "Unsubscribe",
            "style": {
              "align": "center",
              "color": "#6b7280",
              "font_family": "{{brand.font}}",
              "font_size": 12,
              "line_height": 1.5,
              "padding_x": 24,
              "padding_y": 16,
              "link_color": "#6b7280"
            }
          }
        ]
      }
    ]
  },
  "used_in": 4,
  "created_at": "2026-09-11T12:00:00.000Z",
  "updated_at": "2026-09-11T12:00:00.000Z",
  "templates_published": 3,
  "templates_skipped": []
}
```

- A new `document` reaches what is sent: every live visual template whose published version places the block gets a new version carrying it, compiled and published. `templates_published` counts them.
- A template with an unpublished draft keeps the draft as it is; the draft picks the change up the next time it is saved.
- A template that cannot take the change is named in `templates_skipped` with the reason, and picks it up when it is next saved.
- The `kind` never changes.

### `DELETE /saved-blocks/{id}`

Templates that use it keep sending the copy they last compiled.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `id` (required) | string | The saved block's ID. |

Delete a saved block:

```sh
curl -X DELETE "https://api.rasket.com/saved-blocks/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/saved-blocks/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/saved-blocks/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": "saved_block",
  "id": "0192f6f0-5c3a-7b1e-9d2a-4f1e8c7b6a01",
  "deleted": true
}
```

- The dashboard's editor marks each place a deleted saved block was used; detach it there to edit that copy.
- Deleting your header or footer frees the slot for a new one.
