WhatsApp templates
The catalog of Meta-approved message shapes for writing outside the 24-hour window, and how Vitrina fills each variable.
Download the full API pública projection: openapi.json.
Use WhatsApp templates and Flows explains this resource in prose, with runnable examples.
| Method | Path | What it does |
|---|---|---|
GET | /whatsapp-templates | List WhatsApp templates |
POST | /whatsapp-templates | Submit a WhatsApp template to Meta |
DELETE | /whatsapp-templates/{id} | Delete a WhatsApp template |
GET | /whatsapp-templates/{id} | Fetch one WhatsApp template |
PATCH | /whatsapp-templates/{id} | Update a WhatsApp template’s local settings |
GET | /whatsapp-templates/{id}/analytics | A WhatsApp template’s analytics (Vitrina’s own data) |
POST | /whatsapp-templates/{id}/infer-bindings | Re-run the binder over every slot ("volver a sugerir") |
PATCH | /whatsapp-templates/{id}/param-binding | Set what fills each {{variable}} when Vitrina sends this template |
PATCH | /whatsapp-templates/{id}/param-meta | Set the agent-facing label/example for each {{variable}} |
PATCH | /whatsapp-templates/{id}/usage | Re-file the template under another category |
GET | /whatsapp-templates/analytics/summary | Sends, reads, replies and failures per WhatsApp template |
GET | /whatsapp-templates/categories | List the workspace’s template categories |
POST | /whatsapp-templates/categories | Add a template category |
DELETE | /whatsapp-templates/categories/{id} | Delete a template category |
PATCH | /whatsapp-templates/categories/{id} | Rename a template category or change its visibility |
PUT | /whatsapp-templates/categories/order | Reorder the template categories |
POST | /whatsapp-templates/category-check | Predict whether Meta will reclassify a UTILITY draft as MARKETING |
POST | /whatsapp-templates/header-media | Upload a media-header file (image, video or PDF) |
GET | /whatsapp-templates/readiness | Are the workspace’s WhatsApp templates ready? |
POST | /whatsapp-templates/readiness/retry | Resubmit missing or rejected feature templates |
POST | /whatsapp-templates/sync | Force a reconcile of a channel's templates against Meta |
PATCH | /whatsapp-templates/usage | Re-file several templates under one category |
GET /whatsapp-templates
List WhatsApp templates
Optionally narrowed to one messaging_account_id and/or status. Every template the workspace has created, mirrored from Meta.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
messaging_account_id | query | uuid | no | |
status | query | PENDING \ | APPROVED \ | REJECTED \ |
pickable | query | true \ | false | no |
curl https://api.vitrinadev.com/api/v1/whatsapp-templates \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "UTILITY",
"category_reclassified_at": "2026-09-16T09:00:00.000Z",
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"header_text": null,
"header_media_url": null,
"footer_text": "Autos del Valle",
"params": [
"1",
"2"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": null,
"status": "APPROVED",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": "2026-09-15T18:20:00.000Z",
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /whatsapp-templates
Submit a WhatsApp template to Meta
name is lowercase letters, digits and underscores only (Meta’s constraint); language looks like es, es_CL or en_US. Answers 201 with status: "PENDING" — approval is asynchronous, reconciled by the worker poll or by POST /whatsapp-templates/sync.
A MEDIA header: header_media: \{ format: IMAGE|VIDEO|DOCUMENT, url \} with a public https url (upload the file with POST /whatsapp-templates/header-media). Mutually exclusive with header_text — 422. The url is Meta’s sample AND the template’s default header_media_url: every send that supplies no header_media of its own uses it. param_spec.slots then carries \{ key: "header_media", component: "header", media_format \}.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
messaging_account_id | uuid | yes | |
name | string | yes | mín. 1, máx. 512, patrón ^[a-z0-9_]+$ |
language | string | yes | patrón ^[a-z]{2}(_[A-Z]{2})?$ |
category | MARKETING \ | UTILITY \ | AUTHENTICATION |
body_text | string | yes | mín. 1, máx. 1024 |
header_text | string | null | — | mín. 1, máx. 60 |
header_media | object | null | — | |
footer_text | string | null | — | mín. 1, máx. 60 |
buttons | any[] | — | |
param_meta | object | — | |
usage | string | — | mín. 1, máx. 40, patrón ^[a-z0-9_]+$ |
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-templates \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"footer_text": "Autos del Valle"
}'Example response (201)
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "MARKETING",
"category_reclassified_at": null,
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"header_text": null,
"header_media_url": null,
"footer_text": "Autos del Valle",
"params": [
"1",
"2"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": null,
"status": "PENDING",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": null,
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /whatsapp-templates/{id}
Delete a WhatsApp template
Removes the local mirror; does not withdraw it from Meta.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/whatsapp-templates/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
GET /whatsapp-templates/{id}
Fetch one WhatsApp template
The local mirror row, including Meta’s status, the param labels/examples set on it, and its bindings.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl https://api.vitrinadev.com/api/v1/whatsapp-templates/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "UTILITY",
"category_reclassified_at": "2026-09-16T09:00:00.000Z",
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"header_text": null,
"header_media_url": null,
"footer_text": "Autos del Valle",
"params": [
"1",
"2"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": null,
"status": "APPROVED",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": "2026-09-15T18:20:00.000Z",
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /whatsapp-templates/{id}
Update a WhatsApp template’s local settings
header_media_url: the DEFAULT public https link for a media (IMAGE / VIDEO / DOCUMENT) header — what fills the header_media slot when a sender supplies none; null clears it. The way a template authored in Meta Business Manager (whose synced sample is Meta’s own CDN handle, not a link anyone may send) becomes sendable by campaigns without an override and by the automated rails. Local only: no Meta round-trip, no re-approval, never overwritten by a sync. 422 on a template without a media header or a non-https url.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
header_media_url | string | null | yes | uri, máx. 2000 |
curl -X PATCH https://api.vitrinadev.com/api/v1/whatsapp-templates/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"header_media_url": "https://api.vitrina.example/api/v1/public/marketplace-media/0b6f1a2c-6d0e-4a55-9c39-1f4b2b6a7e10/3f2a9d4e-8c1b-4f6a-9e2d-7b5c4a3e1f00.jpg"
}'Example response (200)
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "promo_primavera",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "UTILITY",
"category_reclassified_at": "2026-09-16T09:00:00.000Z",
"body_text": "Hola {{nombre}}, esta semana tenemos precios especiales. ¿Te cuento?",
"header_text": null,
"header_media_url": "https://api.vitrina.example/api/v1/public/marketplace-media/0b6f1a2c-6d0e-4a55-9c39-1f4b2b6a7e10/3f2a9d4e-8c1b-4f6a-9e2d-7b5c4a3e1f00.jpg",
"footer_text": "Autos del Valle",
"params": [
"nombre"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": [
{
"type": "HEADER",
"format": "IMAGE",
"example": {
"header_handle": [
"https://scontent.whatsapp.net/v/t61.29466-34/sample.jpg"
]
}
},
{
"type": "BODY",
"text": "Hola {{nombre}}, esta semana tenemos precios especiales. ¿Te cuento?"
}
],
"status": "APPROVED",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": "2026-09-15T18:20:00.000Z",
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z",
"param_spec": {
"slots": [
{
"key": "header_media",
"component": "header",
"media_format": "image"
},
{
"key": "nombre",
"component": "body"
}
],
"unsupported": [],
"supported": true,
"header_media_url": "https://api.vitrina.example/api/v1/public/marketplace-media/0b6f1a2c-6d0e-4a55-9c39-1f4b2b6a7e10/3f2a9d4e-8c1b-4f6a-9e2d-7b5c4a3e1f00.jpg"
}
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /whatsapp-templates/{id}/analytics
A WhatsApp template’s analytics (Vitrina’s own data)
What happened to every send of this template in the range, from Vitrina’s own records — available on every number, including the WhatsApp Business app (coexistence) numbers Meta gives no template insights. sent = sends Meta accepted (every sender: campaigns, Seguimientos, Recontacto, first touch, clinic notices, the inbox, tests); delivered / read / failed from the delivery receipts; replied = the person wrote on the same number within 72 h and this was their latest template; opted_out = the number opted out within 72 h. Broken down by sender, by quick-reply button (a reply whose first line is a button label) and by Meta failure code with a plain-language reason, plus a per-day (or per-week) series by send time in the workspace time zone. Sends are kept 400 days; sender and reply tracking start 2026-10-14 (older sends were backfilled where derivable).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes | |
from | query | string | no | mín. 1 |
to | query | string | no | mín. 1 |
group_by | query | day \ | week | no |
curl https://api.vitrinadev.com/api/v1/whatsapp-templates/<id>/analytics \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"template": {
"id": "0199a3c2-7b10-7000-8000-0000000000a1",
"name": "invitacion_exclusiva_v2",
"language": "es",
"category": "MARKETING",
"status": "APPROVED",
"messaging_account_id": "0199a3c2-7b10-7000-8000-0000000000b1"
},
"range": {
"from": "2026-09-15T03:00:00.000Z",
"to": "2026-10-14T18:00:00.000Z",
"timezone": "America/Santiago",
"group_by": "day"
},
"totals": {
"sent": 120,
"delivered": 110,
"read": 84,
"failed": 6,
"replied": 31,
"opted_out": 2
},
"rates": {
"delivered": 0.9167,
"read": 0.7636,
"replied": 0.2583,
"failed": 0.05
},
"by_sender": [
{
"sender": "campaign",
"sent": 100,
"delivered": 92,
"read": 70,
"failed": 5,
"replied": 24
},
{
"sender": "manual",
"sent": 20,
"delivered": 18,
"read": 14,
"failed": 1,
"replied": 7
}
],
"buttons": [
{
"text": "Quiero mi cupo",
"count": 18
},
{
"text": "No me interesa",
"count": 3
}
],
"free_text": 10,
"failures": [
{
"code": "131026",
"title": "Message undeliverable",
"human_es": "El número no puede recibir mensajes de WhatsApp.",
"human_en": "The number cannot receive WhatsApp messages.",
"action": "remove_number",
"count": 6
}
],
"daily": [
{
"date": "2026-10-13",
"sent": 80,
"delivered": 74,
"read": 60,
"failed": 4,
"replied": 22
},
{
"date": "2026-10-14",
"sent": 40,
"delivered": 36,
"read": 24,
"failed": 2,
"replied": 9
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /whatsapp-templates/{id}/infer-bindings
Re-run the binder over every slot ("volver a sugerir")
Suggests a binding for every \{\{variable\}\}, not just the unbound ones. The merge rule never overwrites a slot a person already bound by hand (origin: "manual"), so re-running this can never lose the workspace’s own choices.
Connected apps: refused with 403 CONNECTED_APP_EXCLUDED_OPERATION and absent from the MCP catalogue, whatever write pack was granted — the «Agentes de IA» pack promises not to include this act. An API key or a personal token of the workspace holding the scope reaches it as usual.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-templates/<id>/infer-bindings \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "UTILITY",
"category_reclassified_at": "2026-09-16T09:00:00.000Z",
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"header_text": null,
"header_media_url": null,
"footer_text": "Autos del Valle",
"params": [
"1",
"2"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": null,
"status": "APPROVED",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": "2026-09-15T18:20:00.000Z",
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /whatsapp-templates/{id}/param-binding
Set what fills each {{variable}} when Vitrina sends this template
Local-only, like param-meta: describes how Vitrina fills an ALREADY-APPROVED body, so there is no Meta round-trip and no re-approval. Unknown slot keys and malformed entries are dropped rather than failing the whole request.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
param_binding | object | yes |
curl -X PATCH https://api.vitrinadev.com/api/v1/whatsapp-templates/<id>/param-binding \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
}
}
}'Example response (200)
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "UTILITY",
"category_reclassified_at": "2026-09-16T09:00:00.000Z",
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"header_text": null,
"header_media_url": null,
"footer_text": "Autos del Valle",
"params": [
"1",
"2"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": null,
"status": "APPROVED",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": "2026-09-15T18:20:00.000Z",
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /whatsapp-templates/{id}/param-meta
Set the agent-facing label/example for each {{variable}}
Local-only — no Meta round-trip, no re-approval. Purely descriptive metadata for the composer (Seguimientos).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
param_meta | object | yes |
curl -X PATCH https://api.vitrinadev.com/api/v1/whatsapp-templates/<id>/param-meta \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
}
}
}'Example response (200)
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "UTILITY",
"category_reclassified_at": "2026-09-16T09:00:00.000Z",
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"header_text": null,
"header_media_url": null,
"footer_text": "Autos del Valle",
"params": [
"1",
"2"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": null,
"status": "APPROVED",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": "2026-09-15T18:20:00.000Z",
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /whatsapp-templates/{id}/usage
Re-file the template under another category
What the template is FOR, operator-facing — distinct from Meta's billing category. usage is the KEY of one of the workspace’s template categories (GET /whatsapp-templates/categories); an unknown key answers 422. Inferred from the name at create/sync, editable here. Local-only: no Meta round-trip, no re-approval. Templates in a category with hidden_from_customers are hidden from customer-facing send pickers.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
usage | string | yes | mín. 1, máx. 40, patrón ^[a-z0-9_]+$ |
curl -X PATCH https://api.vitrinadev.com/api/v1/whatsapp-templates/<id>/usage \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"usage": "follow_up"
}'Example response (200)
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "seguimiento_stock",
"language": "es_CL",
"category": "MARKETING",
"submitted_category": "UTILITY",
"category_reclassified_at": "2026-09-16T09:00:00.000Z",
"body_text": "Hola {{1}}, el {{2}} que consultaste sigue disponible. ¿Seguimos viéndolo?",
"header_text": null,
"header_media_url": null,
"footer_text": "Autos del Valle",
"params": [
"1",
"2"
],
"param_meta": {
"1": {
"label": "Nombre del contacto",
"example": "María"
},
"2": {
"label": "Modelo consultado",
"example": "Chevrolet Onix 2023"
}
},
"param_binding": {
"1": {
"source": "contact_field",
"field": "name"
},
"2": {
"source": "lead_interest"
}
},
"components": null,
"status": "APPROVED",
"rejection_reason": null,
"managed_for": null,
"managed_key": null,
"usage": "follow_up",
"synced_at": "2026-09-15T18:20:00.000Z",
"created_at": "2026-01-10T13:00:00.000Z",
"updated_at": "2026-09-15T18:20:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /whatsapp-templates/analytics/summary
Sends, reads, replies and failures per WhatsApp template
The list-page numbers for every template of the workspace in one read: per template with at least one send in the range, the counts of GET /whatsapp-templates/\{id\}/analytics’s totals. Same sources and definitions.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
from | query | string | no | mín. 1 |
to | query | string | no | mín. 1 |
curl https://api.vitrinadev.com/api/v1/whatsapp-templates/analytics/summary \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"range": {
"from": "2026-09-15T03:00:00.000Z",
"to": "2026-10-14T18:00:00.000Z",
"timezone": "America/Santiago"
},
"templates": [
{
"template_id": "0199a3c2-7b10-7000-8000-0000000000a1",
"sent": 120,
"delivered": 110,
"read": 84,
"failed": 6,
"replied": 31,
"opted_out": 2
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /whatsapp-templates/categories
List the workspace’s template categories
The operator-facing buckets templates are filed under (a template’s usage is one of these keys), ordered by position, each with the number of the workspace’s templates filed there. Every workspace starts with seven seeded (system: true) categories: first_contact, reminder, follow_up, update, aftercare, internal (hidden from customers) and other.
curl https://api.vitrinadev.com/api/v1/whatsapp-templates/categories \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "0199a3c2-7b10-7000-8000-00000000c000",
"key": "first_contact",
"name": "Primer contacto",
"position": 0,
"hidden_from_customers": false,
"system": true,
"template_count": 2
},
{
"id": "0199a3c2-7b10-7000-8000-00000000c001",
"key": "reminder",
"name": "Recordatorios",
"position": 1,
"hidden_from_customers": false,
"system": true,
"template_count": 4
},
{
"id": "0199a3c2-7b10-7000-8000-00000000c005",
"key": "internal",
"name": "Equipo",
"position": 5,
"hidden_from_customers": true,
"system": true,
"template_count": 1
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /whatsapp-templates/categories
Add a template category
Appended at the end of the list. The immutable key is derived from name (deaccented, [a-z0-9_], ≤40 chars) with a numeric suffix when taken.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | yes | mín. 1, máx. 80 |
hidden_from_customers | boolean | — |
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-templates/categories \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Cobranza",
"hidden_from_customers": false
}'Example response (201)
{
"data": {
"id": "0199a3c2-7b10-7000-8000-00000000c007",
"key": "cobranza",
"name": "Cobranza",
"position": 7,
"hidden_from_customers": false,
"system": false
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
DELETE /whatsapp-templates/categories/{id}
Delete a template category
Its templates are re-filed under other in the same transaction. internal and other cannot be deleted (400).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
curl -X DELETE https://api.vitrinadev.com/api/v1/whatsapp-templates/categories/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /whatsapp-templates/categories/{id}
Rename a template category or change its visibility
Seeded categories can be renamed and hidden too; the internal category must stay hidden from customers (400 when unhiding it). The key never changes.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | uuid | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
name | string | — | mín. 1, máx. 80 |
hidden_from_customers | boolean | — |
curl -X PATCH https://api.vitrinadev.com/api/v1/whatsapp-templates/categories/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Recordatorios de cita"
}'Example response (200)
{
"data": {
"id": "0199a3c2-7b10-7000-8000-00000000c001",
"key": "reminder",
"name": "Recordatorios de cita",
"position": 1,
"hidden_from_customers": false,
"system": true
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /whatsapp-templates/categories/order
Reorder the template categories
ids must list every category of the workspace exactly once (422 otherwise); positions are rewritten to the array order. Answers the reordered list.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
ids | uuid[] | yes |
curl -X PUT https://api.vitrinadev.com/api/v1/whatsapp-templates/categories/order \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"ids": [
"0199a3c2-7b10-7000-8000-00000000c001",
"0199a3c2-7b10-7000-8000-00000000c000",
"0199a3c2-7b10-7000-8000-00000000c005"
]
}'Example response (200)
{
"data": [
{
"id": "0199a3c2-7b10-7000-8000-00000000c000",
"key": "first_contact",
"name": "Primer contacto",
"position": 0,
"hidden_from_customers": false,
"system": true,
"template_count": 2
},
{
"id": "0199a3c2-7b10-7000-8000-00000000c001",
"key": "reminder",
"name": "Recordatorios",
"position": 1,
"hidden_from_customers": false,
"system": true,
"template_count": 4
},
{
"id": "0199a3c2-7b10-7000-8000-00000000c005",
"key": "internal",
"name": "Equipo",
"position": 5,
"hidden_from_customers": true,
"system": true,
"template_count": 1
}
]
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /whatsapp-templates/category-check
Predict whether Meta will reclassify a UTILITY draft as MARKETING
Jev-calibrated preview the template editor calls before submit (#2972). Meta silently reclassifies UTILITY templates as MARKETING at review time; this warns before that happens instead of after. category other than UTILITY always answers applicable: false — Meta’s reclassification only runs that one direction, so nothing is asked. Never changes what gets submitted and never writes to the Jev ledger (a preview is not an acted-on decision — the actual submission is what gets ledgered and later graded against Meta’s verdict). cause is a deterministic guess (never Jev’s own answer) set only when likely_marketing is true.
Connected apps: refused with 403 CONNECTED_APP_EXCLUDED_OPERATION and absent from the MCP catalogue, whatever write pack was granted — the «Agentes de IA» pack promises not to include this act. An API key or a personal token of the workspace holding the scope reaches it as usual.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
category | MARKETING \ | UTILITY \ | AUTHENTICATION |
body_text | string | yes | mín. 1, máx. 1024 |
header_text | string | null | — | mín. 1, máx. 60 |
footer_text | string | null | — | mín. 1, máx. 60 |
buttons | any[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-templates/category-check \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"category": "UTILITY",
"body_text": "Hola {{1}}, ¿sigues interesado en el {{2}}? Si quieres, coordinamos una visita para que lo veas.",
"footer_text": "Autos del Valle"
}'Example response (200)
{
"data": {
"applicable": true,
"likely_marketing": true,
"cause": "promotional_wording"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /whatsapp-templates/header-media
Upload a media-header file (image, video or PDF)
Multipart upload in a field named file: image/jpeg or image/png up to 5 MB, video/mp4 up to 16 MB (normalised to a WhatsApp-safe profile), application/pdf up to 16 MB — Meta’s own send caps. Answers \{ url, mime_type, size_bytes, format \}; url is a PUBLIC https link (Meta fetches it anonymously at send time), so anything uploaded here is readable by anyone with the link. Use it as header_media.url when creating a template, as a template’s header_media_url (PATCH /whatsapp-templates/\{id\}), or as a campaign’s per-campaign override template_params.header_media = \{ source: "static", value: <url> \}. 415 on another type, 413 over the cap.
Or a JSON body (application/json) — what an MCP connector sends through call_operation, which cannot build multipart: \{ filename, content, content_encoding?, content_type? \} plus the operation's own form fields. content is UTF-8 text by default, or base64 when content_encoding is "base64" (images, PDF, audio, video). The same byte cap applies to the decoded bytes (413 above it); invalid base64 is a 400; the content type defaults from the filename extension.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
filename | string | yes | mín. 1, máx. 255 |
content | string | yes | mín. 1, máx. 35000000 |
content_encoding | utf8 \ | base64 | — |
content_type | string | — | máx. 200 |
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-templates/header-media \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"file": "(binary file part: image/jpeg, image/png, video/mp4 or application/pdf)"
}'Example response (200)
{
"data": {
"url": "https://api.vitrina.example/api/v1/public/marketplace-media/0b6f1a2c-6d0e-4a55-9c39-1f4b2b6a7e10/3f2a9d4e-8c1b-4f6a-9e2d-7b5c4a3e1f00.jpg",
"mime_type": "image/jpeg",
"size_bytes": 284113,
"format": "IMAGE"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /whatsapp-templates/readiness
Are the workspace’s WhatsApp templates ready?
Per ENABLED feature (first touch, and each feature as it joins the pack registry) and per connected number: the templates it needs, with counts of required, approved, pending, rejected, missing and blocked (paused or disabled by Meta), and the rejection reason where there is one. A feature that is off answers status: "not_needed" with a reason and is left out of the totals. Reads only: nothing is sent to Meta. ready is true when every required template is approved. Use POST /whatsapp-templates/readiness/retry to resubmit.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
pack | query | string | no | mín. 1 |
messaging_account_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/whatsapp-templates/readiness \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"ready": false,
"counts": {
"required": 2,
"approved": 1,
"pending": 0,
"rejected": 1,
"missing": 0,
"blocked": 0
},
"packs": [
{
"pack": "first_touch",
"feature": "First reply to a new customer",
"audience": "customer",
"status": "enabled",
"reason": null,
"ready": false,
"counts": {
"required": 2,
"approved": 1,
"pending": 0,
"rejected": 1,
"missing": 0,
"blocked": 0
},
"numbers": [
{
"messaging_account_id": "0199a3c2-7b10-7000-8000-0000000000a1",
"counts": {
"required": 2,
"approved": 1,
"pending": 0,
"rejected": 1,
"missing": 0,
"blocked": 0
},
"templates": [
{
"entry": "website_named",
"name": "first_touch_website_named",
"language": "es",
"state": "approved",
"status": "APPROVED",
"rejection_reason": null,
"error": null
},
{
"entry": "portal_named",
"name": "first_touch_portal_named",
"language": "es",
"state": "rejected",
"status": "REJECTED",
"rejection_reason": "INVALID_FORMAT",
"error": null
}
]
}
]
}
]
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /whatsapp-templates/readiness/retry
Resubmit missing or rejected feature templates
Resubmits through the template pack: a MISSING template is submitted as is; a REJECTED one goes to Meta under a new versioned name (<name>_r2, _r3, …) because Meta refuses to reuse a rejected name. Approved and pending templates are left alone. Answers what was done per template plus the pack’s readiness afterwards (submitted ones are pending until Meta decides).
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
pack | string | yes | mín. 1 |
entry | string | — | mín. 1 |
messaging_account_id | uuid | — |
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-templates/readiness/retry \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"pack": "first_touch"
}'Example response (200)
{
"data": {
"results": [
{
"pack": "first_touch",
"entry": "portal_named",
"messaging_account_id": "0199a3c2-7b10-7000-8000-0000000000a1",
"name": "first_touch_portal_named_r2",
"language": "es",
"action": "submitted",
"state": "pending",
"status": "PENDING",
"rejection_reason": null,
"error": null
}
],
"readiness": {
"pack": "first_touch",
"feature": "First reply to a new customer",
"audience": "customer",
"status": "enabled",
"reason": null,
"ready": false,
"counts": {
"required": 2,
"approved": 1,
"pending": 0,
"rejected": 1,
"missing": 0,
"blocked": 0
},
"numbers": [
{
"messaging_account_id": "0199a3c2-7b10-7000-8000-0000000000a1",
"counts": {
"required": 2,
"approved": 1,
"pending": 0,
"rejected": 1,
"missing": 0,
"blocked": 0
},
"templates": [
{
"entry": "website_named",
"name": "first_touch_website_named",
"language": "es",
"state": "approved",
"status": "APPROVED",
"rejection_reason": null,
"error": null
},
{
"entry": "portal_named",
"name": "first_touch_portal_named",
"language": "es",
"state": "rejected",
"status": "REJECTED",
"rejection_reason": "INVALID_FORMAT",
"error": null
}
]
}
]
}
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /whatsapp-templates/sync
Force a reconcile of a channel's templates against Meta
Fetches every template Meta reports for the messaging account and updates local status/rejection_reason/components. Use this right after submitting a change on Meta’s side instead of waiting for the periodic poll.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
messaging_account_id | uuid | yes |
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-templates/sync \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001"
}'Example response (200)
{
"data": {
"checked": 4,
"updated": 1,
"created": 0
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /whatsapp-templates/usage
Re-file several templates under one category
Bulk form of PATCH /whatsapp-templates/\{id\}/usage: 1–200 template ids, one category key (422 when it is not one of the workspace’s). Ids that are not the workspace’s are ignored; updated counts the rows that actually changed. Local-only.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
ids | uuid[] | yes | |
usage | string | yes | mín. 1, máx. 40, patrón ^[a-z0-9_]+$ |
curl -X PATCH https://api.vitrinadev.com/api/v1/whatsapp-templates/usage \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"ids": [
"e2e2e2e2-0000-4000-8000-000000000001"
],
"usage": "follow_up"
}'Example response (200)
{
"data": {
"updated": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429