Escribir primero por WhatsApp sin quemar el número
Consentimiento, categoría de plantilla y los límites que cuidan tu número
Alguien dejó su teléfono en un formulario y nunca te escribió. Ese primer mensaje es el más caro que vas a mandar. Si molesta, la persona bloquea el número, y WhatsApp puntúa ese bloqueo contra tu cuenta entera.
Reabrir una conversación recupera una relación que existe. Abrirla es pedir permiso. Esta receta arma ese primer mensaje con las tres cosas que lo hacen aceptable: una dirección real, un consentimiento anotado y la categoría correcta.
Trampa
El hilo que abres por la API todavía no tiene la dirección del cliente
POST /conversations deja el hilo anotado, pero no resuelve a dónde enviar: la
conversación vuelve con external_id en la forma manual:<uuid>, en todos los
canales. Ese identificador no es un teléfono, y es justamente el que la API le
entrega al proveedor al enviar. Lee external_id antes de mandar nada. El hilo
que sí se puede enviar es el que abrió el propio cliente al escribirte, o el que
abrió un Seguimiento que él pidió.
Antes de empezar
- Una API key con
conversations:write,messages:sendycontacts:write. - El teléfono del contacto en formato E.164 (
+56944180332). - Una plantilla aprobada por Meta en la cuenta de canal que va a enviar.
1. Anota el consentimiento antes de escribir
curl -X POST https://api.vitrinadev.com/api/v1/contacts/$CONTACT/outbound-preferences \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"scope": "marketing",
"status": "allowed",
"legal_basis": "Marcó la casilla de novedades en el formulario del sitio, 23-09-2026"
}'{
"data": {
"id": "c70ac636-af84-4d05-bb6f-c8a27b793c14",
"tenant_id": "82ee1231-7431-4f63-a716-224d61905e23",
"contact_id": "01a0ce32-960a-7e88-aa0a-045eba7cfbdf",
"channel": "whatsapp",
"scope": "marketing",
"status": "allowed",
"source": "admin",
"evidence_message_id": null,
"legal_basis": "Marcó la casilla de novedades en el formulario del sitio, 23-09-2026",
"recorded_at": "2026-09-23T12:17:45.114Z",
"expires_at": null,
"created_by": null,
"created_at": "2026-09-23T12:17:45.114Z"
}
}Un contacto sin ninguna fila aquí no está bloqueado. La plataforma lo trata como allowed y el envío sale. Eso no equivale a un permiso del cliente.
Bajo la Ley 21.719 lo que te defiende es la fila: su legal_basis escrito en palabras, y su recorded_at puesto por el servidor. Ese campo no se acepta en el cuerpo. Cuando la prueba es un mensaje del cliente, apunta a él con evidence_message_id.
2. Comprueba que la ventana nunca se abrió
curl -X POST https://api.vitrinadev.com/api/v1/conversations/$CONVERSATION/messages \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "content": "Hola Ignacio, te escribo por la solicitud que dejaste en el sitio." }'{
"error": {
"reasons": ["template_missing", "window_closed"],
"hint": "La ventana de 24 horas está cerrada: usa una plantilla aprobada (POST /conversations/{id}/templates).",
"code": "OUTBOUND_BLOCKED",
"message": "This message was not sent: the outbound policy blocks it (template_missing, window_closed). A block cannot be acknowledged — see error.hint for what to do instead.",
"details": {
"reasons": [
{
"code": "template_missing",
"kind": "bloqueo",
"hint": "La ventana de 24 horas está cerrada: usa una plantilla aprobada (POST /conversations/{id}/templates).",
"detail": { "channel": "whatsapp", "windowOpen": false }
},
{
"code": "window_closed",
"kind": "bloqueo",
"hint": "La ventana de 24 horas está cerrada: usa una plantilla aprobada (POST /conversations/{id}/templates).",
"detail": { "channel": "whatsapp", "lastInboundAt": null }
}
]
},
"requestId": "6e3b20da-cc94-468b-8c31-b8948643ac03"
}
}lastInboundAt: null es la diferencia con una conversación fría. Ahí hay una fecha vieja; aquí no hay ninguna. La ventana de atención no se abre con lo que tú envías, solo con lo que el cliente escribe. Un primer contacto, entonces, siempre sale por plantilla.
Esta llamada no es un paso obligatorio. Es la forma barata de saber en qué estado está el hilo antes de gastar una plantilla.
3. Elige la categoría por lo que vas a decir
La categoría fija la base legal del mensaje. Tu código no puede declarar otra en el cuerpo de la llamada:
| Categoría | Base legal | Consentimiento que exige |
|---|---|---|
UTILITY | servicio | Ninguno específico. Es una obligación hacia alguien que ya te pidió algo. |
MARKETING | marketing | El ámbito marketing del contacto. Sin él, 422 marketing_consent_missing. |
AUTHENTICATION | servicio | Ninguno. Es un código que el propio cliente pidió. |
Mandar una promoción con una plantilla UTILITY tampoco es la salida. Meta clasifica las plantillas por su contenido al aprobarlas, y una que invita a comprar sale rechazada o reclasificada. La categoría es el veredicto de Meta sobre lo que dice la plantilla.
4. Manda el primer mensaje
curl -X POST https://api.vitrinadev.com/api/v1/conversations/$CONVERSATION/templates \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "4a1e0001-0000-4000-8000-000000000002",
"params": { "nombre": "Ignacio" }
}'{
"data": {
"created_at": "2026-09-23T12:17:45.160466+00:00",
"sender_role": "assistant",
"metadata": {
"template": {
"id": "4a1e0001-0000-4000-8000-000000000002",
"name": "novedades_del_mes",
"language": "es_CL"
}
},
"type": "text",
"media_urls": null,
"content": "Hola Ignacio, este mes tenemos novedades para ti. ¿Te las cuento?",
"updated_at": "2026-09-23T12:17:45.160466",
"tool_call_id": null,
"tool_calls": null,
"tokens": 0,
"id": "42ff6abc-f157-4875-b9f2-66e9ebd9f4e5",
"tenant_id": "82ee1231-7431-4f63-a716-224d61905e23",
"sender_type": "api_key",
"sender_id": "76647f68-da82-4d36-8cba-e97a70661e3d",
"correlation_id": null,
"conversation_id": "01a0ce32-962e-720b-9bbc-d4ff5ec8a1fd",
"external_message_id": "sandbox-captured:efe8a445-54a6-4012-93c4-ec5c53599a5c",
"delivery_status": "sent",
"delivery_error": null,
"delivered_at": null,
"read_at": null,
"media_sensitivity": null,
"media_processing_state": null,
"derived_context_expires_at": null,
"media_expires_at": null,
"next_attempt_at": null,
"delivery_attempt": 0,
"author": {
"id": "76647f68-da82-4d36-8cba-e97a70661e3d",
"kind": "api_key",
"name": "mi-integracion"
},
"rfc822_message_id": null
}
}La respuesta trae el cuerpo ya renderizado. Esa plantilla termina en «Responde BAJA para dejar de recibir estos mensajes», una salida visible que reduce las denuncias. Qué pasa cuando alguien la usa está en Respetar a quien pidió que no le escribas.
5. Conoce el límite antes de chocarlo
Escribirle a uno es fácil. El daño aparece cuando un ciclo se descontrola y le escribe a cuatrocientos. La política lleva dos contadores por credencial, en ventanas móviles. Así se ve el segundo cuando se cruza:
{
"error": {
"reasons": ["loop_burst"],
"hint": "Esta credencial envió más de 60 mensajes en el último minuto. Si igual quieres enviarlo, reenvía con acknowledge: [\"loop_burst\"].",
"code": "OUTBOUND_WARNING",
"message": "This message was not sent yet: the outbound policy warns about loop_burst. Resend the same request with \"acknowledge\": [\"loop_burst\"] to send it anyway; the acknowledgment is recorded.",
"details": {
"reasons": [
{
"code": "loop_burst",
"kind": "advertencia",
"hint": "Esta credencial envió más de 60 mensajes en el último minuto. Si igual quieres enviarlo, reenvía con acknowledge: [\"loop_burst\"].",
"detail": { "sendsLastMinute": 60, "limit": 60 }
}
]
},
"requestId": "1a4c7a3d-e1eb-4d63-9b23-16e10183fa54"
}
}loop_burstcuenta envíos por credencial: más de sesenta en un minuto.detailtrae el contador y el límite, así que tu código puede esperar en vez de reintentar.loop_same_contentcuenta destinatarios: el mismo contenido a más de veinte contactos distintos en diez minutos.
Las dos son advertencias, así que un reenvío con acknowledge sigue adelante. Protegen la calidad del número cuando una integración queda atascada reintentando.
Lo que la API no puede cuidar por ti
La política no rechaza envíos según la calificación de calidad que WhatsApp le pone a tu número.
Lo que sí observa es el estado del canal. Una cuenta desconectada es un bloqueo (account_disconnected). Una cuenta marcada para revisión levanta account_quality_degraded, una advertencia sobre el tráfico discrecional.
La calidad del número depende de tres decisiones tuyas, todas antes de la llamada: a quién le escribes, con qué categoría y cada cuánto.
En la aplicación: el mismo primer mensaje se manda desde el compositor de la bandeja, eligiendo una plantilla. El consentimiento se anota en la ficha del contacto. Guía completa en Manual de plataforma → Escribir primero sin quemar el número.
Si la conversación ya existía y solo se enfrió, el camino es otro: Reabrir una conversación fría con una plantilla. Para crear la plantilla y seguir su aprobación, Plantillas y Flows de WhatsApp.