Use WhatsApp templates and Flows
Meta-approved message shapes, and the structured forms a WhatsApp conversation can open.
Writing to a contact outside the 24-hour window needs a Meta-approved template. Opening a form inside the conversation needs a Flow. Both are configuration catalogs for the WhatsApp channel. Neither sends anything on its own: both are consumed from a conversation's message-send surface.
Templates
A template (whatsapp_template) is the Meta-approved shape for writing to a contact outside the 24-hour window. You create it here and it syncs against Meta. There's no approval webhook, so status (PENDING → APPROVED | REJECTED | …) is reconciled by a periodic poll. POST /whatsapp-templates/sync forces an on-demand reconcile.
Reading and writing need campaigns:read / campaigns:write.
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_consulta",
"language": "es_CL",
"category": "MARKETING",
"body_text": "Hola {{1}}, {{2}} sigue disponible. ¿Seguimos adelante?",
"footer_text": "Equipo de atención"
}'name is lowercase letters, digits and underscores only, which is Meta's constraint. language looks like es, es_CL or en_US. The response is 201 with status: "PENDING", because approval is asynchronous.
param_meta holds the label and example for each {{variable}}, for whoever is composing. param_binding says what fills each variable: a contact field, a custom attribute, the lead's interest, a literal, or "the person types it". Both are edited separately, with PATCH /whatsapp-templates/{id}/param-meta and PATCH /whatsapp-templates/{id}/param-binding. Neither touches Meta or re-triggers approval: they describe how Vitrina fills an already-approved body, not the body itself.
{
"data": {
"id": "e2e2e2e2-0000-4000-8000-000000000001",
"name": "seguimiento_consulta",
"language": "es_CL",
"status": "APPROVED",
"param_binding": {
"1": { "source": "contact_field", "field": "name" },
"2": { "source": "lead_interest" }
}
}
}POST /whatsapp-templates/{id}/infer-bindings re-suggests a binding for every {{variable}}. The merge rule never overwrites a slot a person bound by hand (origin: "manual"), so re-running it can't lose a workspace's own choice.
Flows
A Flow (whatsapp_flow) is a structured, multi-screen form that opens inside the conversation: booking, sign-up, a short survey.
There are two surfaces. GET /whatsapp-flows is a live mirror of what Meta reports for a channel. It's what the composer's picker shows, and it includes a Flow authored outside Vitrina. /whatsapp-flows/managed/* is the builder: local rows with builder_state as the source of truth, meaning ordered screens from a component palette. Saving regenerates the Flow JSON and uploads it to Meta.
curl -X POST https://api.vitrinadev.com/api/v1/whatsapp-flows/managed \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"messaging_account_id": "e4e4e4e4-0000-4000-8000-000000000001",
"name": "Agenda tu visita",
"categories": ["APPOINTMENT_BOOKING"],
"builder_state": {
"screens": [{
"title": "¿Cuándo te acomoda?",
"footer_label": "Continuar",
"components": [
{ "kind": "heading", "text": "Agenda tu visita" },
{ "kind": "date_picker", "name": "fecha", "label": "Fecha preferida", "required": true }
]
}]
}
}'{
"data": {
"id": "e3e3e3e3-0000-4000-8000-000000000001",
"meta_flow_id": "1200300400500600",
"name": "Agenda tu visita",
"status": "DRAFT",
"validation_errors": null
}
}meta_flow_id is the id Meta assigns on upload. We don't generate it, so it isn't a uuid. validation_errors is Meta's verdict on the last upload, and it blocks publish while non-empty.
Trap
PATCH and DELETE on a published Flow answer 409
Meta makes it immutable on publish. POST /whatsapp-flows/managed/{id}/duplicate is the way out: it always creates a
fresh DRAFT copy, ready to keep editing. A row with builder_state: null
was created outside Vitrina and is read-only here.