Respetar a quien pidió que no le escribas
Cuatro ámbitos de consentimiento, un historial que no se borra
Un cliente respondió «BAJA». Lo que pidió no es silencio: pidió que no le manden más promociones. Si tu integración lo lee como una baja total, deja de avisarle de lo que él mismo pidió que le avisaran. Si no lo lee, le sigue llegando publicidad.
Esta receta anota esa preferencia con el ámbito exacto que el cliente dio. El mismo contacto termina rechazando un mensaje y recibiendo otro en el mismo minuto.
Trampa
Una baja bloquea el marketing, no el servicio
Cuando alguien responde una palabra de baja, Vitrina le contesta: «Listo, no te
enviaremos más mensajes promocionales por este canal». La preferencia que se
guarda tiene ámbito marketing, no all_proactive. Un Seguimiento que el
propio cliente pidió sigue saliendo después de esa baja.
Antes de empezar
- Una API key con
contacts:readpara leer el historial ycontacts:writepara anotar. - El id del contacto.
- Un criterio escrito de qué ámbito corresponde a cada situación. La tabla del paso 2 es el punto de partida.
1. Anota lo que el cliente pidió
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": "blocked",
"legal_basis": "Respondió BAJA por WhatsApp el 23-09-2026"
}'{
"data": {
"id": "df71f63d-f49f-4d6d-8c90-e3a506b9817e",
"tenant_id": "82ee1231-7431-4f63-a716-224d61905e23",
"contact_id": "01a0ce37-bf26-7e2c-bcc8-ced619c07df6",
"channel": "whatsapp",
"scope": "marketing",
"status": "blocked",
"source": "admin",
"evidence_message_id": null,
"legal_basis": "Respondió BAJA por WhatsApp el 23-09-2026",
"recorded_at": "2026-09-23T12:22:35.107Z",
"expires_at": null,
"created_by": null,
"created_at": "2026-09-23T12:22:35.107Z"
}
}Tres campos hacen el trabajo. channel acota el canal, o vale any para todos. scope acota el tipo de mensaje. status es allowed, blocked o unknown.
legal_basis es texto libre y se guarda literal. Es lo que vas a mostrar el día que alguien pregunte «¿desde cuándo, y cómo lo saben?». Cuando la prueba es un mensaje del propio cliente, evidence_message_id apunta a él, y la respuesta deja de ser una fecha para pasar a ser el mensaje.
recorded_at lo pone el servidor y no se acepta en el cuerpo.
2. Elige el ámbito por lo que el cliente dijo
scope | Qué cubre | Cuándo se usa |
|---|---|---|
marketing | Promociones y reactivaciones. | La palabra de baja, el enlace de desuscripción, «no me manden más ofertas». |
service | Avisos operativos de algo que el cliente ya tiene en curso. | Casi nunca por decisión del cliente: quien pide esto suele estar pidiendo all_proactive. |
promised_followup | Los avisos que el propio cliente pidió recibir. | «Ya no me avises de eso», sobre un aviso puntual. |
all_proactive | Todo lo que salga sin que él haya escrito primero. | «No me contacten más», en cualquiera de sus formas. |
Las palabras que Vitrina reconoce como baja en un mensaje entrante son baja, stop, alto, unsubscribe, no molestar y salir. Solo cuentan cuando son el mensaje completo.
«Cancelar» no cuenta como baja, porque suele usarse para anular una cita o un pedido.
3. Comprueba que el bloqueo muerde
El mismo contacto, la misma conversación, una plantilla MARKETING:
{
"error": {
"reasons": ["scope_blocked"],
"hint": "El contacto se dio de baja de este tipo de mensajes. Respeta su preferencia.",
"code": "OUTBOUND_BLOCKED",
"message": "This message was not sent: the outbound policy blocks it (scope_blocked). A block cannot be acknowledged — see error.hint for what to do instead.",
"details": {
"reasons": [
{
"code": "scope_blocked",
"kind": "bloqueo",
"hint": "El contacto se dio de baja de este tipo de mensajes. Respeta su preferencia.",
"detail": { "scope": "marketing", "source": "contact_outbound_preference" }
}
]
},
"requestId": "e7128069-c571-47df-8405-f6fcd2286538"
}
}detail.scope dice qué ámbito decidió, y detail.source de dónde salió el dato. Es un bloqueo, así que ningún acknowledge lo levanta. La preferencia la puso el cliente, no quien envía.
4. Comprueba que el servicio sigue pasando
Segundos después, el mismo contacto, el mismo canal, una plantilla UTILITY:
{
"data": {
"created_at": "2026-09-23T12:22:35.169854+00:00",
"sender_role": "assistant",
"metadata": {
"template": {
"id": "4a1e0001-0000-4000-8000-000000000001",
"name": "retomar_solicitud",
"language": "es_CL"
}
},
"type": "text",
"media_urls": null,
"content": "Hola Valentina, quedamos en retomar tu solicitud SOL-4821. ¿La seguimos ahora?",
"updated_at": "2026-09-23T12:22:35.169854",
"tool_call_id": null,
"tool_calls": null,
"tokens": 0,
"id": "e53b0168-b7cb-46d2-97fa-2c3691ac544a",
"tenant_id": "82ee1231-7431-4f63-a716-224d61905e23",
"sender_type": "api_key",
"sender_id": "76647f68-da82-4d36-8cba-e97a70661e3d",
"correlation_id": null,
"conversation_id": "01a0ce37-bf2f-7430-a199-59c85ba55d35",
"external_message_id": "sandbox-captured:dcbe70b8-d912-4e19-ab57-530183e84b3e",
"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 categoría de la plantilla fija la base legal del envío, y la base legal elige el ámbito que se consulta. El bloqueo de un ámbito no afecta a los otros tres.
5. La baja total, y su precedencia
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": "any",
"scope": "all_proactive",
"status": "blocked",
"legal_basis": "Pidió por teléfono no recibir ningún mensaje que no haya pedido"
}'Con esa fila puesta, la misma plantilla UTILITY del paso 4 deja de salir:
{
"error": {
"reasons": ["scope_blocked"],
"hint": "El contacto se dio de baja de este tipo de mensajes. Respeta su preferencia.",
"code": "OUTBOUND_BLOCKED",
"message": "This message was not sent: the outbound policy blocks it (scope_blocked). A block cannot be acknowledged — see error.hint for what to do instead.",
"details": {
"reasons": [
{
"code": "scope_blocked",
"kind": "bloqueo",
"hint": "El contacto se dio de baja de este tipo de mensajes. Respeta su preferencia.",
"detail": { "scope": "all_proactive", "source": "contact_outbound_preference" }
}
]
},
"requestId": "89987b46-09e9-4f8e-b3fd-08bd96b1fbae"
}
}El orden de precedencia, de arriba hacia abajo:
- El contacto bloqueado o marcado como spam en su ficha bloquea todo.
- Un
all_proactivevigente bloquea todo. - La fila vigente del ámbito consultado manda. Una fila de canal específico gana a una de
any; entre iguales, gana la más reciente. - Sin ninguna fila, el envío sale.
La ausencia de una preferencia no es un consentimiento, pero tampoco detiene el envío. Anota el consentimiento el día que lo recibes: es tu respaldo ante la Ley 21.719.
6. Revocar, y el historial que queda
No hay PATCH ni DELETE aquí. Revocar es un POST con el estado contrario, y la fila anterior se queda donde está:
{
"data": [
{
"id": "8461b248-323c-46b1-8277-22c639277373",
"tenant_id": "82ee1231-7431-4f63-a716-224d61905e23",
"contact_id": "01a0ce37-bf26-7e2c-bcc8-ced619c07df6",
"channel": "any",
"scope": "all_proactive",
"status": "allowed",
"source": "admin",
"evidence_message_id": null,
"legal_basis": "Volvió a pedir el aviso de su solicitud el 23-09-2026",
"recorded_at": "2026-09-23T12:22:35.238Z",
"expires_at": null,
"created_by": null,
"created_at": "2026-09-23T12:22:35.238Z"
},
{
"id": "16029957-30a2-4c00-81fb-3f004aa1d05c",
"tenant_id": "82ee1231-7431-4f63-a716-224d61905e23",
"contact_id": "01a0ce37-bf26-7e2c-bcc8-ced619c07df6",
"channel": "any",
"scope": "all_proactive",
"status": "blocked",
"source": "admin",
"evidence_message_id": null,
"legal_basis": "Pidió por teléfono no recibir ningún mensaje que no haya pedido",
"recorded_at": "2026-09-23T12:22:35.191Z",
"expires_at": null,
"created_by": null,
"created_at": "2026-09-23T12:22:35.191Z"
},
{
"id": "df71f63d-f49f-4d6d-8c90-e3a506b9817e",
"tenant_id": "82ee1231-7431-4f63-a716-224d61905e23",
"contact_id": "01a0ce37-bf26-7e2c-bcc8-ced619c07df6",
"channel": "whatsapp",
"scope": "marketing",
"status": "blocked",
"source": "admin",
"evidence_message_id": null,
"legal_basis": "Respondió BAJA por WhatsApp el 23-09-2026",
"recorded_at": "2026-09-23T12:22:35.107Z",
"expires_at": null,
"created_by": null,
"created_at": "2026-09-23T12:22:35.107Z"
}
],
"meta": { "total": 3 }
}GET /contacts/{id}/outbound-preferences devuelve el historial completo, del hecho más nuevo al más viejo. Una fila superada se sigue leyendo igual que antes. La prueba de que el cliente pidió la baja tiene que sobrevivir al día en que la levantó.
source dice quién escribió cada fila. admin es una persona o una credencial. Las otras se escriben solas: cuando el cliente responde una palabra de baja, cuando usa el enlace de desuscripción, o cuando el proveedor informa un rebote.
Cuando falla
scope_blocked y marketing_consent_missing son bloqueos y no se reintentan. El reintento manda el mismo mensaje al mismo contacto que dijo que no. Lo que sí sirve es ramificar por detail.scope y decidir si hay otra forma legítima de decir lo mismo. Un aviso de servicio no necesita ir como promoción.
Cuando el bloqueo viene de la ficha del contacto y no de esta tabla, el código cambia: contact_blocked o contact_spam. Esos dos se levantan desde la ficha.
En la aplicación: la misma preferencia se anota en la ficha del contacto, y la lista de quienes pidieron no recibir está en Campañas → No contactar. Guía completa en Manual de plataforma → Respetar a quien pidió no escribirle.

Qué exige la ley chilena sobre estos datos, y cómo se responde a un titular que los reclama, está en Datos personales. El consentimiento que se anota antes del primer mensaje está en Escribir primero por WhatsApp sin quemar el número.