Conectar un sistema propio
Versión del contrato 1, revisada el 10/11/2026. Base: https://atencio.ar
Índice
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.
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
- El número tiene que estar conectado en ATENCIO, en Configuración → Números.
- 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.
- En Configuración → Bot → Quién contesta, elegí el modo (ver abajo) y pegá la URL de tu sistema.
- 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.
- 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. - Guardá los cambios.
Hay dos valores secretos, uno para cada dirección:
| Nombre | Dirección | Cabecera HTTP | Quién lo elige |
|---|---|---|---|
| Secreto | ATENCIO → tu sistema | x-atencio-secreto | Vos, en el panel |
| Clave | Tu sistema → ATENCIO | x-atencio-clave | ATENCIO 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 sistema | Llama a POST /api/externo/enviar cuando quiere | Devuelve la respuesta en la misma llamada |
| Tiempo máximo | Sin límite para contestar. Tenés 10 s para responder 200 al aviso | 30 s |
| Varios mensajes seguidos | Sí, un llamado por mensaje | Sí, con una lista de acciones |
Recibe los avisos eco y estado | Sí | No |
| Con la conversación pausada (la atiende una persona) | Te avisa igual, con responder: false | No te llama |
| Palabras de escalado del panel | No 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 URLTodo 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ó:
evento | Cuándo llega | Modos |
|---|---|---|
mensaje | Un cliente escribió | los dos |
eco | Alguien contestó desde el celular o la app del negocio | sólo relay |
estado | Cambió 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 URLLlega 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.
{
"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"
}| Campo | Qué es |
|---|---|
conversacion.id | El id de la conversación en ATENCIO. Es lo que usás para contestar. |
conversacion.telefono | El número del cliente en formato internacional, sólo dígitos, sin +. Con esto lo buscás en tu sistema. |
conversacion.numero | La etiqueta del número del negocio al que escribió, si el negocio tiene más de uno. |
conversacion.contacto | El nombre del cliente, si se conoce (el de su perfil de WhatsApp). Puede ser null. |
conversacion.dentroDeHorario | Según el horario cargado en el panel. |
conversacion.responder | false significa que la atiende una persona: tu sistema guarda el mensaje y NO contesta. |
conversacion.escalamiento_abierto | Hay un caso abierto en Escalamientos para esta conversación. |
conversacion.escalamiento | Só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. |
reenvio | Só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. |
mensajes | Los ú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[].tipo | text, image, audio, video, document, sticker, location, button, interactive u otros de Meta. |
mensajes[].texto | El 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_url | Sólo si trae un archivo. Se baja con la clave (ver Bajar un archivo). No es pública. |
mensajes[].transcripcion | Sólo en audios, cuando ATENCIO pudo transcribirlos. |
mensajes[].ubicacion | Só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_soportado | Só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[].fecha | Milisegundos desde 1970 (epoch en ms). |
conocimiento, reglas, bienvenida, fueraDeHorario | Lo 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 relayAlguien 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.
{
"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 relayMeta avisa qué pasó con un mensaje que salió. Llegan varios por mensaje: sent, delivered, read o failed.
{
"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:
"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
textosale como un mensaje de WhatsApp aparte. escalarabre 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 tutextosi 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/enviarTodas 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 | |
|---|---|
texto | El 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.nombre | El nombre de una plantilla aprobada de la cuenta del número por el que sale. |
plantilla.idioma | El código de idioma (es_AR). Sólo hace falta si la plantilla está en más de un idioma. |
plantilla.variables | Los 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_id | El conversacion.id del aviso. Es la forma recomendada: sale por el mismo número al que escribió el cliente. |
telefono | En lugar de conversacion_id: número internacional, sólo dígitos. Si no hay una conversación con ese teléfono, se crea. |
numero | Só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ódigo | Cuerpo | Qué 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
502traecodigo131047(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,131056u80007, 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; eltelefono, de 8 a 15 dígitos. Si necesitás más, escribinos. - Envíos repetidos: mandá la cabecera
Idempotency-Keycon 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 misma200de la primera vez, con"repetido": true. Si el primer intento terminó en502sinincierto(Meta lo rechazó o no lo procesó), el reintento con la misma clave sí sale; si terminó en502con"incierto": true, el reintento contesta409concodigoinciertoy 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/plantillasLas 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"{
"numero": "Principal",
"plantillas": [
{
"nombre": "recordatorio_turno",
"idioma": "es_AR",
"categoria": "UTILITY",
"variables": ["1", "2"],
"texto": "Hola {{1}}, te esperamos {{2}}.",
"se_puede_mandar": true
}
]
}variablesson 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}}.categoriala pone Meta:MARKETING(promociones; no salen a quien pidió la baja),UTILITY(avisos de algo que el cliente pidió) oAUTHENTICATION.se_puede_mandar: falsetrae unmotivo: 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/escalarcurl -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"}'motivoes obligatorio. Es lo que se ve en Escalamientos y en la tarjeta de Telegram: corto y claro.detallees opcional. La conversación se identifica conconversacion_id, o contelefono(y opcionalmentenumero).- Respuesta:
200 {"ok": true, "nuevo": true, "conversacion_id": 812}.nuevo: falsequiere decir que ya había un caso abierto: no se duplica. - Pausa el bot en esa conversación: los avisos siguientes llegan con
responder: falsehasta 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 conresponder: 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.jpgSi 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
- Conectar el número en ATENCIO, con el negocio creado.
- Adaptar la entrada. El endpoint que hoy recibe el webhook de Meta pasa a recibir el aviso de ATENCIO. Verificá
x-atencio-secretoen lugar de la firma de Meta (x-hub-signature-256), y leéconversacion.telefonoy los mensajes conde: "cliente"en lugar deentry[].changes[].value.messages[]. Un aviso puede traer varios mensajes seguidos del cliente: tomá todos los que llegaron después del últimoidque procesaste. Los audios ya vienen transcriptos, y los archivos se bajan demedia_urlcon la clave, no de la Graph API. Respondé200enseguida. - Adaptar la salida. Cada llamada que hoy va a
graph.facebook.com/.../messagespasa a serPOST /api/externo/enviarconconversacion_idytexto. Guardá elwam_idque devuelve si necesitás relacionarlo con los avisosestado. - 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ó. - Respetar a las personas: con
responder: falseo uneco, el bot no habla. - 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.
- En el panel: elegí "Mi sistema, que contesta cuando quiere", cargá la URL y el secreto, generá la clave y guardá.
- 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 -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 responde401. - Con
"responder": falsetu 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 Escalamientos | Qué pasó |
|---|---|
| el sistema del negocio no responde | La URL no contesta: está caída, el DNS falla o hay un problema con HTTPS. |
| el sistema del negocio no recibió el mensaje | Só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 entiende | Sólo en modo externo: tardó más de 30 s, respondió un error o una respuesta que no se entiende. |
| el asistente no está configurado | Falta la URL en el panel. |
Recibir los pedidos en tu sistema
POST → tu URL de pedidosPara 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ó:
evento | Cuándo |
|---|---|
pedido_nuevo | Se confirmó un pedido (o se aceptó, si el local acepta los pedidos a mano) |
pedido_cambiado | El cliente cambió un pedido ya mandado: llega el pedido completo como quedó |
pedido_cancelado | Se canceló un pedido ya mandado |
prueba | Tocaron «Mandar una prueba»: no lo prepares |
{
"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"
}
}referenciaes única y fija para cada pedido: usala para no cargarlo dos veces (un reintento llega con la misma).codigo_externoes el código del producto en tu sistema, si el local lo cargó (Configuración → Pedidos → Códigos de tus productos). Si vienenull, usánombreocodigo.- Los montos van en la moneda del negocio, que dice
moneda(código ISO 4217:ARSen Argentina,MXN,EUR…): en pesos argentinos, enteros; en las monedas con centavos, con hasta dos decimales (12.5).totalya tiene los descuentos restados. - Respondé
2xxcuando lo cargaste. Si devolvés JSON conid({"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.
| Herramienta | Argumentos | Devolvé |
|---|---|---|
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 archivosLos 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.
{
"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.
referenciaes única y fija para cada archivo: usala para no cargarlo dos veces (un reintento llega con la misma).textoes lo que el empleado escribió con la foto, onull.- Respondé
2xxcuando 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á2xxsin 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.