ATENCIODocumentación de la API
Versión .md

Cómo funciona

Esta guía es para conectar un bot o sistema que ya existe (un backend propio o n8n) con un número de WhatsApp que atiende ATENCIO. Está escrita para que la siga un programador o un agente de código sin más contexto.

Esquema
Cliente en WhatsApp
      │  ▲
      ▼  │
   Meta (Cloud API)
      │  ▲
      ▼  │
   ATENCIO  ── POST con la conversación ──▶  TU SISTEMA
      ▲                                           │
      └──── POST /api/externo/enviar ◀────────────┘

ATENCIO recibe todo de Meta, lo guarda, lo muestra en su bandeja y te lo pasa. Vos decidís qué contestar y ATENCIO lo manda.

Tu sistema no habla con Meta. No recibe el webhook de Meta y no manda mensajes con un token de Meta: todo pasa por ATENCIO. Si mandás por tu cuenta, ATENCIO ve salir un mensaje que no reconoce, cree que lo escribió una persona desde el celular y pausa la conversación. Además, ese mensaje no aparece en la bandeja.

A cambio, ATENCIO se encarga de esto por vos:

  • recibir de Meta y juntar los mensajes seguidos del cliente en un solo aviso;
  • guardar fotos, audios y documentos antes de que Meta los borre, y transcribir los audios;
  • detectar cuando alguien contesta desde el celular del negocio;
  • la ventana de 24 h, los estados de entrega y los errores de Meta;
  • la bandeja, los escalamientos a una persona y los avisos por Telegram.

Antes de empezar

  1. El número tiene que estar conectado en ATENCIO, en Configuración → Números.
  2. Tu sistema necesita una URL HTTPS pública que acepte POST con cuerpo JSON. Si usás n8n, el nodo Webhook viene en GET por defecto: cambialo a POST.
  3. En Configuración → Bot → Quién contesta, elegí el modo (ver abajo) y pegá la URL de tu sistema.
  4. Poné un secreto: un texto largo al azar que inventás vos. ATENCIO lo manda en cada aviso para que tu sistema sepa que viene de ATENCIO.
  5. Tocá Generar clave nueva. Es la clave con la que tu sistema llama a ATENCIO: empieza con atc_ y se muestra una sola vez. Si generás otra, la anterior deja de funcionar.
  6. Guardá los cambios.

Hay dos valores secretos, uno para cada dirección:

NombreDirecciónCabecera HTTPQuién lo elige
SecretoATENCIO → tu sistemax-atencio-secretoVos, en el panel
ClaveTu sistema → ATENCIOx-atencio-claveATENCIO la genera

Guardá los dos en variables de entorno de tu sistema, nunca en el código ni en el repositorio. En los ejemplos de esta página se llaman ATENCIO_SECRETO y ATENCIO_CLAVE.

Los dos modos

Contesta cuando quiere (relay)Contesta en el momento (externo)
Cómo contesta tu sistemaLlama a POST /api/externo/enviar cuando quiereDevuelve la respuesta en la misma llamada
Tiempo máximoSin límite para contestar. Tenés 10 s para responder 200 al aviso30 s
Varios mensajes seguidosSí, un llamado por mensajeSí, con una lista de acciones
Recibe los avisos eco y estadoSíNo
Con la conversación pausada (la atiende una persona)Te avisa igual, con responder: falseNo te llama
Palabras de escalado del panelNo se aplican (las revisa tu sistema)ATENCIO escala antes de llamarte
Necesita la clave atc_SíSólo para escalar o bajar archivos

Para un bot que ya existe, usá relay. Un bot de verdad consulta otros sistemas, manda más de un mensaje y necesita saber cuándo intervino una persona.

Verificar que el aviso viene de ATENCIO

POST → tu URL

Todo lo que te manda ATENCIO es un POST con content-type: application/json y, si configuraste el secreto, la cabecera x-atencio-secreto. Verificalo antes de hacer cualquier otra cosa, con una comparación en tiempo constante, y si no coincide respondé 401.

import crypto from "node:crypto";

function vieneDeAtencio(req) {
  const recibido = Buffer.from(req.headers["x-atencio-secreto"] ?? "");
  const esperado = Buffer.from(process.env.ATENCIO_SECRETO);
  return recibido.length === esperado.length && crypto.timingSafeEqual(recibido, esperado);
}

Todos los cuerpos traen version y evento. Mirá evento para saber qué llegó:

eventoCuándo llegaModos
mensajeUn cliente escribiólos dos
ecoAlguien contestó desde el celular o la app del negociosólo relay
estadoCambió el estado de un mensaje que salió (entregado, leído, fallido)sólo relay

Si llega un evento que no conocés, respondé 200 y descartalo. Si algún día cambia la forma de lo que ya existe, version va a subir.

Evento mensaje

POST → tu URL

Llega cuando un cliente escribe. ATENCIO espera unos segundos (7 por defecto) después del último mensaje del cliente y te manda un solo aviso con todo. Si el cliente escribió "hola", "quería saber" y "el precio" seguidos, te llega un aviso, no tres.

JSON
{
  "version": 1,
  "evento": "mensaje",
  "negocio": { "id": 6, "nombre": "Entre Ríos Net" },
  "conversacion": {
    "id": 812,
    "telefono": "5493446123456",
    "numero": "Principal",
    "contacto": "Ana",
    "dentroDeHorario": true,
    "responder": true,
    "escalamiento_abierto": false,
    "escalamiento": null
  },
  "reenvio": false,
  "mensajes": [
    { "id": 10401, "de": "cliente", "texto": "Hola, no tengo internet", "tipo": "text", "fecha": 1790000000000 },
    { "id": 10402, "de": "negocio", "texto": "Hola Ana, ¿las luces del equipo están prendidas?", "tipo": "text", "fecha": 1790000005000 },
    { "id": 10403, "de": "cliente", "texto": "", "tipo": "image", "fecha": 1790000060000,
      "media_url": "https://atencio.ar/api/externo/media/10403" },
    { "id": 10404, "de": "cliente", "texto": "", "tipo": "audio", "fecha": 1790000065000,
      "media_url": "https://atencio.ar/api/externo/media/10404",
      "transcripcion": "ahí te mandé la foto, la de internet está en rojo" }
  ],
  "conocimiento": [{ "titulo": "Planes", "contenido": "..." }],
  "reglas": "texto de Configuración → Bot → Reglas propias, o null",
  "bienvenida": "texto o null",
  "fueraDeHorario": "texto o null"
}
CampoQué es
conversacion.idEl id de la conversación en ATENCIO. Es lo que usás para contestar.
conversacion.telefonoEl número del cliente en formato internacional, sólo dígitos, sin +. Con esto lo buscás en tu sistema.
conversacion.numeroLa etiqueta del número del negocio al que escribió, si el negocio tiene más de uno.
conversacion.contactoEl nombre del cliente, si se conoce (el de su perfil de WhatsApp). Puede ser null.
conversacion.dentroDeHorarioSegún el horario cargado en el panel.
conversacion.responderfalse significa que la atiende una persona: tu sistema guarda el mensaje y NO contesta.
conversacion.escalamiento_abiertoHay un caso abierto en Escalamientos para esta conversación.
conversacion.escalamientoSólo en modo relay: el caso abierto, {"motivo": "el sistema del negocio no responde", "desde": 1790000000000}, o null. Para cargarlo en tu propia lista de escalamientos.
reenvioSólo en modo relay. true: este aviso no te había llegado (tu sistema estaba caído o respondió un error) y ATENCIO te lo vuelve a mandar ahora. Ver Si tu sistema no recibe un aviso.
mensajesLos últimos mensajes de los dos lados, del más viejo al más nuevo (hasta 44). Los del negocio incluyen lo que contestó tu sistema y lo que escribieron las personas.
mensajes[].de"cliente" o "negocio".
mensajes[].tipotext, image, audio, video, document, sticker, location, button, interactive u otros de Meta.
mensajes[].textoEl texto. En una foto o un video es el pie, y puede venir vacío. En un audio viene vacío: usá transcripcion.
mensajes[].media_urlSólo si trae un archivo. Se baja con la clave (ver Bajar un archivo). No es pública.
mensajes[].transcripcionSólo en audios, cuando ATENCIO pudo transcribirlos.
mensajes[].ubicacionSólo en location: {"lat": -33.0066, "lng": -58.5422, "nombre": "Plaza San Martín", "direccion": "Av. Rivadavia 100"}. lat y lng son números y vienen siempre, también cuando el cliente eligió un lugar en vez de mandar dónde está. nombre y direccion vienen sólo con un lugar elegido; si no, null. El texto sigue trayendo [ubicación lat, lng], con el nombre y la dirección delante si los hay.
mensajes[].no_soportadoSólo en unsupported (algo que la API de WhatsApp no deja ver): {"tipo": "unknown", "codigo": "131060"}, lo que dice Meta. El tipo casi siempre es unknown. La ubicación en tiempo real no llega: Meta la manda así, sin las coordenadas, y en la prueba vino con el código 131060 (otros mensajes que no se pueden ver vinieron con 131051). Si tu bot necesita la ubicación, que pida la actual («Enviar tu ubicación actual»), no la de tiempo real.
mensajes[].fechaMilisegundos desde 1970 (epoch en ms).
conocimiento, reglas, bienvenida, fueraDeHorarioLo que el negocio cargó en el panel. Usalo si tu bot lo necesita; si no, ignoralo.

El mensaje nuevo es el último con de: "cliente". Guardá el id del último mensaje que procesaste por conversación y no proceses dos veces el mismo: así, si un aviso te llega repetido, no contestás dos veces.

Evento eco

POST → tu URL · sólo relay

Alguien del negocio contestó desde el celular (coexistence) o desde la app de WhatsApp Business. ATENCIO ya pausó la conversación: los avisos siguientes van a traer responder: false.

JSON
{
  "version": 1,
  "evento": "eco",
  "conversacion": { "id": 812, "telefono": "5493446123456" },
  "texto": "Ana, ya te mando un técnico",
  "tipo": "text",
  "fecha": 1790000100000
}

Si el eco trae un archivo, también viene media_url; si es una ubicación, ubicacion, como en el evento mensaje. Qué hacer: que tu bot deje de hablar en esa conversación. Si guarda su propio estado, que la marque como atendida por una persona.

Si la persona contesta desde la Bandeja de ATENCIO (no desde el celular), no llega un eco, pero la conversación queda pausada igual y el próximo aviso trae responder: false. Por eso la regla es siempre la misma: mirá responder en cada aviso.

Evento estado

POST → tu URL · sólo relay

Meta avisa qué pasó con un mensaje que salió. Llegan varios por mensaje: sent, delivered, read o failed.

JSON
{
  "version": 1,
  "evento": "estado",
  "conversacion": { "telefono": "5493446123456" },
  "wam_id": "wamid.HBgN...",
  "estado": "failed",
  "error_codigo": "131047",
  "error_texto": "Re-engagement message",
  "fecha": 1790000200000
}

wam_id es el mismo que te devolvió /api/externo/enviar. El que importa es failed: el mensaje no llegó aunque /enviar te haya contestado 200. El más común es 131047, que significa que pasaron más de 24 h desde el último mensaje del cliente. Los demás los podés ignorar.

Qué contestarle a ATENCIO

En modo relay

Respondé 200 en menos de 10 s, con cualquier cuerpo, y procesá después. Lo que tengas para decirle al cliente lo mandás con /api/externo/enviar. Si respondés un error o no respondés, ATENCIO te lo vuelve a mandar cuando tu sistema ande: ver abajo. Una redirección 307 o 308 se sigue con el mismo POST; una 301, 302 o 303 no (perdería el cuerpo): cuenta como no recibido, así que poné en Conexión la URL final.

Si tu sistema no recibe un aviso

Si tu sistema está caído, tarda más de 10 s o responde un error, la conversación queda guardada y ATENCIO te reenvía el aviso cuando tu sistema vuelve a andar, con "reenvio": true y el historial al día. Mientras falla, prueba con una conversación por minuto; cuando una llega, sigue con las demás de a 10 por minuto, para no volver a tumbarlo. Pasadas 24 horas no se reenvía. Si mientras tanto el cliente vuelve a escribir y ese aviso llega, ya no hace falta reenviarlo.

Puede llegar repetido: un aviso que tardó más de 10 s pudo llegarte igual. Usá el id de cada mensaje para no contestar dos veces lo mismo.

En Conexión elegís además si ATENCIO pasa esas conversaciones a una persona (de entrada, no) y qué le dice al cliente (vacío, nada). Si las pasa, el reenvío te llega igual, con responder: false y el caso en conversacion.escalamiento: así ves en tu sistema cuáles escalaron.

En modo externo

La respuesta de tu POST es lo que se le contesta al cliente. Respondé 200 con alguna de estas formas; todas valen:

Respuestas válidas
"Hola Ana, ya lo revisamos"                        // texto plano (también como text/plain)
{ "texto": "Hola Ana, ya lo revisamos" }           // también valen "mensaje", "respuesta" u "output"
{ "tipo": "texto", "texto": "Hola Ana" }
{ "tipo": "escalar", "motivo": "pide hablar con un técnico",
  "detalle": "sin servicio desde ayer", "texto": "Te paso con una persona" }
{ "escalar": "pide hablar con un técnico" }
{ "tipo": "nada" }                                 // no contestar nada
[ { "texto": "Primer mensaje" }, { "texto": "Segundo mensaje" } ]
{ "acciones": [ { "texto": "..." }, { "tipo": "escalar", "motivo": "..." } ] }
[ { "json": { "texto": "..." } } ]                 // lo que devuelve n8n, tal cual
  • Cada texto sale como un mensaje de WhatsApp aparte.
  • escalar abre un caso en Escalamientos y pausa el bot en esa conversación hasta que alguien lo resuelva. Al cliente se le manda el aviso de escalado del panel, o tu texto si el negocio no configuró ninguno.
  • Si pasan más de 30 s, respondés algo distinto de 2xx, o el cuerpo viene vacío o no se entiende, ATENCIO escala la conversación a una persona. Nunca se queda callado.

Mandar un mensaje

POST /api/externo/enviar

Todas las llamadas a ATENCIO llevan la cabecera x-atencio-clave. La clave va sólo del lado del servidor: nunca en un navegador ni en una app. Si falta o es incorrecta, la respuesta es 401 {"error": "Clave inválida"}.

curl -X POST https://atencio.ar/api/externo/enviar \
  -H "x-atencio-clave: $ATENCIO_CLAVE" \
  -H "content-type: application/json" \
  -d '{"conversacion_id":812,"texto":"Hola Ana, ya lo revisamos"}'

Para escribirle a alguien que no te escribió en las últimas 24 h (un recordatorio de turno, un aviso al otro día) hace falta una plantilla aprobada por Meta: en lugar de texto, mandá plantilla. Las plantillas se crean en el panel, en Plantillas, y las que tenés las devuelve GET /api/externo/plantillas.

curl -X POST https://atencio.ar/api/externo/enviar \
  -H "x-atencio-clave: $ATENCIO_CLAVE" \
  -H "content-type: application/json" \
  -d '{"telefono":"5493446123456","plantilla":{"nombre":"recordatorio_turno","variables":["Ana","mañana a las 10:30"]}}'
Campo
textoEl mensaje, sólo texto (no se pueden mandar imágenes ni documentos). Sale sólo dentro de las 24 h desde el último mensaje del cliente. Va texto o plantilla, no los dos.
plantilla.nombreEl nombre de una plantilla aprobada de la cuenta del número por el que sale.
plantilla.idiomaEl código de idioma (es_AR). Sólo hace falta si la plantilla está en más de un idioma.
plantilla.variablesLos valores de las variables: una lista en el orden en que aparecen (primero las del encabezado, después las del cuerpo), o un objeto por nombre ({"nombre": "Ana"}). Texto sin saltos de línea. Sin variables, no hace falta.
conversacion_idEl conversacion.id del aviso. Es la forma recomendada: sale por el mismo número al que escribió el cliente.
telefonoEn lugar de conversacion_id: número internacional, sólo dígitos. Si no hay una conversación con ese teléfono, se crea.
numeroSólo junto con telefono: la etiqueta del número del negocio desde el que sale. Si no la mandás, sale por el principal.
CódigoCuerpoQué significa
200{"ok": true, "wam_id": "wamid...", "conversacion_id": 812}Meta lo aceptó. Todavía puede fallar después: mirá los avisos estado.
400{"error": "Falta texto o plantilla"} (u otro dato)El pedido está mal armado: falta un dato, la plantilla lleva otra cantidad de variables, no está aprobada o tiene algo que ATENCIO todavía no manda (una imagen en el encabezado). No reintentes.
401{"error": "Clave inválida"}Revisá o regenerá la clave.
402{"error": "...", "codigo": "plan"}El plan de la cuenta no está al día (falta la tarjeta, se venció la prueba o se dio de baja). No sale nada hasta regularizarlo en Configuración → Plan.
404{"error": "Conversación inexistente"}El id no existe o es de otro negocio, o la plantilla no existe en la cuenta de ese número.
429{"error": "...", "codigo": "limite"}Pasaste un límite (ver abajo). La cabecera retry-after dice cuántos segundos esperar. No sale nada.
409{"error": "...", "codigo": "baja"}Es una plantilla de marketing y ese cliente pidió no recibir promociones. No sale. Las de utilidad (un turno, un pedido) sí.
409{"error": "...", "codigo": "en_curso"}Con Idempotency-Key: ya hay un envío con esa clave en curso, o se cortó sin saber si salió. Mirá la conversación antes de mandarlo con otra clave.
409{"error": "...", "codigo": "incierto"}Con Idempotency-Key: el primer intento con esa clave terminó en un 502 con "incierto": true (pudo haber salido). No se manda de nuevo: mirá la conversación (o el aviso estado) antes de mandarlo con otra clave.
422{"error": "...", "codigo": "clave_usada"}Con Idempotency-Key: esa clave ya se usó con otro mensaje en las últimas 24 h. No sale nada.
502{"error": "...", "codigo": "131047"}Meta lo rechazó. El mensaje queda en la bandeja marcado como fallido.
502{"error": "...", "codigo": null, "incierto": true}Meta no contestó a tiempo o dio un error suyo (5xx): el mensaje pudo haber salido. No lo reintentes a ciegas: el cliente lo recibiría dos veces (y Meta lo cobraría dos veces).
  • Si el 502 trae codigo 131047 (más de 24 h desde el último mensaje del cliente), reintentar el texto no sirve: mandá una plantilla.
  • Una plantilla queda en la bandeja como la ve el cliente, con las variables puestas.
  • Si trae 130429, 131056 u 80007, Meta pide ir más despacio: esperá unos segundos y reintentá una vez.
  • Límites, por cuenta (todas las claves y números juntos): 60 mensajes por minuto, 1.000 por hora y, de ésos, 200 plantillas de marketing por hora. El texto, hasta 4.096 caracteres; el telefono, de 8 a 15 dígitos. Si necesitás más, escribinos.
  • Envíos repetidos: mandá la cabecera Idempotency-Key con un valor único por mensaje (el id del turno o del pedido en tu sistema, o un UUID; hasta 200 caracteres visibles, sin espacios). Si reintentás con la misma clave algo que ya salió, no se manda de nuevo: la respuesta es la misma 200 de la primera vez, con "repetido": true. Si el primer intento terminó en 502 sin incierto (Meta lo rechazó o no lo procesó), el reintento con la misma clave sí sale; si terminó en 502 con "incierto": true, el reintento contesta 409 con codigo incierto y no sale. La clave vale 24 h. Sin la cabecera no hay control: si reintentás algo que en realidad salió, el cliente lo recibe dos veces (y Meta lo cobra dos veces).

Antes de mandar, respetá responder. Si en el último aviso de esa conversación vino responder: false, o te llegó un eco, no mandes: la está atendiendo una persona. ATENCIO no te frena si mandás igual, y el mensaje se pisa con lo que está escribiendo la persona.

Cada mensaje se paga. Desde el 1/10/2026 Meta cobra cada mensaje que sale, también los que contestan dentro de las 24 h, y lo paga el negocio. Mandá una respuesta completa en un solo mensaje, no tres cortos seguidos.

Ver las plantillas

GET /api/externo/plantillas

Las plantillas aprobadas de la cuenta de un número, con sus variables: lo que necesitás para armar plantilla en /enviar. Con ?numero=Etiqueta, las de ese número; sin nada, las del principal.

curl https://atencio.ar/api/externo/plantillas \
  -H "x-atencio-clave: $ATENCIO_CLAVE"
Respuesta
{
  "numero": "Principal",
  "plantillas": [
    {
      "nombre": "recordatorio_turno",
      "idioma": "es_AR",
      "categoria": "UTILITY",
      "variables": ["1", "2"],
      "texto": "Hola {{1}}, te esperamos {{2}}.",
      "se_puede_mandar": true
    }
  ]
}
  • variables son los nombres de las variables en el orden en que van en la lista de /enviar: los números de {{1}}, {{2}}, o los nombres si la plantilla usa {{nombre}}.
  • categoria la pone Meta: MARKETING (promociones; no salen a quien pidió la baja), UTILITY (avisos de algo que el cliente pidió) o AUTHENTICATION.
  • se_puede_mandar: false trae un motivo: es una plantilla aprobada con algo que ATENCIO todavía no manda, como una imagen en el encabezado.

Cada plantilla se cobra según su categoría, y Meta mira cómo reaccionan los clientes: si muchos la bloquean o la reportan, baja la calidad del número y puede limitar cuántos mensajes manda por día. Mandá plantillas a quien las espera.

Pasarle la conversación a una persona

POST /api/externo/escalar
curl -X POST https://atencio.ar/api/externo/escalar \
  -H "x-atencio-clave: $ATENCIO_CLAVE" \
  -H "content-type: application/json" \
  -d '{"conversacion_id":812,"motivo":"pide un técnico","detalle":"sin servicio desde ayer, luz LOS en rojo"}'
  • motivo es obligatorio. Es lo que se ve en Escalamientos y en la tarjeta de Telegram: corto y claro.
  • detalle es opcional. La conversación se identifica con conversacion_id, o con telefono (y opcionalmente numero).
  • Respuesta: 200 {"ok": true, "nuevo": true, "conversacion_id": 812}. nuevo: false quiere decir que ya había un caso abierto: no se duplica.
  • Pausa el bot en esa conversación: los avisos siguientes llegan con responder: false hasta que alguien marca el caso como resuelto en el panel.
  • En un negocio dado de alta por un integrador (sin panel ni usuarios), nadie puede resolver el caso en ATENCIO: el caso queda tomado y la pausa dura lo que la de una persona (pausa_humano_min, 30 minutos de entrada). Al vencer, el caso se resuelve solo y los avisos vuelven a llegar con responder: true.

A diferencia del modo externo, esta llamada no le manda nada al cliente. Si querés avisarle ("te paso con una persona"), mandalo antes con /api/externo/enviar.

Bajar un archivo

GET /api/externo/media/{id}

Es el media_url que vino en el aviso. Devuelve los bytes del archivo con su content-type.

curl https://atencio.ar/api/externo/media/10403 \
  -H "x-atencio-clave: $ATENCIO_CLAVE" \
  -o foto.jpg

Si el mensaje no existe, no tiene archivo o es de otro negocio, devuelve 404. ATENCIO guarda los archivos por su cuenta, así que no vencen a los 7 días como los de Meta.

Ejemplo completo en modo relay

Un servidor mínimo que recibe el aviso, lo verifica, responde 200 enseguida y contesta después. pensarRespuesta es tu bot.

import express from "express";
import crypto from "node:crypto";

const app = express();
app.use(express.json({ limit: "2mb" }));
const procesados = new Map(); // conversación → id del último mensaje procesado

app.post("/atencio", (req, res) => {
  const recibido = Buffer.from(req.headers["x-atencio-secreto"] ?? "");
  const esperado = Buffer.from(process.env.ATENCIO_SECRETO);
  if (recibido.length !== esperado.length || !crypto.timingSafeEqual(recibido, esperado)) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // primero el 200: ATENCIO espera 10 s como mucho
  atender(req.body).catch(console.error);
});

async function atender(aviso) {
  if (aviso.evento === "eco") return marcarComoAtendidaPorPersona(aviso.conversacion.id);
  if (aviso.evento !== "mensaje") return;
  const { conversacion, mensajes } = aviso;
  if (!conversacion.responder) return; // la atiende una persona

  const ultimo = procesados.get(conversacion.id) ?? 0;
  const nuevos = mensajes.filter((m) => m.de === "cliente" && m.id > ultimo);
  if (!nuevos.length) return; // aviso repetido
  procesados.set(conversacion.id, nuevos.at(-1).id);

  const texto = await pensarRespuesta(conversacion, mensajes); // tu bot
  await fetch("https://atencio.ar/api/externo/enviar", {
    method: "POST",
    headers: { "x-atencio-clave": process.env.ATENCIO_CLAVE, "content-type": "application/json" },
    body: JSON.stringify({ conversacion_id: conversacion.id, texto }),
  });
}

app.listen(3000);

En producción, guardá procesados en tu base y no en memoria: si tu servidor se reinicia, no tiene que volver a contestar lo que ya contestó.

Pasar a ATENCIO un bot que ya habla con Meta

  1. Conectar el número en ATENCIO, con el negocio creado.
  2. Adaptar la entrada. El endpoint que hoy recibe el webhook de Meta pasa a recibir el aviso de ATENCIO. Verificá x-atencio-secreto en lugar de la firma de Meta (x-hub-signature-256), y leé conversacion.telefono y los mensajes con de: "cliente" en lugar de entry[].changes[].value.messages[]. Un aviso puede traer varios mensajes seguidos del cliente: tomá todos los que llegaron después del último id que procesaste. Los audios ya vienen transcriptos, y los archivos se bajan de media_url con la clave, no de la Graph API. Respondé 200 enseguida.
  3. Adaptar la salida. Cada llamada que hoy va a graph.facebook.com/.../messages pasa a ser POST /api/externo/enviar con conversacion_id y texto. Guardá el wam_id que devuelve si necesitás relacionarlo con los avisos estado.
  4. Pasar los avisos a personas por ATENCIO. Si el bot hoy avisa a un grupo de Telegram, un mail o algo propio, cambialo por /api/externo/escalar: el caso aparece en Escalamientos y en los avisos de Telegram de ATENCIO, con quién lo tomó y cuándo se resolvió.
  5. Respetar a las personas: con responder: false o un eco, el bot no habla.
  6. Apagar la conexión vieja con Meta. Si la app de Meta anterior sigue suscripta a la misma cuenta de WhatsApp, a tu sistema le llegan los mensajes dos veces (de Meta y de ATENCIO) y el cliente recibe dos respuestas. Desuscribí la app vieja o desactivá su webhook, y sacá el token de Meta de tu sistema: ya no lo necesita.
  7. En el panel: elegí "Mi sistema, que contesta cuando quiere", cargá la URL y el secreto, generá la clave y guardá.
  8. Probar con un teléfono que no sea el del negocio (ver la sección siguiente).

Probar

Sin tocar WhatsApp, simulá un aviso contra tu sistema:

cURL
curl -X POST "$TU_URL" \
  -H "content-type: application/json" \
  -H "x-atencio-secreto: $ATENCIO_SECRETO" \
  -d '{"version":1,"evento":"mensaje","negocio":{"id":0,"nombre":"Prueba"},
       "conversacion":{"id":1,"telefono":"5490000000000","numero":"Principal","contacto":"Prueba",
         "dentroDeHorario":true,"responder":true,"escalamiento_abierto":false,"escalamiento":null},"reenvio":false,
       "mensajes":[{"id":1,"de":"cliente","texto":"hola","tipo":"text","fecha":0}],
       "conocimiento":[],"reglas":null,"bienvenida":null,"fueraDeHorario":null}'
  • Sin la cabecera x-atencio-secreto, o con otro valor, tu sistema responde 401.
  • Con "responder": false tu sistema no contesta.
  • De punta a punta: escribile al número desde otro teléfono y confirmá en la Bandeja de ATENCIO que aparecen el mensaje del cliente y la respuesta de tu sistema.

Si algo falla, la conversación aparece en Escalamientos con el motivo (en modo relay, sólo si lo elegiste en Conexión: ver Si tu sistema no recibe un aviso):

Motivo en EscalamientosQué pasó
el sistema del negocio no respondeLa URL no contesta: está caída, el DNS falla o hay un problema con HTTPS.
el sistema del negocio no recibió el mensajeSólo en modo relay: tu sistema respondió un error, por ejemplo 401 por un secreto mal copiado. El código aparece en el detalle.
el flujo del negocio no respondió / falló / devolvió algo que no se entiendeSólo en modo externo: tardó más de 30 s, respondió un error o una respuesta que no se entiende.
el asistente no está configuradoFalta la URL en el panel.

Recibir los pedidos en tu sistema

POST → tu URL de pedidos

Para los negocios que toman pedidos con el asistente de ATENCIO: cada pedido que confirma un cliente (por WhatsApp o desde la web del local) entra solo a tu sistema (tu POS, tu sistema de delivery). Se configura en Configuración → Pedidos → Mandar los pedidos a tu sistema, de dos formas: por API (ATENCIO le hace un POST a tu URL) o por MCP (ATENCIO llama a la herramienta crear_pedido de tu app, conectada en Conectar MCP). Lo manda el programa, no el modelo: es el pedido como lo calculó ATENCIO, con el sí del cliente.

Por API

Un POST con content-type: application/json y la clave en x-atencio-secreto (la ves en esa misma pantalla; verificala como en Verificar que el aviso viene de ATENCIO). El campo evento dice qué pasó:

eventoCuándo
pedido_nuevoSe confirmó un pedido (o se aceptó, si el local acepta los pedidos a mano)
pedido_cambiadoEl cliente cambió un pedido ya mandado: llega el pedido completo como quedó
pedido_canceladoSe canceló un pedido ya mandado
pruebaTocaron «Mandar una prueba»: no lo prepares
JSON
{
  "evento": "pedido_nuevo",
  "pedido": {
    "referencia": "atencio-12-3456",
    "numero": 142,
    "estado": "confirmado",
    "origen": "whatsapp",
    "tipo": "delivery",
    "pago": "transferencia",
    "cobro": "esperando",
    "cliente": { "nombre": "Juan Pérez", "telefono": "5493445123456" },
    "direccion": "Neyra 504, timbre 2",
    "notas": "Sin cebolla en la hamburguesa",
    "para_las": "21:30",
    "items": [
      { "codigo": "pizza-maldita", "codigo_externo": "PZ-01", "nombre": "Pizza Maldita",
        "opcion": "Grande", "adicionales": null, "cantidad": 1,
        "precio_unitario": 39300, "subtotal": 39300, "notas": null }
    ],
    "descuentos": [{ "texto": "Martes 10% en delivery", "monto": 3930 }],
    "total": 35370,
    "moneda": "ARS",
    "confirmado": "2026-10-22T00:41:00.000Z"
  }
}
  • referencia es única y fija para cada pedido: usala para no cargarlo dos veces (un reintento llega con la misma).
  • codigo_externo es el código del producto en tu sistema, si el local lo cargó (Configuración → Pedidos → Códigos de tus productos). Si viene null, usá nombre o codigo.
  • Los montos van en la moneda del negocio, que dice moneda (código ISO 4217: ARS en Argentina, MXN, EUR…): en pesos argentinos, enteros; en las monedas con centavos, con hasta dos decimales (12.5). total ya tiene los descuentos restados.
  • Respondé 2xx cuando lo cargaste. Si devolvés JSON con id ({"id": "A-981"}), el local lo ve en el pedido.
  • Cualquier otra respuesta (o ninguna en 20 segundos) se reintenta varias veces. Si no entra, el pedido queda igual en ATENCIO, marcado «No entró a tu sistema», y le avisa al local por Telegram para que lo cargue a mano.

Por MCP

Si tu app tiene un servidor MCP, ATENCIO llama a estas herramientas con el mismo pedido de arriba. Sólo crear_pedido es obligatoria; sin las otras, cuando un pedido cambia o se cancela, el local recibe un aviso para hacerlo a mano.

HerramientaArgumentosDevolvé
crear_pedido{ pedido }{ "id": "…" } con el id en tu sistema (opcional)
modificar_pedido{ referencia, id_externo, pedido }cualquier cosa; un error (isError) se reintenta
cancelar_pedido{ referencia, id_externo }cualquier cosa

Recibir los archivos de tu equipo

POST → tu URL de archivos

Los contactos que el local marca como empleados (Configuración → No contestar y equipo) no reciben respuestas del asistente, y cada foto o PDF que mandan al WhatsApp del local se le pasa a tu sistema apenas llega: por ejemplo, las facturas de los proveedores, para cargarlas en tu sistema de gestión. ATENCIO le hace un POST a la dirección que cargó el local, con la clave en el encabezado x-atencio-secreto, y el archivo adentro.

JSON
{
  "evento": "archivo_empleado",
  "referencia": "atencio-8-m12345",
  "empleado": { "telefono": "5493446000000", "nombre": "Juan (cocina)" },
  "archivo": { "nombre": "whatsapp-12345.jpg", "tipo": "image/jpeg", "contenido_base64": "/9j/4AAQ…" },
  "texto": "factura de la carne",
  "recibido": "2026-10-26T19:40:00.000Z"
}
  • Van fotos (JPG, PNG, WEBP) y PDF de hasta 12 MB. Audios, videos y textos no.
  • referencia es única y fija para cada archivo: usala para no cargarlo dos veces (un reintento llega con la misma).
  • texto es lo que el empleado escribió con la foto, o null.
  • Respondé 2xx cuando lo guardaste. Cualquier otra respuesta (o ninguna en 30 segundos) se reintenta varias veces.
  • El botón «Mandar una prueba» manda "evento": "prueba" con "archivo": null: contestá 2xx sin guardar nada.
  • Si el local cargó un mensaje para el empleado («📄 Recibida, queda para revisar»), ATENCIO se lo manda cuando tu sistema contestó 2xx.

Lo que todavía no existe

  • Mandar imágenes o documentos por la API: sale texto o una plantilla. Tampoco plantillas con una imagen, video o documento en el encabezado, ni con un botón de enlace variable.
  • La firma HMAC de los avisos. Hoy se autentican con el secreto fijo en x-atencio-secreto; usá HTTPS siempre.