VitrinaAPI

Enviar mensajes

Qué permiso pide un envío y los dos veredictos de la política de envíos.

Un mensaje que sale de Vitrina llega a una persona. Por eso todo envío hecho con una credencial pasa antes por la política de envíos. Vale para una API key, un token personal y una aplicación conectada, y es la misma política que rige los envíos automáticos. Un miembro escribiendo en la bandeja no pasa por ella: ya está mirando la conversación.

La política frena los envíos que pueden dañar un número o la relación con un cliente:

  • un contacto que pidió no recibir mensajes;
  • una ventana de WhatsApp cerrada;
  • un script en bucle;
  • una aplicación que entendió mal una instrucción.

El permiso

Enviar pide messages:send, además de conversations:write.

Son dos permisos porque son dos cosas distintas. conversations:write es operar la bandeja: asignar, resolver, posponer, etiquetar. messages:send es poner un mensaje delante de un cliente. Una integración que ordena la bandeja no necesita lo segundo.

Una key con conversations:write y sin messages:send:

{
  "error": {
    "code": "FORBIDDEN",
    "message": "Missing required scope: messages:send",
    "requestId": "b4bab5e3-56aa-43d2-acdd-90d558a25656"
  }
}

…y la misma key asigna y resuelve la conversación sin problema.

Las operaciones que lo piden:

OperaciónQué envía
POST /conversations/{id}/messagesTexto, y correo con cc, bcc y cuerpo enriquecido.
POST /conversations/{id}/templatesUna plantilla aprobada de WhatsApp.
POST /conversations/{id}/flowsUn formulario (Flow) de WhatsApp.
POST /conversations/{id}/locationUna ubicación.
POST /conversations/{id}/attachmentsUn archivo o una imagen, con caption opcional.
POST /conversations/{id}/voiceUna nota de voz.

Los roles de Vitrina traen messages:send. Una key nueva lo lleva solo si lo pides; está en Autenticación.

Los dos veredictos

La política responde una de dos cosas, y cada una te pide algo distinto.

422 OUTBOUND_BLOCKED: el bloqueo de envío

Alguien más ya dijo que no: el contacto, la ley o el proveedor del canal. No se puede pasar por encima. error.hint dice qué hacer en su lugar.

Un texto libre a una conversación de WhatsApp cuya última respuesta del cliente fue hace tres días:

{
  "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": "2026-09-19T03:19:38.824255+00:00" }
        }
      ]
    },
    "requestId": "03c42c0e-0769-4659-b500-6464a85c3a73"
  }
}

Bloquean, entre otros:

  • un contacto bloqueado, marcado como spam o dado de baja de este canal;
  • una retención de envíos activa;
  • la ventana de 24 horas cerrada sin plantilla;
  • una plantilla que no está aprobada, no está completa o no corresponde a la categoría del mensaje;
  • una conversación sin dirección a la que enviar;
  • una cuenta de canal desconectada;
  • un número con calidad en rojo en WhatsApp;
  • en correo, una dirección en cc o bcc que rebotó, marcó spam o pidió no recibir nada (y, en un envío de marketing, también la que se dio de baja de promociones): 422 copy_address_blocked, que la nombra; no se quita en silencio. Un envío programado la vuelve a revisar al salir; si ya no puede recibir, el envío queda como no enviado, con el motivo.

Responder a quien se dio de baja y volvió a escribir. Una baja no impide contestarle a quien escribe. Si el contacto se dio de baja y después escribió en esa misma conversación, puedes responderle con texto libre (POST /conversations/{id}/messages) mientras la ventana de 24 horas de WhatsApp, Instagram o Messenger siga abierta, contada desde su último mensaje. En correo, la respuesta va en el mismo hilo en que escribió, sin límite de tiempo. Ese mensaje tiene que ser posterior a la baja y no puede ser el propio mensaje de baja. Solo cuenta la baja que pidió el propio cliente: si además hay un bloqueo que puso tu equipo, o uno por queja (marcó el correo como spam) o rechazo, la respuesta sigue bloqueada. Una baja por el enlace para darse de baja sí cuenta como del cliente. Una respuesta automática del cliente (fuera de la oficina, acuse de recibo) no cuenta como que escribió, y la respuesta nunca va a la dirección que la mandó. En Gmail y Outlook se reconoce por los headers Auto-Submitted y Precedence; en un buzón IMAP, además, por X-Autoreply, X-Autorespond y los avisos de entrega o lectura. Cada correo que llega queda marcado como escrito por una persona o como automático, y solo cuenta el que quedó marcado como escrito por una persona: un correo recibido antes de que existiera esta marca no cuenta. Siguen bloqueados una plantilla, un adjunto, una conversación o hilo nuevo, una campaña, un envío masivo, una respuesta con copia (cc o bcc) y un envío programado (send_at), que se rechaza al programarlo con un 422. Cada respuesta se juzga por su conversación, una a la vez: la que una aplicación conectada dejaría esperando aprobación por escribir a muchos contactos se bloquea. Cada respuesta que pasa así queda registrada con el motivo opted_out_customer_reply.

409 OUTBOUND_WARNING: la advertencia de envío

Una cortesía o un riesgo que quien envía puede asumir a sabiendas. Detiene el envío una vez.

{
  "error": {
    "reasons": ["quiet_hours", "conversation_human_owned"],
    "hint": "Es horario de silencio del espacio de trabajo. Si igual quieres enviarlo, reenvía con acknowledge: [\"quiet_hours\"].",
    "code": "OUTBOUND_WARNING",
    "message": "This message was not sent yet: the outbound policy warns about quiet_hours, conversation_human_owned. Resend the same request with \"acknowledge\": [\"quiet_hours\",\"conversation_human_owned\"] to send it anyway; the acknowledgment is recorded.",
    "details": {
      "reasons": [
        {
          "code": "quiet_hours",
          "kind": "advertencia",
          "hint": "Es horario de silencio del espacio de trabajo. Si igual quieres enviarlo, reenvía con acknowledge: [\"quiet_hours\"].",
          "detail": {
            "timezone": "UTC",
            "basis": "service",
            "basisAssumed": false,
            "localHour": 3,
            "windowStartHour": 9,
            "windowEndHour": 20,
            "retryAfter": "2026-09-22T09:00:00.000Z"
          }
        },
        {
          "code": "conversation_human_owned",
          "kind": "advertencia",
          "hint": "Un miembro del equipo está atendiendo esta conversación. Si igual quieres enviarlo, reenvía con acknowledge: [\"conversation_human_owned\"].",
          "detail": { "conversationId": "2476d000-0000-4000-8000-000000000002" }
        }
      ]
    },
    "requestId": "c29e8967-a22f-4b83-8c62-a9c5002ea77a"
  }
}

Advierten:

  • el horario de silencio del espacio de trabajo;
  • una conversación que está atendiendo un miembro del equipo;
  • una conversación cerrada;
  • un contacto archivado o fusionado;
  • un número con calidad degradada;
  • un dato que no se pudo verificar;
  • la protección contra bucles.

Reenviar con acknowledge

La misma llamada otra vez, con los códigos de error.reasons en el cuerpo:

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, te confirmo tu cita para el jueves a las 10.",
        "acknowledge": ["quiet_hours", "conversation_human_owned"]
      }'

Responde 201 con el mensaje persistido. El reconocimiento queda anotado en los logs de auditoría y en el registro de envíos del espacio de trabajo, junto al envío que justificó.

Trampa

Reconocer un código de dos vuelve a responder 409 con los dos

Reenvía con la lista completa de error.reasons, no con el primero.

Un código de bloqueo dentro de acknowledge se ignora: la respuesta es el mismo 422, palabra por palabra.

En un envío multipart (un archivo o una nota de voz), acknowledge va como un campo más del formulario, con los códigos separados por coma:

curl -X POST https://api.vitrinadev.com/api/v1/conversations/$CONVERSATION/attachments \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -F "[email protected]" \
  -F "caption=Te dejo el detalle por escrito." \
  -F "acknowledge=quiet_hours"

Nada se guarda mientras el envío no pase la política: un 422 o un 409 no deja el archivo subido a medias.

Protección contra bucles

Dos contadores por credencial, en ventanas móviles. Enviar el mismo contenido a más de veinte contactos distintos en diez minutos levanta loop_same_content. Pasar de sesenta envíos en un minuto levanta loop_burst. Los dos son advertencias: un reenvío con acknowledge sigue adelante.

Aplicaciones conectadas

Una aplicación conectada actúa con el permiso del miembro que la autorizó, y escribe a un cliente con su nombre detrás. Por eso conviene mirar uno de sus mensajes antes de que se multiplique.

Una aplicación conectada escribe directamente a hasta tres contactos distintos en diez minutos. A partir del cuarto contacto nuevo, sus envíos entran a la cola de aprobaciones como borradores «vía <aplicación>», y esperan a que un miembro del equipo los apruebe.

El cuarto contacto, entonces, responde 202:

{
  "data": {
    "status": "pending_approval",
    "action_id": "2386f55f-438e-4804-9f4b-2a1a3952d579",
    "message": "El mensaje quedó pendiente de aprobación: Asistente de ventas ya escribió a 3 contactos distintos en los últimos 10 minutos, así que un miembro del equipo debe aprobarlo en Vitrina (Seguimientos › Aprobaciones) antes de que se envíe."
  }
}

data.message viene en español y está escrito para que un asistente se lo repita a la persona con la que está trabajando. Un envío a un contacto al que ya le escribió en esa misma ventana sale directo: el límite cuenta contactos distintos, no mensajes.

El borrador aparece en la cola con la aplicación que lo escribió:

{
  "data": {
    "items": [
      {
        "actionId": "2386f55f-438e-4804-9f4b-2a1a3952d579",
        "rail": "api",
        "purpose": "conversation_reply",
        "obligationClass": "transactional_service",
        "contactId": "2476c000-0000-4000-8000-000000000006",
        "conversationId": "2476d000-0000-4000-8000-000000000006",
        "message": "Hola, te escribo para coordinar tu próxima cita.",
        "reason": "Escrito vía Asistente de ventas. Quedó en espera porque esta aplicación ya escribió a 3 contactos distintos en los últimos 10 minutos.",
        "channel": "web",
        "via": { "kind": "connected_app", "name": "Asistente de ventas" },
        "decisionVersion": 0,
        "expired": false
      }
    ],
    "total": 1
  }
}

Y un miembro lo aprueba con POST /outbound/approvals/{id}/approve:

{
  "data": {
    "outcome": "sent",
    "actionId": "2386f55f-438e-4804-9f4b-2a1a3952d579",
    "reason": null,
    "runId": null
  }
}

Aprobar vuelve a evaluar el envío con los hechos del momento, no con los de cuando se escribió el borrador. Un bloqueo que apareció mientras esperaba lo detiene aquí. Sus advertencias no: la persona que aprueba es el reconocimiento. Si el cliente escribió después de que la aplicación redactó el mensaje, la aprobación responde stale. El borrador vuelve a la cola para que alguien lea la conversación antes de decidir de nuevo.

El borrador se aprueba o se rechaza tal como la aplicación lo escribió. No se edita desde la cola.

Trampa

El conjunto de permisos que Vitrina propone al autorizar una aplicación es de solo lectura

No incluye conversations:write ni messages:send. Una aplicación que necesite escribir a clientes los pide explícitamente, y solo entonces queda sujeta a la regla de los tres contactos.

Detalles que conviene saber

Un correo programado se evalúa al programarlo. POST /conversations/{id}/messages con send_at retiene el correo ya compuesto y lo dispara después. La política lo juzga en el momento en que lo programas, con los hechos de ese momento.

La categoría de la plantilla manda sobre el cuerpo de la llamada. Una plantilla MARKETING es marketing aunque tu integración la use para confirmar algo, y arrastra el consentimiento de marketing con ella. Eso no se puede escribir en el cuerpo de la llamada: se lee de la plantilla.

Ramifica por los códigos de reasons. message y hint están escritos para una persona y pueden cambiar de redacción. Ramifica por code, como en el resto de la API; está en Errores.

En esta página