# Templates and assets

Reply templates, the render step that fills them, folders, and workspace assets.

Source: https://docs.leadarc.io/reference-templates.html

- [GET /v1/workspaces/{ws}/templates](#get-v1-workspaces-ws-templates)
- [GET /v1/workspaces/{ws}/templates/{id}](#get-v1-workspaces-ws-templates-id)
- [POST /v1/workspaces/{ws}/templates/{id}/render](#post-v1-workspaces-ws-templates-id-render)
- [GET /v1/workspaces/{ws}/template_folders](#get-v1-workspaces-ws-template-folders)
- [GET /v1/workspaces/{ws}/assets](#get-v1-workspaces-ws-assets)

## GET /v1/workspaces/{ws}/templates

`templates:read` · read bucket · returns list of template

Reply templates

```bash
curl -sS https://api.leadarc.io/v1/workspaces/airpay/templates?limit=25 \
  -H "Authorization: Bearer $LEADARC_API_KEY"
```

**Query parameters.** Anything else is [`400 unsupported_filter`](errors.html#status-400).

`cursor` `folder_id` `limit` `order` `visibility`

**Errors from this endpoint**, on top of the [common ones](errors.html): [`invalid_cursor`](errors.html#status-400), [`not_found`](errors.html#status-404).

```json
{
  "object": "list",
  "data": [{
    "object": "template",
    "id": "tpl_RkPo3VxECLfgHxyPBBBi5GTDYNiRwMWrLWUEo_",
    "workspace": "airpay",
    "scope": "workspace",
    "name": "Video sent, follow-up 1",
    "subject": "Re: {{first_name}}, the walkthrough",
    "body": "Hi {{first_name}},\n\nHere is that walkthrough for {{company}}: {{asset:loom}}\n\n{{signature}}",
    "is_html": false,
    "visibility": "workspace",
    "folder_id": "tfd_ow492XVxBkyviOTWP5Taa4TGc-SbTMw48fTGOk",
    "order_index": 1,
    "variables": ["first_name", "company", "asset:loom", "signature"],
    "ai_variables": [{ "name": "PAIN_POINT", "read_website": true }],
    "created_by_id": "usr_zaDRRpdu8EPZSU3i89lvZgKPkVQo4uBFA_KOhm",
    "created_at": "2026-06-14T11:00:00Z"
  }],
  "has_more": false,
  "next_cursor": null
}
```
**Notes.**

First, `body` is the raw template with unresolved `{{tokens}}`. **Never send it as an email.** Call `/render` first. This is the single most likely way an agent damages a client, and the whole reason `/render` exists.

Second, `scope` is a new field, and it exists because of a real cross-tenant quirk. `visibleWhere` in the app returns every template with `visibility = "global"` from **any** workspace, with no `client_id` bound on that branch. So a list under `/workspaces/airpay/templates` legitimately includes rows whose own `client_id` is a different workspace. Rather than hide that, the API adds `scope`: `"workspace"` when `reply_templates.client_id` equals the requested workspace, and `"global"` when the row is shared across all of them. A global template's [`workspace`](objects.html#workspace) field is `null`, so the nested-path invariant is never quietly broken.

`variables` is derived by scanning the body against the app. Three variables (`cal_link`, `one_pager`, `quarter`) are retired from the insert menus but still resolve, so they can appear here on legacy templates.

`ai_variables` exposes `name` and `read_website` only. **`reply_templates.ai_vars[].instructions` is withheld.** It is free-text model instructions, which is internal copy and a prompt-injection surface.

`reply_templates.group` is withheld. It is a legacy bucketing column superseded by `folder_id`.

## GET /v1/workspaces/{ws}/templates/{id}

`templates:read` · read bucket · returns template

One template

```bash
curl -sS https://api.leadarc.io/v1/workspaces/airpay/templates/tpl_RkPo3VxECLfgHxyPBBBi5GTDYNiRwMWrLWUEo_ \
  -H "Authorization: Bearer $LEADARC_API_KEY"
```

**Errors from this endpoint**, on top of the [common ones](errors.html): `not_found`.

## POST /v1/workspaces/{ws}/templates/{id}/render

`templates:read` · read bucket

Fill the variables for a lead. Read this before sending anything.

```bash
curl -sS https://api.leadarc.io/v1/workspaces/airpay/templates/tpl_RkPo3VxECLfgHxyPBBBi5GTDYNiRwMWrLWUEo_/render \
  -X POST \
  -H "Authorization: Bearer $LEADARC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"lead_id":"lead_BP2vnPmKyttNP2OlH_mWL78t1o6nBVC6oVbHny","resolve_ai_variables":"...","overrides":"..."}'
```

**Body fields.** An unknown field is rejected.

`lead_id` `resolve_ai_variables` `overrides`

**Errors from this endpoint**, on top of the [common ones](errors.html): [`invalid_request`](errors.html#status-400), `not_found`, [`unprocessable`](errors.html#status-422).

```json
{
  "lead_id": "lead_BP2vnPmKyttNP2OlH_mWL78t1o6nBVC6oVbHny",
  "resolve_ai_variables": false,
  "overrides": { "PAIN_POINT": "manual claim reconciliation" }
}
```
Response:
```json
{
  "object": "rendered_template",
  "template_id": "tpl_RkPo3VxECLfgHxyPBBBi5GTDYNiRwMWrLWUEo_",
  "lead_id": "lead_BP2vnPmKyttNP2OlH_mWL78t1o6nBVC6oVbHny",
  "subject": "Re: Mary, the walkthrough",
  "body": "Hi Mary,\n\nHere is that walkthrough for Northgate Care: https://www.loom.com/share/abc123\n\nSara Whitfield\nAirPay",
  "is_html": false,
  "resolved": ["first_name", "company", "asset:loom", "signature"],
  "unresolved": [],
  "ready_to_send": true
}
```
`ready_to_send` is `false` whenever `unresolved` is non-empty. In v2, `POST.../leads/{lead}/reply` **rejects any body still containing an unresolved `{{token}}`** with `422 unprocessable` and names the tokens. That is a mechanical precondition, not a warning.

`resolveTemplateBody` in the app is called from client components and from `/api/ai/fill-template`, never from the reply-send route. The send route guards only `{{ai:NAME}}` via `hasUnfilledAiToken`, and the comment at `template-variables.ts:227-231` explains why that guard cannot catch `{{first_name}}`: the colon in `ai:NAME` is not a word character, so the two grammars are deliberately separate. And `resolveTemplateBody`'s own contract is that unknown tokens **and empty values** stay literal.

`resolve_ai_variables: true` is **rejected in v1** with `403 unprocessable`. Resolving an `{{ai:NAME}}` slot calls Anthropic on our account. `/api/ai/fill-template` exists internally with a per-user hourly cap counted out of `action_log` (`AI_FILL_HOURLY_CAP`, default 30), and exposing it through a key needs its own scope and its own spend cap. Use `overrides` to fill AI slots from the caller's own model, which is the right shape anyway.

## GET /v1/workspaces/{ws}/template_folders

`templates:read` · read bucket · returns list of template_folder

Folders with counts

```bash
curl -sS https://api.leadarc.io/v1/workspaces/airpay/template_folders?limit=25 \
  -H "Authorization: Bearer $LEADARC_API_KEY"
```

**Query parameters.** Anything else is `400 unsupported_filter`.

`cursor` `limit` `order`

**Errors from this endpoint**, on top of the [common ones](errors.html): `invalid_cursor`.

## GET /v1/workspaces/{ws}/assets

`assets:read` · read bucket · returns list of asset

Looms, docs, links, snippets

```bash
curl -sS https://api.leadarc.io/v1/workspaces/airpay/assets?limit=25 \
  -H "Authorization: Bearer $LEADARC_API_KEY"
```

**Query parameters.** Anything else is `400 unsupported_filter`.

`cursor` `kind` `limit` `order`

**Errors from this endpoint**, on top of the [common ones](errors.html): `invalid_cursor`.

```json
{
  "object": "asset",
  "id": "ast_fdCtnnliOFHi7Bu9YmN1RhbQBGTym_pixihr4u",
  "workspace": "airpay",
  "kind": "loom",
  "title": "Intake walkthrough (2 min)",
  "url": "https://www.loom.com/share/abc123",
  "body": null,
  "visibility": "workspace",
  "order_index": 0,
  "created_by_id": "usr_zaDRRpdu8EPZSU3i89lvZgKPkVQo4uBFA_KOhm",
  "created_at": "2026-06-14T11:00:00Z"
}
```
`kind` is `loom`, `doc`, `link`, or `text`. `url` carries the link for the first three; `body` carries the snippet for `text` and is null otherwise.

**`client_assets.visibility` has only two values, `private` and `workspace`. There is no `global`**, unlike `reply_templates.visibility` and `followup_sequences.visibility`, which have three. An agent that learns `visibility` on templates and sends `global` to assets gets `422 unprocessable`. `GET /v1/capabilities` calls this out in its `notes` block.

Rows with `visibility: "private"` are returned only when the key's owner created them.

---

---

Base URL `https://api.leadarc.io/v1`. Generated from the API source.
