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

# Template endpoints

> REST endpoints for managing WhatsApp templates — list, create, update, delete, submit, and seed the starter pack.

Switchbord exposes a small set of authenticated endpoints under `/api/templates` in the control-plane app (`apps/app`) for managing the workspace's WhatsApp template library. All routes require a signed-in operator with **settings access** (`owner`, `admin`, or `developer`); the seed route additionally requires `owner` or `admin`.

Requests are scoped to the caller's active workspace — you cannot see or mutate templates from another workspace. Auth errors return `401 unauthenticated` or `403 forbidden`.

## Summary

| Method | Path                               | Purpose                              | Role required   |
| ------ | ---------------------------------- | ------------------------------------ | --------------- |
| GET    | `/api/templates`                   | List all templates in the workspace  | settings access |
| POST   | `/api/templates`                   | Create a draft template              | settings access |
| GET    | `/api/templates/{id}`              | Fetch one template                   | settings access |
| PUT    | `/api/templates/{id}`              | Update a draft template              | settings access |
| DELETE | `/api/templates/{id}`              | Delete a draft (only `draft` status) | settings access |
| POST   | `/api/templates/{id}/submit`       | Submit a draft to Meta for approval  | settings access |
| POST   | `/api/templates/seed-starter-pack` | Seed the GB Viaggi starter pack      | owner or admin  |

***

## GET /api/templates

List every template for the active workspace, sorted by most recently updated.

**Response 200**

```json theme={null}
{
  "templates": [
    {
      "id": "b3d2b8f1-...",
      "name": "booking_confirmed",
      "language": "it",
      "category": "UTILITY",
      "status": "approved",
      "parameterFormat": "POSITIONAL",
      "components": [ ... ],
      "variables": ["var1", "var2"],
      "preview": "Prenotazione confermata per var1 a var2.",
      "metaTemplateId": "123456789",
      "rejectionReason": null,
      "updatedAt": "2026-04-22T10:15:00Z"
    }
  ]
}
```

**Errors** — `401 unauthenticated`, `403 forbidden`, `500 internal_error`.

***

## POST /api/templates

Create a new template draft. The template is stored with `status = "draft"` and is **not** submitted to Meta. Use `POST /api/templates/{id}/submit` after the draft is ready.

**Request body**

```json theme={null}
{
  "name": "winback_30d",
  "language": "it",
  "category": "MARKETING",
  "parameterFormat": "POSITIONAL",
  "components": [
    { "type": "BODY",
      "text": "Ciao {{1}}, è passato un po' dall'ultima volta...",
      "example": { "body_text": [["Marco"]] } }
  ]
}
```

Fields:

* `name` (string, required) — lowercase and non-matching characters are stripped to `_`. See [name-regex](/whatsapp/template-rules#name-regex).
* `language` (string, default `"en_US"`) — Meta locale code, e.g. `"it"`, `"en_US"`, `"pt_BR"`.
* `category` (`"MARKETING" | "UTILITY" | "AUTHENTICATION"`, default `"UTILITY"`).
* `parameterFormat` (`"POSITIONAL" | "NAMED"`, default `"POSITIONAL"`).
* `components` (array) — the component shape from Meta's schema.

**Response 200**

```json theme={null}
{ "template": { "id": "...", "status": "draft", ... } }
```

**Errors** — `400 create_failed` with `message` describing the DB or validation error, plus auth errors.

***

## GET /api/templates/{id}

Fetch a single template by id. Returns `404 not_found` if the id does not exist in the caller's workspace.

**Response 200** — same shape as a list entry.

***

## PUT /api/templates/{id}

Update one or more fields on a draft template. Sent fields are patched; unsent fields are left untouched.

**Request body** (all fields optional)

```json theme={null}
{
  "name": "winback_30d_v2",
  "language": "it",
  "category": "MARKETING",
  "parameterFormat": "POSITIONAL",
  "components": [ ... ]
}
```

**Response 200**

```json theme={null}
{ "template": { ... } }
```

**Errors** — `400 update_failed`, auth errors.

***

## DELETE /api/templates/{id}

Delete a draft template. **Only templates in `status = "draft"`** can be deleted — once submitted, templates are retained for audit. A non-draft template returns `409 conflict`.

**Response 200**

```json theme={null}
{ "ok": true }
```

**Errors** — `404 not_found`, `409 conflict` with `"Only draft templates can be deleted"`, auth errors.

***

## POST /api/templates/{id}/submit

Submit a draft template to Meta's `POST /{waba_id}/message_templates` endpoint. Switchbord first runs the full client-side validator (the same rules documented in [Template rules](/whatsapp/template-rules)); if any check fails, no call is made to Meta and a `400 validation_failed` is returned with the failing issues array.

On success, the template row is updated with `meta_template_id`, `status = "pending_approval"`, and `rejection_reason = null`.

On Meta rejection (HTTP 4xx with an `error` body), the row is updated with `status = "rejected"` and `rejection_reason` = Meta's human-readable message, and a `400 meta_submit_failed` is returned to the caller with the full Meta error payload.

**Response 200**

```json theme={null}
{ "template": { "id": "...", "status": "pending_approval", "metaTemplateId": "123456789" } }
```

**Errors**

* `400 validation_failed` — client-side validator rejected the draft. `issues` is the Zod issue array.
* `400 meta_submit_failed` — Meta returned an error. `meta` is the raw Meta `error` object (code, subcode, `error_user_msg`, `error_data.details`).
* `400 no_waba` — no WhatsApp Business Account configured for the workspace.
* `400 no_access_token` — no Meta access token in the workspace vault.
* `404 not_found`, auth errors.

***

## POST /api/templates/seed-starter-pack

Admin-only one-shot seeder that submits the GB Viaggi starter pack (8 `UTILITY` + 5 `MARKETING`, Italian + English where applicable) to Meta and upserts the matching rows in the local `templates` table. Safe to re-run — the upsert key is `(workspace_id, name, locale)`.

Required role: **owner** or **admin**.

**Request body** — empty.

**Response 200**

```json theme={null}
{
  "total": 13,
  "submitted": 11,
  "errored": 2,
  "results": [
    {
      "name": "booking_confirmed",
      "language": "it",
      "category": "UTILITY",
      "status": "submitted",
      "metaTemplateId": "987654321"
    },
    {
      "name": "flash_sale",
      "language": "it",
      "category": "MARKETING",
      "status": "error",
      "error": "Variable parameters in the body text cannot be next to each other."
    }
  ]
}
```

Per-entry `status`:

* `submitted` — accepted by Meta, row stored as `pending_approval`.
* `error` — Meta or DB error, row stored as `rejected` with the Meta message.
* `skipped` — reserved; not currently emitted.

**Errors**

* `400 no_waba` — no WhatsApp Business Account configured.
* `400 no_access_token` — vault secret `meta-access-token` missing.
* `500 internal_error` — unhandled failure.
* `401 unauthenticated`, `403 forbidden` — caller lacks owner/admin role.

***

## Related internal endpoints

Two internal, non-public routes exist in `apps/api` for machine-to-machine sync flows:

* `POST /internal/templates` — worker-to-API template upsert after a webhook event.
* `POST /internal/templates/sync` — full-library resync from Meta back to Switchbord.

These are not exposed publicly and should not be called from the frontend. See [Internal API spec](/api-reference/internal-spec) for the authoritative list.

## OpenAPI

The machine-readable shape for these endpoints will be generated into `apps/docs/api-reference/openapi.json` by `pnpm openapi:generate`. Run the generator after changing a route to keep the spec in sync.
