# domi en línea · API de WhatsApp por coexistencia > Conecta el número que un negocio ya usa en la app WhatsApp Business a la API oficial de > WhatsApp de Meta (Cloud API), sin dejar de usar el celular. El software del negocio entra por > https://api.domienlinea.com, con las mismas rutas y cuerpos JSON de la Graph API, y su IA entra > por un servidor MCP en https://api.domienlinea.com/mcp. Las dos usan la misma llave. Servicio de domienlinea.com by Daniel Montoya, Medellín (Colombia). Cuesta US$50 al mes o US$550 al año, cobrados en pesos colombianos ($162.000 COP o $1.780.000 COP): https://domienlinea.com/whatsapp-api/. La conexión, la llave, el webhook y la renovación se manejan desde el panel del negocio en https://conectar.domienlinea.com/panel. ## La llave - Va en cada petición, API o MCP: `Authorization: Bearer dl_live_…`. Reemplaza al token de Meta, que nunca sale de nuestro servidor. - Se crea en el panel y se muestra una sola vez. «Cambiar la llave» crea otra y la anterior deja de servir en el acto: es lo que hay que hacer si se filtra. - El panel también muestra el `phone_number_id` y el `waba_id` del número. - Es un secreto: va en variables de entorno o en el gestor de secretos, nunca en el código ni en el navegador. ## La API espejo Base: `https://api.domienlinea.com`. Cambia el dominio y la llave; lo demás es igual que en Meta: el cuerpo y la respuesta pasan tal cual y la versión (`v25.0` u otra `vNN.0`) también. Solo se permiten estas rutas, y solo sobre el número y la cuenta (WABA) de la llave: | Método | Ruta | Para qué | |---|---|---| | POST | `/v25.0/{phone_number_id}/messages` | Mandar mensajes: texto, plantilla, medios, interactivos, reacciones, marcar como leído | | POST | `/v25.0/{phone_number_id}/media` | Subir un medio (multipart/form-data, hasta 100 MB por petición) | | GET | `/v25.0/{phone_number_id}` | Datos del número: nombre, calidad, límites | | GET | `/v25.0/{media_id}` | Datos de un medio. La `url` viene cambiada por una nuestra (abajo) | | DELETE | `/v25.0/{media_id}` | Borrar un medio | | GET, POST, DELETE | `/v25.0/{waba_id}/message_templates` | Listar, crear y borrar plantillas | | GET | `/descargar/{id}` | Bajar el archivo de un medio, con la misma llave. Vence a los 5 minutos | Cualquier otra ruta responde 404 sin llegar a Meta: perfil de empresa, registrar o dar de baja el número, otros números de la cuenta, suscripciones de webhooks, etc. Un número o una cuenta que no son de la llave responden 403. Ejemplo: ``` POST https://api.domienlinea.com/v25.0/{phone_number_id}/messages Authorization: Bearer dl_live_xxxxxxxxxxxx Content-Type: application/json {"messaging_product": "whatsapp", "to": "573001234567", "type": "text", "text": {"body": "Tu pedido ya salió"}} ``` ### Errores Los de Meta llegan tal cual, con su código HTTP. Los nuestros tienen la misma forma, con `"type": "DomienlineaError"`: ``` {"error": {"message": "…", "type": "DomienlineaError", "code": 409}} ``` | HTTP | `code` | Cuándo | |---|---|---| | 401 | 190 | Falta la llave o no existe (también si el negocio se desconectó desde la app) | | 402 | 402 | El plan no está activo: venció hace más de 3 días o está pausado. El mensaje trae el enlace para renovar | | 403 | 10 | El número o la cuenta no son de esta llave | | 404 | 100 | Ruta no disponible en la pasarela, o enlace de descarga vencido | | 409 | 409 | El número no está conectado: todavía no se conectó, o Meta lo desconectó (ver «la regla de los 14 días») | | 429 | 4 | Más de 20 peticiones en un segundo | | 502 | 2 | Meta no respondió en 60 segundos | ## El webhook Lo que Meta manda para el número se reenvía al webhook del negocio, con el mismo cuerpo de Meta (`"object": "whatsapp_business_account"`, `entry[].changes[]`). Pasan los campos `messages` (mensajes entrantes y estados de entrega: sent, delivered, read, failed), `smb_message_echoes` (lo que el equipo escribe desde el celular), `history` (el historial al conectar), `smb_app_state_sync` (contactos) y `message_template_status_update` (plantillas aprobadas o rechazadas). - **Dónde:** se pone en el panel. Tiene que ser https, en el puerto 443 y con IP pública. - **Verificación:** igual que la de Meta. Le hacemos un GET con `hub.mode=subscribe`, `hub.verify_token` (el que el negocio escribe en el panel) y `hub.challenge`; hay que responder 200 con el `hub.challenge` como cuerpo. - **Firma:** cada POST trae `X-Hub-Signature-256: sha256=`, el HMAC-SHA256 del cuerpo crudo calculado con el **secreto de firma del panel** (no con el secreto de una app de Meta). Hay que verificarlo sobre los bytes tal como llegan, antes de parsear el JSON, y comparar en tiempo constante. Si cambias el secreto en el panel, el anterior deja de usarse. - **Respuesta:** un 2xx en menos de 10 segundos. Si no, se reintenta a los 10 s, 1 min, 5 min, 30 min y 1 h, y después cada hora, hasta 24 horas; ahí el evento se borra. Una redirección cuenta como fallo. Un evento puede llegar dos veces: deduplica por el `id` del mensaje (wamid). - **Plan vencido:** los eventos se descartan sin guardar. - **Prueba:** el botón «Mandar un evento de prueba» del panel manda un mensaje entrante de ejemplo. - **Contenido:** no guardamos los mensajes. Solo esperan en la cola de reintentos, hasta 24 horas. Verificar la firma en Node: ```js import { createHmac, timingSafeEqual } from 'node:crypto'; function firmaValida(cuerpoCrudo, cabecera, secreto) { const esperada = Buffer.from('sha256=' + createHmac('sha256', secreto).update(cuerpoCrudo).digest('hex')); const llegada = Buffer.from(String(cabecera ?? '')); return esperada.length === llegada.length && timingSafeEqual(esperada, llegada); } ``` En Python: ```python import hashlib, hmac def firma_valida(cuerpo_crudo: bytes, cabecera: str, secreto: str) -> bool: esperada = 'sha256=' + hmac.new(secreto.encode(), cuerpo_crudo, hashlib.sha256).hexdigest() return hmac.compare_digest(esperada, cabecera or '') ``` ## Límites y reglas de Meta - **20 mensajes por segundo** por número, el tope de Meta en coexistencia. Contamos todas las peticiones de la llave, por la API y por el MCP juntas; pasarse responde 429. - **Ventana de 24 horas:** el texto libre solo se entrega a quien le escribió al negocio en las últimas 24 horas. A los demás se les escribe con una plantilla aprobada. Meta acepta el envío igual (devuelve un `wamid`) y el fallo llega después al webhook, como estado `failed` con el error 131047. - **La regla de los 14 días:** si el celular principal pasa unos 14 días sin abrir WhatsApp Business, Meta desconecta la línea. Desde ahí la API responde 409. Le avisamos al negocio por WhatsApp con un enlace para reconectar; el plan sigue corriendo. - **Desconectarse desde la app** (Configuración > Cuenta > Plataforma) da de baja el servicio y la llave deja de servir (401). - **Costo de los mensajes:** los que salen por la API los cobra Meta a la tarjeta del negocio, con la tarifa de la Cloud API. Los que se mandan desde el celular siguen siendo gratis. - **Lo que no pasa por la API:** grupos y llamadas. Con la línea conectada se apagan los mensajes temporales, los de ver una vez y la ubicación en tiempo real, y las listas de difusión quedan de solo lectura. ## El servidor MCP - **URL:** `https://api.domienlinea.com/mcp` - **Transporte:** Streamable HTTP. Solo POST, responde JSON (sin SSE) y no usa sesiones. Versiones del protocolo: 2025-11-25, 2025-06-18 y 2025-03-26. - **Autenticación:** dos formas. - **Claude y ChatGPT** (claude.ai, Claude Desktop, la app de Claude, ChatGPT): OAuth 2.1. El dueño pega la URL en los conectores y toca «Permitir» en conectar.domienlinea.com, identificándose con la sesión de su panel o con su llave. La IA recibe un pase (`dl_pase_…`, 1 hora, con renovador) que solo sirve en `/mcp`. Lista cerrada: solo esos dos clientes, por su documento de cliente (`https://claude.ai/oauth/mcp-oauth-client-metadata` y `https://chatgpt.com/oauth/client.json`). Las IAs conectadas se ven y se desconectan en el panel. - **Los demás** (Claude Code, Cursor, VS Code, las APIs): `Authorization: Bearer dl_live_…`, la misma llave de la API. - Descubrimiento: el 401 de `/mcp` trae `WWW-Authenticate: Bearer resource_metadata="https://api.domienlinea.com/.well-known/oauth-protected-resource/mcp"`; el servidor de autorización es `https://conectar.domienlinea.com` (metadatos en `/.well-known/oauth-authorization-server`, PKCE S256, sin registro abierto). - **Reglas:** las mismas de la API. Una llave o un pase malos responden 401. Con el plan vencido o el número desconectado, la IA sí se conecta, pero cada herramienta devuelve `isError` con el mismo error JSON de la API (402 o 409) y su explicación. Lo que responde Meta, bien o mal, llega tal cual. ### Herramientas | Herramienta | Argumentos | Qué hace | |---|---|---| | `estado_linea` | ninguno | GET del número en Meta: número visible, nombre, calidad, límite | | `enviar_texto` | `para`, `texto`, `vista_previa` (opcional) | Manda un texto. Solo se entrega dentro de la ventana de 24 horas | | `enviar_plantilla` | `para`, `plantilla`, `idioma`, `parametros` o `componentes` (opcionales) | Manda una plantilla aprobada. `parametros` llena {{1}}, {{2}}… del cuerpo; `componentes` es el `components` de Meta tal cual | | `listar_plantillas` | `nombre`, `despues` (opcionales) | Las plantillas con estado, motivo de rechazo, categoría, idioma y texto, de a 50 | | `crear_plantilla` | `nombre`, `categoria`, `idioma`, y `texto` + `ejemplos` o `componentes` | Crea una plantilla; queda PENDING mientras Meta la revisa | - `para` es el número con código de país, solo dígitos: `573001234567`. Un celular colombiano de 10 dígitos sin el 57 se rechaza, para no mandarle a otro país. - `categoria`: UTILITY (sobre algo que la persona ya pidió o compró), MARKETING (promociones) o AUTHENTICATION (códigos). Meta rechaza una plantilla cuya categoría no corresponde al texto. ### Leer mensajes El MCP no lee mensajes, porque no los guardamos: los entrantes van solo al webhook del negocio. Si la IA necesita leer la conversación, que la lea del sistema que recibe ese webhook. ### Conectarlo claude.ai, Claude Desktop o la app de Claude: *Configuración → Conectores → Agregar conector personalizado*, pegar `https://api.domienlinea.com/mcp` y tocar «Conectar». Se abre la página de permiso de domi en línea: con el panel abierto en ese navegador basta tocar «Permitir»; si no, primero se pega la llave. Claude deja agregar conectores propios desde el plan gratis (uno); en Team y Enterprise los agrega el dueño de la organización. ChatGPT: *Plugins → Add custom MCP server*, la misma URL y el mismo permiso. Depende de que el plan deje agregar conectores propios. Para los demás clientes, `dl_live_…` es la llave del panel. Claude Code: ``` claude mcp add --transport http domienlinea https://api.domienlinea.com/mcp --header "Authorization: Bearer dl_live_…" ``` API de Claude (conector MCP de la Messages API, beta `mcp-client-2025-11-20`). Hacen falta las dos partes, `mcp_servers` y la entrada `mcp_toolset` en `tools`: ``` POST https://api.anthropic.com/v1/messages anthropic-beta: mcp-client-2025-11-20 { "model": "claude-opus-5-5", "max_tokens": 4096, "mcp_servers": [{"type": "url", "url": "https://api.domienlinea.com/mcp", "name": "domienlinea", "authorization_token": "dl_live_…"}], "tools": [{"type": "mcp_toolset", "mcp_server_name": "domienlinea"}], "messages": [{"role": "user", "content": "Avisale a 573001234567 que su pedido ya salió."}] } ``` Cursor (`~/.cursor/mcp.json` o `.cursor/mcp.json`): ```json {"mcpServers": {"domienlinea": {"url": "https://api.domienlinea.com/mcp", "headers": {"Authorization": "Bearer dl_live_…"}}}} ``` VS Code (`.vscode/mcp.json`; la llave se pide una vez y no queda escrita en el archivo): ```json { "inputs": [{"type": "promptString", "id": "domienlinea-llave", "description": "Llave de domi en línea (dl_live_…)", "password": true}], "servers": {"domienlinea": {"type": "http", "url": "https://api.domienlinea.com/mcp", "headers": {"Authorization": "Bearer ${input:domienlinea-llave}"}}} } ``` API de Responses de OpenAI: ```json {"type": "mcp", "server_label": "domienlinea", "server_url": "https://api.domienlinea.com/mcp", "headers": {"Authorization": "Bearer dl_live_…"}} ``` Un cliente que solo hable stdio, con el puente `mcp-remote` (necesita Node). Claude Desktop ya no lo necesita: se conecta por los conectores, como arriba. La llave va en `env` y no en `args` porque algunos clientes rompen los argumentos con espacios: ```json { "mcpServers": { "domienlinea": { "command": "npx", "args": ["-y", "mcp-remote", "https://api.domienlinea.com/mcp", "--header", "Authorization:${DOMIENLINEA_AUTH}"], "env": {"DOMIENLINEA_AUTH": "Bearer dl_live_…"} } } } ``` Otro cliente: sirve cualquiera que hable Streamable HTTP y deje poner la cabecera `Authorization`. ## Enlaces - [La página del servicio](https://domienlinea.com/whatsapp-api/): qué es, precio, requisitos y preguntas. - [Contratar](https://conectar.domienlinea.com/contratar) - [Términos, sección 14](https://domienlinea.com/terminos/): las condiciones de la conexión por coexistencia. - [Documentación de la Cloud API de Meta](https://developers.facebook.com/docs/whatsapp/cloud-api): los cuerpos de mensajes, plantillas y webhooks. - Contacto: WhatsApp +57 319 252 5375 (https://wa.me/573192525375).