Construye un agente de WhatsApp
con OpenClaw + TimelinesAI

Convierte WhatsApp en un canal de cliente que tu agente OpenClaw opera: responde mensajes entrantes, envía notificaciones transaccionales y disparadas por eventos, sincroniza con tu CRM, comparte la bandeja con el equipo. TimelinesAI gestiona el gateway de WhatsApp. Tu agente se encarga del razonamiento.

Auto-respuestaEnvíos transaccionalesSync CRMBandeja compartida

Qué puedes construir

Qué puede hacer tu agente con WhatsApp

Con un skill de TimelinesAI instalado, tu agente OpenClaw puede operar WhatsApp como un canal de cliente real. La versión larga de esta lista está en las secciones de abajo — cada capacidad tiene las llamadas API específicas que necesita.

Responder mensajes entrantes automáticamente — autoresponder 24/7, respondedor fuera de horario o un chatbot completo que escala a un humano cuando se atasca.

Enviar mensajes transaccionales y disparados por eventos — confirmaciones de pedido, actualizaciones de envío, recordatorios de cita, recibos de pago y notificaciones desde otras herramientas (HubSpot, Stripe, Calendly). Solo a clientes que esperan el mensaje.

Calificar leads en conversaciones de varios turnos — hacer una secuencia de preguntas, guardar respuestas, etiquetar el chat como calificado o no.

Sincronizar la actividad de WhatsApp con tu CRM — buscar números entrantes en HubSpot/Pipedrive, actualizar etapas del deal, anotar de vuelta.

Resumir y puntuar conversaciones — “qué preguntó ACME la semana pasada”, “puntuar este chat 1–10 por intención”.

Compartir la bandeja de entrada con el equipo — tu agente redacta respuestas como notas privadas y los humanos las envían; o el agente envía y los humanos miran.

Manejar archivos — fotos de recibos, PDFs, notas de voz — procesados por OpenClaw y respondidos automáticamente.


Empieza aquí

Cómo usar esta guía

Esta guía es dual-mode. Puedes leerla tú mismo y seguir el Setup a mano — el camino que las secciones de abajo despliegan — o puedes entregarla entera a tu agente OpenClaw (o cualquier agente que pueda leer una URL) y dejar que haga el setup por ti. Ambos caminos llegan al mismo lugar: cuatro skills de WhatsApp instalados, un token y un webhook conectados, tu agente respondiendo mensajes de clientes.

Si estás leyendo esto tú mismo

Pasa por los grupos de capacidades de abajo — Incoming, Outbound, CRM & analytics, Operations — y decide cuáles importan para tu caso de uso. Luego sigue Setup manualmente; son cuatro pasos y unos diez minutos. Cada capability lista las llamadas de API exactas que hace, y cada code block en esta guía fue ejecutado contra la API real de TimelinesAI antes de publicar. Si te atascas, Things-to-know reúne todos los gotchas que te costarían una hora si no los conoces con antelación.

Si quieres que tu agente haga el setup

Pega este prompt en OpenClaw (o Claude Code, Cursor, Claude desktop, cualquier agente que pueda leer una URL). Apunta a esta guía por URL para que el agente lea la versión live, y luego te acompaña por install, token, webhook, smoke-test y habilitar tu primer skill — pidiéndote input sólo cuando de verdad necesita algo de ti.

Read https://timelines.ai/guide/openclaw-whatsapp-skills end to end.

Then help me set up an OpenClaw + TimelinesAI WhatsApp agent:

1. Clone InitechSoftware/openclaw-whatsapp-skills into ~/.openclaw/workspace/
   and symlink the four skills into ~/.openclaw/skills/.

2. Ask me for my TimelinesAI API token (Integrations → Public API → Copy
   on app.timelines.ai) and write it to ~/.openclaw/workspace/.env.timelinesai
   as TIMELINES_AI_API_KEY.

3. Run the smoke-test curl from Setup. If it fails, walk me through the
   "If you see something else" failure block in the guide.

4. Help me deploy examples/vercel-webhook-receiver from the companion repo
   and register the webhook with TimelinesAI.

5. Ask which of the four skills I want to enable first — whatsapp-autoresponder,
   whatsapp-lead-qualifier, whatsapp-send, or whatsapp-delivery-check — and
   walk me through its capability section plus any relevant gotchas from
   Things-to-know.

6. Before enabling anything that sends outbound messages, show me Channel
   choice so I pick the right WhatsApp channel (personal number vs Business
   API) for my use case.

El prompt se detiene deliberadamente antes de encender cualquier envío outbound — te rutea primero por Channel choice para que elijas el canal de WhatsApp correcto (número personal vs Business API) para lo que realmente estás intentando hacer. El riesgo de ban es real y vive upstream, no es algo de lo que el gateway te pueda proteger.


Empezando

Setup

Cuatro cosas que conectar. Después, tu skill solo hace llamadas API.

  1. 1

    Conecta tu número de WhatsApp

    Inicia sesión en app.timelines.ai, escanea el código QR con el teléfono que tiene tu número de negocio. TimelinesAI gestiona el gateway desde aquí. Mismo flujo que el setup estándar de número personal.

  2. 2

    Obtén un token de API

    Integraciones → API Pública → Copiar. Guárdalo como TIMELINES_AI_API_KEY. Un solo token cubre todo el workspace.

  3. 3

    Instala un skill del repo compañero

    Clona InitechSoftware/openclaw-whatsapp-skills en ~/.openclaw/workspace/, luego haz symlink de cada directorio de skill en ~/.openclaw/skills/. Guía rápida de cuatro líneas en el README del repo compañero.

  4. 4

    Registra un webhook

    Apunta message:received:new a una URL HTTPS pública. TimelinesAI le hará push de los mensajes entrantes. Tu receptor invoca a OpenClaw.

Prueba rápida del token

Antes de construir nada, confirma que la auth funciona:

curl -sS -w "\nHTTP: %{http_code}\n" \
  -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
  https://app.timelines.ai/integrations/api/whatsapp_accounts

Deberías ver

{
  "status": "ok",
  "data": {
    "whatsapp_accounts": [
      {
        "id": "<phone>@s.whatsapp.net",
        "phone": "+<phone>",
        "status": "connected",
        "account_name": "<your label>"
      }
    ]
  }
}

Si ves otra cosa

  • HTTP 401 con el texto plano Unauthorized — el token no se aplicó. Revisa el paso 2, asegúrate de incluir el prefijo Bearer y de que el token no se haya partido en líneas al pegarlo. La respuesta es texto plano, no JSON — pasarla a jq dará error.
  • Una página HTML con estilo bootstrap “Page not found” con HTTP 404 — tienes una barra final o un error tipográfico en la ruta. La API devuelve HTML (no un error JSON) para rutas mal escritas, así que si tu salida empieza con <!DOCTYPE html>, quita cualquier barra final y revisa la ruta.

Registra el webhook una vez

# Generate a random secret once. Add it as a query param so only
# TimelinesAI's registered URL can reach your receiver.
WEBHOOK_SECRET=$(openssl rand -hex 16)
WEBHOOK_URL="https://your-app.example.com/api/webhook?secret=${WEBHOOK_SECRET}"

curl -sS -X POST \
  -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"event_type\":\"message:received:new\",\"url\":\"${WEBHOOK_URL}\",\"enabled\":true}" \
  https://app.timelines.ai/integrations/api/webhooks

Receptor de webhook mínimo (Node / Vercel)

~40 líneas. Valida el query param ?secret=, descarta los echos de salida, responde 2xx en la ventana de reintento de 5 segundos y luego pasa el mensaje a tu host OpenClaw.

// api/webhook.js
export default async function handler(req, res) {
  // Path-segment auth — TimelinesAI doesn't sign webhooks yet, so the
  // ?secret=<token> you registered is the only thing stopping strangers
  // from invoking this endpoint.
  if (req.query.secret !== process.env.WEBHOOK_SECRET) {
    return res.status(404).end();
  }
  if (req.method !== "POST") return res.status(405).end();

  const { event_type, data } = req.body || {};
  if (event_type !== "message:received:new") {
    return res.status(200).json({ ignored: event_type });
  }

  // Drop your agent's own sends echoing back as "received".
  // TimelinesAI also fires message:received:new for outbound messages
  // synced from another WhatsApp client on the same number.
  if (data.from_me === true) {
    return res.status(200).json({ ignored: "from_me" });
  }

  // Ack FAST. TimelinesAI retries 3x with a 5-second timeout per attempt —
  // if you take longer than 5s the same event hits you up to 3 times.
  res.status(200).json({ ok: true });

  // TL webhook payloads sometimes arrive flat (data.chat_id) and sometimes
  // nested (data.chat.id) depending on event flavor and version. Destructure
  // with fallback so both shapes work — see Things to know below.
  const chatId = data.chat_id ?? data.chat?.id;
  const whatsappAccountId =
    data.whatsapp_account_id ?? data.chat?.whatsapp_account_id;

  // Fire-and-forget handoff to wherever your OpenClaw host accepts
  // inbound messages. For production durability replace this with a
  // push to a queue (QStash, Inngest, SQS) your agent drains separately.
  try {
    await fetch(process.env.OPENCLAW_HOOK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        chat_id: chatId,
        whatsapp_account_id: whatsappAccountId,
        text: data.text,
        sender_phone: data.sender_phone,
        message_uid: data.message_uid,
      }),
    });
  } catch (err) {
    console.error("[timelinesai-webhook] handoff failed:", err);
  }
}

OPENCLAW_HOOK_URL es el endpoint que tu despliegue de OpenClaw expone para los mensajes entrantes — depende de cómo alojes el agente. Para una referencia con notas de durabilidad lista para producción, mira examples/vercel-webhook-receiver en el repo compañero.


Tu instalación

Qué hay dentro de tu workspace de OpenClaw

Conectaste un token de TimelinesAI y symlink'easte skills del repo compañero. Aquí está qué instalaste realmente, y cuáles archivos editas cuando quieres cambiar cómo piensa tu agente, qué sabe y cómo se comporta. Nada aquí es magia — es todo markdown simple y archivos env en dos directorios.

Skills — ~/.openclaw/skills/<skill>/

Cada skill que symlink'easte es un directorio con un archivo obligatorio (SKILL.md) más los recursos que ese archivo referencia. Si quieres cambiar cómo suena tu FAQ handler o re-ordenar las preguntas del lead qualifier, la edición vive en el SKILL.md de ese skill. Sin código que recompilar, sin build step.

ArchivoQué controla
SKILL.mdEl skill entero. YAML frontmatter arriba (name, description, user-invocable y flags de dispatch) más el body con las instrucciones/prompt abajo. Aquí editas qué hace el skill, cuándo OpenClaw lo elige y cómo se comporta. Cada uno de los cuatro skills de WhatsApp del repo compañero es un solo SKILL.md — sin código que recompilar, solo editas y reinicias.
README.mdDocumentación humana del skill. OpenClaw NO lee este archivo — es para ti, tu equipo y cualquiera que navegue el skill en GitHub. Opcional; los skills del repo compañero todos traen uno.
Scripts, resources, schemasCualquier cosa referenciada desde SKILL.md vía {baseDir}/... — scripts helper, templates de prompt, schemas JSON, fixtures de test. Ignorado por OpenClaw a menos que tu SKILL.md apunte explícitamente. Pon lo que el skill necesite junto a SKILL.md y referéncialo por path.

Para los cuatro skills de WhatsApp — whatsapp-autoresponder, whatsapp-lead-qualifier, whatsapp-send, whatsapp-delivery-check — la parte superior de cada SKILL.md tiene el role y las tools permitidas; el body tiene el prompt real con el que corre tu agente. Lee uno de ellos end-to-end antes de editar; los patrones se repiten.

Workspace — ~/.openclaw/workspace/

El directorio workspace es la casa de OpenClaw para todo lo que no es un skill: quién es tu agente, qué sabe de ti, qué tools tiene, cómo arranca. Los propios docs de OpenClaw dicen “esta carpeta es hogar. Trátala así”. Estos archivos vienen con el install — los llenas con el tiempo:

ArchivoQué controla
IDENTITY.mdQuién es tu agente — nombre, creature (cómo se conceptualiza), vibe, emoji signature, avatar. Llénalo temprano; la mayoría de los otros archivos referencian de vuelta a él.
SOUL.mdPrincipios operativos y personalidad core. Cómo el agente debe comportarse cuando nadie mira — cuándo ser genuino vs performativo, cómo ganar confianza, dónde se asientan los límites de privacidad. Los docs de OpenClaw lo llaman la conciencia del agente.
USER.mdQuién está siendo ayudado por el agente (tú). Nombre, pronombres, timezone, intereses, contexto del proyecto. Los agentes son mejores ayudando a alguien específico que a un “user” genérico — este archivo es cómo lo recuerdan.
TOOLS.mdConfig específica del entorno que no quieres dentro de ningún skill — nombres de dispositivos, direcciones de host, preferencias locales. Vive en el workspace para que puedas editarlo sin tocar nada que pudieras compartir.
AGENTS.mdEl README del workspace para agentes. Describe cómo OpenClaw espera que este workspace se ejecute. Normalmente viene con el install y raramente se edita.
BOOT.mdInstrucciones cortas y explícitas sobre qué debe hacer OpenClaw al arrancar. Vacío por defecto; llénalo si quieres comportamiento de boot determinista entre reinicios.
BOOTSTRAP.mdConversación de onboarding del primer boot. Guía a un workspace fresco para establecer su identidad, y luego te dirige a guardar los resultados en IDENTITY.md, SOUL.md, USER.md. Bórralo una vez que hayas llenado esos otros archivos.
HEARTBEAT.mdDefiniciones de tareas periódicas. Archivo vacío significa sin heartbeats; añade tareas aquí cuando quieras que el agente chequee algo en un intervalo (p.ej. poll una queue cada cinco minutos).

Archivos env por integración

Junto a los archivos canónicos de arriba, añades un archivo env por cada integración que conectes. Estos NO son parte del install de OpenClaw — son tuyos, y guardan secretos que nunca deberían commitearse en un repo público:

ArchivoQué controla
.env.<integration>Secretos y config para una integración, un archivo por integración. Para el trabajo de WhatsApp creaste .env.timelinesai con TIMELINES_AI_API_KEY y ALLOWED_SENDER_JID en Setup paso 2. Repite este patrón cuando añadas HubSpot, Stripe, Pipedrive o cualquier otra tool — un .env.<name> y lo haces source desde el skill que lo necesita.
La regla de una línea: cambias cómo se COMPORTA tu agente editando el SKILL.md de un skill; cambias QUIÉN es tu agente editando SOUL.md o IDENTITY.md en el workspace; cambias QUÉ SECRETOS usa editando el archivo .env.<integración> correcto. Todo lo demás en el workspace es maquinaria que rara vez tocarás.

Elección del canal

Números personales vs WhatsApp Business API

Antes de construir flujos salientes, entiende a qué canal de WhatsApp pertenece tu caso de uso. Elegir mal banea números.

Esta guía trata de números personales de WhatsApp — los que conectas a TimelinesAI escaneando un QR. Sirven para conversaciones entrantes y envíos transaccionales a clientes que esperan el mensaje. Para cold outreach, broadcasts de marketing o campañas promocionales, usa WhatsApp Business API — TimelinesAI lo soporta hoy desde el dashboard, y la automatización por API pública de los flujos Business API llega en Q2 2026.

Seguro con los skills de esta guía (número personal)

  • Conversaciones entrantes. El cliente te escribe primero, tú respondes. Sin riesgo.
  • Envíos transaccionales. Confirmaciones de pedido, actualizaciones de envío, notificaciones de entrega, recibos de pago, recordatorios de cita — cualquier mensaje que un cliente espera porque acaba de hacer algo con tu negocio.
  • Notificaciones disparadas por eventos en otras herramientas. Deal de HubSpot → confirmación de demo, fallo de pago de Stripe → nota de recuperación, reserva de Calendly → recordatorio previo a la reunión. El cliente optó cuando usó la herramienta upstream.
  • Responder dentro de la ventana de servicio al cliente de 24 horas de WhatsApp. Una vez que el cliente te escribe, tienes 24 horas para responder libremente. Los skills de auto-respuesta, FAQ y calificador de leads operan dentro de esta ventana.

NO seguro en números personales

  • Cold outreach a listas que compraste o scrapeaste — WhatsApp banea números por esto en horas.
  • Broadcasts de marketing a clientes que no optó explícitamente.
  • Campañas promocionales, ofertas de ventas, empujones de temporada, lanzamientos de producto.
  • Cualquier cosa que se parezca a un blast de marketing. Si te estás preguntando “cuánto throughput puedo sacar”, estás en el canal equivocado.

Cómo saber qué canal necesitas

  1. 1.¿El cliente te escribió primero, o estás respondiendo dentro de una sesión activa de 24 horas? → Número personal, esta guía.
  2. 2.¿El cliente está a punto de recibir algo que explícitamente espera (pedido, cita, pago, entrega)? → Número personal, envío transaccional.
  3. 3.¿Disparado por un evento opt-in del cliente en tu CRM o herramienta de facturación? → Número personal, envío disparado por evento.
  4. 4.¿Broadcast, cold outreach o campaña promocional? → Business API, usa el dashboard de TimelinesAI.
  5. 5.¿No estás seguro? → No envíes. Trátalo como promocional y rutéalo al camino Business API.

Cada capacidad saliente más abajo asume que pasaste este test. Si no estás seguro, vuelve a leer esta sección antes de shipear. TimelinesAI no puede protegerte del baneo a nivel de WhatsApp — el baneo se aplica upstream, en la infraestructura de WhatsApp, no en el gateway.


Referencia

Referencia de la API

Cada endpoint que necesitas para construir cada capacidad de abajo. URL base https://app.timelines.ai/integrations/api. Auth Authorization: Bearer $TIMELINES_AI_API_KEY.

Lectura

MétodoRutaQué devuelve
GET/whatsapp_accountsTus números de WhatsApp conectados, cada uno con JID, teléfono, estado y nombre de cuenta.
GET/chatsLista de chats. Acepta filtros ?phone=... y ?label=.... Pagina con ?page=N (50 por página, fijo). Cada chat tiene whatsapp_account_id con el JID del número dueño, más chatgpt_autoresponse_enabled — mira Cosas que saber antes de lanzar tu propio agente.
GET/chats/{id}Detalle completo de un chat.
GET/chats/{id}/messagesHistorial de mensajes, 50 por página. Pagina con ?page=N y vigila data.has_more_pages. Cada mensaje lleva from_me, sender_phone, text, timestamp, message_type, status (Sent/Delivered/Read) y origin (API Pública vs app de WhatsApp).
GET/chats/{id}/labelsEtiquetas del chat.
GET/messages/{uid}/status_historyLínea de tiempo Sent / Delivered / Read para un mensaje saliente.
GET/messages/{uid}/reactionsReacciones a un mensaje. Devuelve {data: {users: [{name, phone, reaction, current}], reactions: {<emoji>: count}, total: N}} — un objeto, no un array plano. users lista quién reaccionó (cada uno trae el emoji elegido más un booleano current que marca tu propio workspace); reactions es un histograma indexado por emoji. Estado vacío: {users: [], reactions: {}, total: 0}.
GET/filesArchivos que has subido por la API.
GET/webhooksTus suscripciones de webhook registradas.

Escritura

MétodoRutaQué hace
POST/messagesEnvía a un número de teléfono. Body: {"phone":"+...","text":"..."}. Devuelve {"message_uid":"..."}.
POST/chats/{id}/messagesEnvía dentro de un chat existente. Body: {"text":"..."}. El remitente es el número de WhatsApp dueño del chat.
POST/chats/{id}/notesAdjunta una nota privada al chat. No se envía a WhatsApp, solo es visible dentro de TimelinesAI. Se usa para estado del agente y workflows de revisión.
POST/chats/{id}/labelsAñade una etiqueta al chat. Útil para tracking de etapas, enrutado y banderas de stop-reply.
PATCH/chats/{id}Actualiza metadatos del chat — responsable (responsible_email), estado de lectura y chatgpt_autoresponse_enabled. Desactiva el último antes de que tu agente empiece a responder, o el responder integrado de TL correrá contra el tuyo.
PATCH/messages/{uid}/reactionsPone un emoji de reacción en un mensaje. El body lleva el carácter emoji literal, no un shortcode.
POST/files_uploadSube un archivo vía multipart/form-data (campo: file). Devuelve data.uid, que pasas a chat/messages como file_uid. No existe variante upload-por-URL.
POST/webhooksRegistra una suscripción de webhook.
PUT/webhooks/{id}Actualiza o habilita/deshabilita una suscripción.
DELETE/webhooks/{id}Elimina una suscripción.

Antes de empezar

Cosas que saber

Algunos detalles fáciles de pasar por alto en la referencia y que cada uno te costará una hora si no los conoces.

!
Sin slashes finales. GET /chats funciona. GET /chats/ devuelve la página 404 de TimelinesAI — que parece un problema de red y no lo es. Cada URL en esta guía está escrita sin slash final a propósito.
!
Los bodies JSON deben ser UTF-8 válido — y la trampa va más allá de los heredocs. El parser rechaza cualquier otra cosa. Em-dashes y comillas tipográficas son el detonante más común, pero TAMBIÉN cualquier shell corriendo en locale no-UTF-8 (Git Bash en Windows, algunas imágenes Docker base, sesiones SSH antiguas) corrompe esos caracteres tanto en heredocs COMO en argumentos curl -d "..." inline — el em-dash se convierte en byte 0x97, que falla el decode UTF-8 en el servidor. Escribe el payload a un archivo guardado explícitamente como UTF-8 y usa curl --data-binary @file.json; no confíes en -d inline para nada con puntuación tipográfica.
!
La URL base es https://app.timelines.ai/integrations/api. Algunos posts antiguos referencian otro subdominio con header X-API-KEY — eso está obsoleto. Usa Bearer auth en app.timelines.ai/integrations/api.
!
Las URLs de adjuntos en payloads de webhook expiran rápido. Cuando un cliente envía una foto o PDF, descárgalo dentro del handler del webhook, no desde un worker asíncrono.
!
Los números personales se banean por cold outreach. Los endpoints de envío de esta guía te dejarán enviar a cualquiera, pero WhatsApp baneará tu número rápidamente por outbound no solicitado. Solo envía outbound a personas que te escribieron primero o que explícitamente esperan un mensaje transaccional. Para broadcasts, usa WhatsApp Business API — ver Elección del canal.
!
El envío es asíncrono. POST /messages devuelve un message_uid — ese es un recibo, no una confirmación de entrega. Usa GET /messages/{uid}/status_history para verificar la entrega real.
!
Las formas de respuesta varían — los endpoints de listas anidan bajo una clave con tipo. La mayoría de los endpoints de listas devuelven {"data":{"<clave-tipada>":[...]}}, no un {"data":[...]} plano. GET /whatsapp_accounts anida bajo data.whatsapp_accounts, GET /chats bajo data.chats (con bandera de paginación data.has_more_pages), GET /chats/{id}/messages bajo data.messages, GET /chats/{id}/labels bajo data.labels. Excepciones: GET /messages/{uid}/status_history y GET /files devuelven data como array plano. Código que asume una forma uniforme se rompe en la primera discrepancia — léelo como res?.data?.<tipada> ?? res?.data ?? [].
!
Usa from_me, no sender_phone, para la dirección en lecturas de historial. GET /chats/{id}/messages devuelve mensajes salientes (tus envíos vía API Pública) con sender_phone puesto al número del equipo, no a cadena vacía. Detectar dirección por sender_phone != "" etiqueta cada saliente como entrante e invierte la conversación. Confía en el booleano from_me — true es saliente (tu agente hablando), false es entrante (cliente).
!
El historial usa el campo uid; el webhook usa message_uid. Mismo valor, clave distinta. Cuando TL entrega un webhook message:received:new, el payload usa message_uid. Cuando lees el historial del mismo chat vía GET /chats/{id}/messages, el campo se llama uid. Es el mismo identificador. Código que guarda UIDs de webhooks y los busca en el historial debe normalizar ambos nombres. Los mensajes del historial también usan timestamp, no created_at.
!
Las etiquetas son un REEMPLAZO, no un AÑADIR. POST /chats/{id}/labels toma {"labels":["needs-human","intent/sales"]} — un array, clave plural. La llamada reemplaza el conjunto completo de etiquetas del chat; para añadir una etiqueta primero haces GET del conjunto existente, le agregas la tuya y haces POST de la lista combinada. Para limpiar todas: POST {"labels":[]}. No existe un endpoint DELETE /chats/{id}/labels/{nombre} que funcione para quitar una etiqueta individual — tienes que hacer POST del conjunto nuevo sin la que quieres eliminar.
!
Registrar un webhook puede reproducir historia reciente. En los primeros eventos tras hacer POST /webhooks, TL puede entregar una ráfaga de eventos message:received:new pasados — o el handshake de registro puede reintentarse contra un contenedor que aún está arrancando en frío. En ambos casos tu receptor debe ser idempotente desde el evento número 1. Deduplica por message_uid desde el inicio, no asumas “slate limpio” al suscribirte.
!
Los payloads de webhook pueden llegar planos O anidados. A veces TL entrega data.chat_id / data.whatsapp_account_id en el nivel superior y otras los anida como data.chat.id / data.chat.whatsapp_account_id — depende del tipo de evento y la versión. Desestructura defensivamente: const chatId = data.chat_id ?? data.chat?.id. Los adjuntos son peor: data.attachment_url, data.attachment?.url y data.file_url son todas posibles. Un receptor que solo lee el formato plano falla silenciosamente en las entregas anidadas.
!
Los endpoints de listado devuelven 50 elementos por página, fijo. ?limit=N, ?per_page=N y ?page_size=N se ignoran silenciosamente — la API elige su propio tamaño. Pagina con ?page=N (indexado desde 1) y para cuando data.has_more_pages sea false. Verificado en /chats y /chats/{id}/messages; el patrón se aplica a otras lecturas de listas. Si necesitas “todos los mensajes desde X”, espera recorrer páginas, no una sola llamada grande limit=1000.
!
Desactiva el autoresponder de ChatGPT integrado de TimelinesAI antes de que tu agente tome el control. Cada chat tiene por defecto chatgpt_autoresponse_enabled: true y el responder integrado de TL responderá a los mensajes entrantes junto con tu skill, produciendo respuestas dobles que el cliente ve. PATCH /chats/{id} con {"chatgpt_autoresponse_enabled": false} lo desactiva por chat. No hay un desactivar-en-bulk en la API Pública, así que haz que esta sea la primera acción que ejecute tu receptor en cualquier chat del que esté a punto de encargarse (y hazlo una vez en cada chat que tu agente ya esté gestionando).
!
Las notas viven en /chats/{id}/messages con message_type=note y from_me=false, siempre. POST /chats/{id}/notes escribe en la misma timeline que los mensajes reales de WhatsApp — cuando lees el historial, las notas vuelven intercaladas con from_me: false sin importar quién las escribió. Cualquier código que razone sobre el flujo de conversación DEBE filtrar por .message_type == "whatsapp": un cálculo ingenuo de tiempo de respuesta que empareje entrante→saliente sólo por from_me tratará cada nota como mensaje entrante del cliente y corromperá la latencia, y un skill de resumen o scoring que alimente el historial completo a un LLM verá sus propias notas previas como palabras del cliente. Mantén las notas en el stream sólo cuando quieras explícitamente estado estructurado (el patrón del qualifier); descártalas en todo lo demás.

Cuando las llamadas API fallan

Cinco modos de fallo te van a tocar tarde o temprano. Cada uno se ve distinto en la respuesta y cada uno tiene una respuesta correcta distinta.

!
400 — cuerpo de petición inválido. La causa más común son caracteres no UTF-8 en tu JSON (em-dashes, comillas tipográficas de un heredoc de shell). Escribe el payload a un archivo con codificación UTF-8 explícita y usa curl --data-binary @file.json. Misma raíz que el tip de UTF-8 de arriba.
!
401 — token expirado o revocado. Rótalo desde app.timelines.ai → Integraciones → API Pública → Regenerar. El token nuevo entra en vigor inmediatamente; el viejo deja de funcionar en el mismo instante. Léelo desde tu entorno para poder rotar sin redeployar.
!
429 — rate limited. La respuesta lleva un header Retry-After en segundos. Respétalo y reintenta una vez. Si chocas con 429 repetidamente, tu skill está enviando más rápido que el throttle de ~30 mensajes por minuto del número personal — frena en el cliente o reparte la carga saliente entre varios números conectados.
!
5xx — problema upstream del gateway. Reintenta con backoff exponencial: 2s, 4s, 8s. Después del tercer fallo, escala a un humano y deja de enviar. No hagas un loop infinito — un 5xx persistente suele ser un evento de la status page de TimelinesAI, no algo que tus reintentos arreglen.
!
Idempotencia de webhooks. TimelinesAI reintenta cada entrega hasta 3 veces con timeout de 5 segundos. Si tu receptor no deduplica, el cliente ve la misma respuesta 3 veces. Guarda el message_uid entrante como nota del chat antes de procesar; si llega el siguiente webhook con el mismo uid, sáltalo. Las notas son baratas y visibles para humanos al depurar.

Un pequeño patrón de shell que ramifica por cada código de estado:

$ # Capture the HTTP status into a variable, then branch
$ http_code=$(curl -sS -o /tmp/resp.json -w "%{http_code}" \
    -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/send.json \
    https://app.timelines.ai/integrations/api/messages)

$ case "$http_code" in
    200|201) ;;                                # ok, carry on
    400)     echo "bad body: $(cat /tmp/resp.json)"; exit 1 ;;
    401)     echo "token expired - rotate"; exit 2 ;;
    429)     sleep 30; retry_once ;;           # respect Retry-After
    5*)      sleep $((2 ** attempt)); retry_with_backoff ;;
  esac

Operar con seguridad en producción

Dos cosas más que no son fallos, pero te ahorrarán un mal día más adelante.

!
Depurar un webhook que se cayó en silencio. Cuando tu skill deja de responder a una conversación real, comprueba en este orden: (1) el log de entrega de webhooks de TimelinesAI — si el evento ni siquiera llega a tu URL, el problema es de registro o DNS, no de tu skill; (2) los logs de tu plataforma de hosting (Vercel logs, etc.) por una excepción no manejada; (3) deja una nota miguita en el chat con POST /chats/{id}/notes antes de procesar el evento, así confirmas que el receptor realmente disparó; (4) GET /webhooks para verificar que la suscripción sigue activa (rotar tokens de vez en cuando deja caer suscripciones en silencio).
!
Un solo token cubre todo el workspace. Tu token de API lee y escribe cada chat, mensaje, etiqueta, nota, archivo y webhook del workspace. Trátalo como una contraseña: guárdalo en .env, no lo commits, y rótalo desde Integraciones → API Pública → Regenerar cuando un compañero se va o cuando cambias un entorno de deploy. El token nuevo entra en vigor inmediatamente; el viejo deja de funcionar en el mismo instante. Hoy no hay scope per-skill ni read-only.

Contexto y memoria

Contexto de conversación

El webhook le entrega a tu skill un solo mensaje — el que acaba de llegar. Sin turnos previos, sin historial de chat, sin transcripción activa. Para un FAQ sin estado o un respondedor fuera de horario eso sobra. Para un agente conversacional que tiene que recordar lo que el cliente dijo tres turnos antes, tu skill tiene que traer el contexto por su cuenta. Tres patrones cubren todo el rango, del bucle más barato al que de verdad recuerda.

1

Sin estado

Responde solo al último mensaje. Un POST por turno, sin GET, sin historial. Es el bucle más simple y el valor por defecto en los skills listos para usar. Sírvelo para bots de FAQ, respondedores fuera de horario y clasificación o enrutamiento — donde la respuesta depende solo del mensaje actual. El agente olvida todo entre turnos: si el cliente escribe un seguimiento que hace referencia a algo dicho antes, no lo va a entender.

2

Ventana de contexto completa

Trae los últimos 20 mensajes antes de cada respuesta y pásalos al modelo como conversación previa. Dos llamadas API por turno en lugar de una, más los tokens extra en el prompt cada vez. Sírvelo para agentes conversacionales, calificación de leads multi-turno y cualquier caso donde el agente tiene que recordar el hilo. Veinte turnos recientes suele bastar — ir más ancho rara vez ayuda y encarece el prompt; ir más estrecho hace que el agente olvide lo que el cliente dijo hace un minuto.

3

Contexto adaptativo

Trae el contexto solo cuando el mensaje entrante parece un seguimiento. Una heurística barata sobre el texto — empieza por un pronombre, referencias a “eso” o “el”, llega antes de los 30 segundos desde tu respuesta anterior, acuses de recibo de una sola palabra — decide si tirar del historial o responder sin estado. La mayoría de los turnos siguen siendo baratos; las continuaciones recuperan memoria. Empieza con la ventana completa y sólo pasa a adaptativo cuando conozcas tu presupuesto de coste y tengas tráfico de producción real para ajustar la heurística.

Traer la ventana es un único GET. La respuesta es un array plano de mensajes del chat, el más nuevo al final, mezclando turnos entrantes del cliente con tus propias respuestas salientes y cualquier nota que haya escrito tu skill:

$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    "https://app.timelines.ai/integrations/api/chats/12345678/messages?limit=20"
{"status":"ok","data":[
  {"message_uid":"INBOUND-UID-PLACEHOLDER",
   "text":"hi, is the blue one still available?",
   "sender_phone":"+15550200",
   "message_type":"text","origin":"WhatsApp",
   "created_at":"2026-04-14T10:22:03Z"},
  {"message_uid":"OUTBOUND-UID-PLACEHOLDER",
   "text":"Yes, one left. Want me to hold it for you?",
   "sender_phone":"",
   "message_type":"text","origin":"Public API",
   "created_at":"2026-04-14T10:22:41Z"},
  {"message_uid":"INBOUND-UID-PLACEHOLDER-2",
   "text":"yes please, until tomorrow",
   "sender_phone":"+15550200",
   "message_type":"text","origin":"WhatsApp",
   "created_at":"2026-04-14T10:23:08Z"}
]}

Trampas que muerden en producción

!
Filtra las notas y tu propio saliente del historial. GET /chats/{id}/messages devuelve todo lo que hay en el chat: turnos entrantes del cliente, tus respuestas salientes y cualquier nota que tu skill haya escrito para estado o debug. Las notas llevan message_type == "note" y origin == "Public API" — fíltralas antes de pasar el historial al modelo, si no tu agente ve su propio bookkeeping interno como si fuera conversación real y empieza a responder a sus propias notas.
!
Empezar a mitad de conversación. Cuando tu skill ve un chat por primera vez, ese chat puede tener ya un historial largo de antes de que te suscribieras al webhook. Trae siempre contexto reciente en un chat nuevo para ti — si no, vas a responder a un “hola” nuevo como si fuera una apertura fresca, cuando el cliente en realidad está treinta turnos metido en un hilo existente con tu equipo.
!
Deduplica ráfagas de webhooks. Cuando un cliente escribe tres mensajes en cuatro segundos, recibes tres webhooks — posiblemente en paralelo. Sin un lock por chat o un debounce corto, el cliente recibe tres respuestas pisadas. Patrón: aguanta el primer webhook 1–2 segundos, luego trae el historial una vez y responde al estado combinado. Los reintentos del webhook (hasta 3 intentos, 5 segundos de timeout cada uno) llegan por el mismo camino — deduplica por message_uid antes de procesar.
!
Ordena por created_at, no por message_uid. Los message_uid son de scope workspace y no son globalmente ordenables. Cuando montes el historial en el prompt, ordena los turnos por el timestamp created_at del payload, no por UID. Los UIDs cross-workspace tampoco están compartidos — el mismo mensaje físico de WhatsApp entregado a dos workspaces distintos de TimelinesAI tiene un UID diferente en cada uno.

Persistencia de estado

Los skills de OpenClaw no mantienen estado en memoria entre invocaciones. Las conversaciones de WhatsApp son multi-turno. La solución es guardar el estado en el propio chat:

  • Las etiquetas guardan la etapa discreta — discovery/q1, qualified, escalate. Añade con POST /chats/{id}/labels, lee con GET /chats/{id}/labels.
  • Las notas guardan datos estructurados — team_size=8, borradores de respuesta, scores de leads. Añade con POST /chats/{id}/notes. Lee iterando GET /chats/{id}/messages y filtrando message_type == "note".

La ventaja: seguridad ante crashes, visibilidad para los humanos del equipo, handoff limpio — un humano puede limpiar una etiqueta para rebobinar el flujo, o añadir escalate para tomar el control. El trade-off: cada transición de estado es una llamada HTTP. Para flujos cara al cliente eso está bien.


Enviar desde el número correcto

Si tu workspace tiene más de un número de WhatsApp conectado, tu skill tiene que asegurarse de enviar desde el número esperado. Cada chat tiene un campo whatsapp_account_id con el JID completo (como TELEFONO@s.whatsapp.net) del número dueño. Cuando haces POST /chats/{id}/messages, el remitente es siempre ese JID — tú no lo eliges, lo elige el chat.

El patrón:

  1. 1Hardcodea el JID de remitente permitido en el entorno de cada skill (p. ej. ALLOWED_SENDER_JID).
  2. 2Antes de enviar, GET /chats/{id} y compara whatsapp_account_id con tu JID permitido.
  3. 3Si no coinciden, salta el envío — escala a un humano o descarta el evento.

Dos llamadas HTTP extra por turno, cero posibilidad de enviar desde el persona equivocado. Para workspaces de un solo número esto no aplica.


Entrantes

Mensajes entrantes — lo que tu agente puede manejar

Cada vez que un cliente le escribe a tu número de WhatsApp, TimelinesAI dispara un evento message:received:new a tu webhook con el chat id, el texto, el teléfono del remitente y los adjuntos. Tu skill lee el evento, decide qué hacer y responde con POST /chats/{chat_id}/messages. Todo lo de abajo es una variación de ese loop.

1

Responder automáticamente a cada mensaje entrante

Responde preguntas sobre envíos, devoluciones y horario. Para todo lo demás, redacta una respuesta y etiqueta el chat para revisión.
Entre las 22:00 y las 08:00 responde automáticamente. Durante el horario laboral solo márcame los chats entrantes.
Maneja mis respuestas de WhatsApp mientras estoy en esta reunión.
How it works

el skill recibe el payload del webhook, redacta una respuesta y llama a POST /chats/{id}/messages con {"text":"..."}. Sin estado por defecto — una llamada por turno. Para conversaciones de varios turnos donde el agente necesita recordar mensajes anteriores, consulta Contexto de conversación más abajo.

2

Manejador de FAQ con escalado a un humano

Responde preguntas sobre envíos, devoluciones y horario. Para todo lo demás, etiqueta el chat needs-human y deja de responder hasta que yo limpie la etiqueta.
How it works

antes de responder, consulta GET /chats/{id}/labels. Si el chat tiene needs-human, sal sin enviar nada. Si el texto entrante coincide con un tema de FAQ, responde. Si no, POST /chats/{id}/labels con la etiqueta de escalado y sal en silencio. La bandeja de tu equipo filtra por la etiqueta.

3

Enrutar conversaciones a la persona correcta

Para cada chat entrante, deduce si es ventas, soporte o facturación y etiquétalo. Asigna los chats de ventas a alex@ours y los de facturación a jamie@ours.
How it works

clasifica la intención a partir del texto, POST /chats/{id}/labels con intent/sales o similar, luego PATCH /chats/{id} con {"responsible_email":"..."} para entregar el chat dentro de la bandeja de TimelinesAI.

4

Calificar leads con una secuencia de preguntas

Para cualquier chat nuevo de nuestra campaña de Facebook, pregunta por su caso de uso, tamaño del equipo y plazos. Guarda las respuestas como notas en el chat. Etíquetalo qualified si el equipo es de 5 o más.
How it works

las etiquetas marcan en qué pregunta vas (discovery/q1, q2, q3); las notas guardan las respuestas. En cada turno: lee la etiqueta de la etapa actual, parsea el texto entrante como respuesta, escríbelo con POST /chats/{id}/notes, avanza la etiqueta y haz la siguiente pregunta. Sin base de datos externa — ver Persistencia de estado más abajo.

$ # 1. Read current stage from the chat's labels. Labels nest under
$ # data.labels as a string array — see Response shapes in Things to know.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    https://app.timelines.ai/integrations/api/chats/12345678/labels
{"status":"ok","data":{"labels":["discovery/q1"]}}

$ # 2. Save the customer's answer as a structured note. Notes are stored
$ # as messages with message_type=note and aren't pushed to WhatsApp.
$ cat > /tmp/note.json <<'JSON'
{"text":"team_size=12"}
JSON
$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/note.json \
    https://app.timelines.ai/integrations/api/chats/12345678/notes
{"status":"ok","data":{"message_uid":"NOTE-UID-PLACEHOLDER"}}

$ # 3. Advance the stage label. POST /labels REPLACES the full set, so
$ # read existing labels, swap q1 for q2, POST the combined list. The
$ # body key is "labels" (plural, array) — not "label" singular.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    https://app.timelines.ai/integrations/api/chats/12345678/labels \
  | jq -c '.data.labels | map(if . == "discovery/q1" then "discovery/q2" else . end) | {labels: .}' \
  > /tmp/labels.json
$ cat /tmp/labels.json
{"labels":["discovery/q2"]}
$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/labels.json \
    https://app.timelines.ai/integrations/api/chats/12345678/labels
{"status":"ok","data":{"labels":["discovery/q2"]}}
5

Entender fotos, PDFs y recibos

Cuando un cliente envíe una foto de un recibo, extrae el monto y el vendedor y añádelos como una nota.
Si alguien manda un PDF, clasifícalo como factura / contrato / identificación y etiqueta el chat según corresponda.
How it works

los payloads de webhook incluyen una URL de adjunto. Descárgala dentro del handler (expira rápido), procésala con las herramientas de visión o documentos de OpenClaw y escribe los datos extraídos con POST /chats/{id}/notes.

$ # The attachment URL in the webhook payload expires in ~15 minutes.
$ # Download it inline in the receiver, before queuing async work.
$ ATTACH_URL="https://files.timelines.ai/abc123/receipt.jpg?expires=..."
$ curl -sS "$ATTACH_URL" -o /tmp/receipt.jpg

$ # Process /tmp/receipt.jpg with OpenClaw vision tools, then write
$ # the extracted fields back as a structured note on the chat:
$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary '{"text":"receipt: amount=$42.50 vendor=Cafe Madrid"}' \
    https://app.timelines.ai/integrations/api/chats/12345678/notes
6

Transcribir notas de voz y responder

Transcribe las notas de voz entrantes. Responde por texto — si de verdad necesitas una respuesta en voz, mándala desde el dashboard de TimelinesAI.
How it works

el webhook entrega una URL al archivo de voz. Descárgalo, transcribe, redacta una respuesta de texto vía POST /chats/{id}/messages. Las respuestas en voz son hoy una función del dashboard de TimelinesAI y no están en la referencia pública de la API actual — si tu skill necesita enviar respuestas en voz programáticamente, contacta al soporte de TimelinesAI para confirmar si el endpoint voice_message heredado sigue disponible en tu workspace.

7

Responder en el idioma del cliente

Si el cliente escribe en español, responde en español. Si cambia de idioma a mitad de la conversación, cámbiate con él.
How it works

razonamiento puramente del lado de OpenClaw sobre el texto entrante. TimelinesAI solo transporta la respuesta.

8

Reaccionar a mensajes sin enviar una respuesta completa

Reacciona con 👀 a cada mensaje entrante para que los clientes sepan que lo vi, y luego tómate tu tiempo redactando la respuesta real.
How it works

PATCH /messages/{uid}/reactions con {"reaction":"👀"}. El campo reaction debe contener el carácter emoji literal — los shortcodes como "eyes" o ":eyes:" son rechazados con HTTP 400 "Reaction has invalid format". No consume crédito de mensaje — las reacciones son ligeras.

$ # Send the raw emoji character in the body. Save to a UTF-8 file and
$ # use --data-binary so shells can't downgrade it.
$ printf '{"reaction":"👀"}' > /tmp/react.json

$ curl -sS -X PATCH \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/react.json \
    https://app.timelines.ai/integrations/api/messages/INBOUND-UID-PLACEHOLDER/reactions
{"status":"ok"}

Salientes

Salientes — mensajes que tu agente inicia

Mensajes que tu agente inicia, no respuestas. Disparados por un evento en tus otras herramientas (un nuevo pedido, un pago fallido, una reunión agendada) o por una instrucción humana directa sobre una persona concreta.

Antes de cada capacidad de esta sección: el cliente o abrió el hilo recientemente (dentro de la ventana de sesión de 24 horas de WhatsApp) o explícitamente espera este mensaje. Si ninguna de las dos cosas es cierta, no lo envíes desde un número personal — ese es territorio Business API.

9

Enviar un mensaje transaccional por nombre o a un nuevo destinatario

Manda a Juan que su factura está lista.
Mandale al plomero la nueva dirección de la oficina para que entregue las piezas.
Envía el contrato firmado al cliente que acaba de transferir el depósito.
How it works

para un chat existente, busca con GET /chats?name=Juan (o tu CRM) y llama POST /chats/{id}/messages. Para un nuevo destinatario, POST /messages con {"phone":"+...","text":"..."}. El skill whatsapp-send del repo compañero maneja ambos modos con serialización UTF-8 segura.

$ cat > /tmp/send.json <<'JSON'
{"phone":"+15550200",
 "text":"Hi - your order shipped. Tracking: ABC123."}
JSON

$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/send.json \
    https://app.timelines.ai/integrations/api/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}
10

Seguimientos programados dentro de una conversación activa

Cada lunes a las 9, revisa los chats etiquetados to-follow-up que tuvieron actividad del cliente en las últimas 24 horas pero ninguna respuesta nuestra, y envía un recordatorio amable.
How it works

progámalo con un cron job de OpenClaw o una standing order (mira los docs de automatización de OpenClaw). El job jala la audiencia con GET /chats?label=to-follow-up&read=false, lee last_message_timestamp de cada chat y sólo envía cuando el último turno del cliente fue hace menos de 24 horas. Fuera de esa ventana, un follow-up se vuelve un toque de re-engagement y entras en territorio Business API — mira Channel choice.

11

Disparar mensajes desde eventos en tus otras herramientas

Cuando un deal de HubSpot pase a 'demo scheduled', envía una confirmación por WhatsApp con el enlace de la reunión.
Cuando Stripe reporte un pago fallido, envía un mensaje amable de recuperación con un enlace para actualizar la tarjeta.
Cuando se cree una reserva de Calendly, envía un recordatorio previo a la reunión la mañana del mismo día.
How it works

tu herramienta existente (HubSpot, Stripe, Calendly, Pipedrive) dispara su propio webhook hacia el mismo receptor que configuraste en Setup — añade un handler enrutado por event source o path, haz un branch desde el handler de TimelinesAI, y llama POST /messages (destinatario nuevo) o POST /chats/{id}/messages (dentro de un chat existente). WhatsApp se convierte en un canal de entrega para cualquier flujo que ya tengas. Como los envíos van detrás de una acción del cliente en la herramienta de origen, son transaccionales por naturaleza — perfectamente dentro de las reglas del número personal.

12

Enviar archivos y documentos a petición

Genera el PDF de la cotización y envíalo al cliente que acaba de pedir precios.
Manda el contrato como PDF por correo y también pásalo al chat de WhatsApp del cliente.
How it works

dos pasos. Sube los bytes del archivo con POST /files_upload como multipart/form-data (no existe un endpoint de subida por URL — tu agente debe descargar el archivo primero y luego hacer POST). La respuesta devuelve un uid que pasas a POST /chats/{id}/messages en el campo file_uid. El cliente pidió el documento — esto es una respuesta a su petición, no cold outreach.

$ # Step 1 - upload the file bytes as multipart/form-data.
$ # The form field name is "file". The response gives you a uid.
$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -F "file=@/path/to/quote.pdf" \
    https://app.timelines.ai/integrations/api/files_upload
{"status":"ok","data":{
  "uid":"FILE-UID-PLACEHOLDER",
  "filename":"quote.pdf",
  "size":128453,
  "mimetype":"application/pdf",
  "uploaded_by_email":"you@yourcompany.com",
  "uploaded_at":"2026-04-14 10:22:03 +0000",
  "temporary_download_url":"https://tl-prod-data.s3.amazonaws.com/..."
}}

$ # Step 2 - attach the uploaded file to a chat message using file_uid.
$ cat > /tmp/send.json <<'JSON'
{"text":"Quote attached for your review.",
 "file_uid":"FILE-UID-PLACEHOLDER"}
JSON
$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/send.json \
    https://app.timelines.ai/integrations/api/chats/12345678/messages
{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}
13

Verificar si un mensaje fue realmente entregado

¿Recibió Juan el mensaje de la factura que mandé esta mañana?
How it works

cada envío devuelve un message_uid. Más tarde, GET /messages/{uid}/status_history devuelve la línea de tiempo Sent / Delivered / Read. La entrega es normalmente en uno o dos segundos en un número activo. El skill whatsapp-delivery-check del repo compañero envuelve esto.

$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    https://app.timelines.ai/integrations/api/messages/OUTBOUND-UID-PLACEHOLDER/status_history
{"status":"ok","data":[
  {"status":"Sent",     "timestamp":"2026-04-12 12:28:40 +0000"},
  {"status":"Delivered","timestamp":"2026-04-12 12:28:41 +0000"}
]}

CRM

CRM y analítica

Los endpoints de lectura le dan a tu agente datos suficientes para responder preguntas analíticas y sincronizar estado con tu CRM en lenguaje natural.

14

Reportes de tiempo de respuesta

¿Cuál fue nuestro tiempo medio de primera respuesta en WhatsApp esta semana?
¿Quién de mi equipo es el más lento en responder?
How it works

trae los chats recientes, luego para cada chat trae la línea de tiempo de mensajes, encuentra el primer saliente después de cada entrante y agrega los deltas del lado del cliente. Dos endpoints, sin llamada analítica especial.

$ # Pull the last page of a chat's messages (50 per page, fixed).
$ # Messages nest under data.messages — not a flat .data[] array.
$ # FILTER message_type=="whatsapp" to exclude notes (see Things to know
$ # below — notes live in the same timeline with from_me=false and would
$ # otherwise corrupt the response-time calculation).
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    "https://app.timelines.ai/integrations/api/chats/12345678/messages?page=1" \
  | jq -r '.data.messages[] | select(.message_type=="whatsapp") | "\(.timestamp)\tfrom_me=\(.from_me)\t\(.text[0:40])"'

# 2026-04-12 12:28:40 +0300  from_me=false  Hi - is the order shipped?
# 2026-04-12 12:30:15 +0300  from_me=true   Yes - tracking ABC123, ETA 2-3
# 2026-04-12 14:02:11 +0300  from_me=false  Got it, thanks!
# ...

$ # Compute first-reply latency from each consecutive (false -> true)
$ # pair, then average across all chats. Pure client-side aggregation.
$ # For full history walk ?page=2, ?page=3, ... until data.has_more_pages is false.
15

Detección de mensajes sin contestar

¿Cuántos mensajes recibimos ayer? ¿Cuántos siguen sin respuesta?
Muéstrame cada chat con un mensaje entrante en las últimas 24 horas y sin respuesta.
How it works

GET /chats?read=false para los chats no leídos, luego filtra a .data.messages[] donde message_type=="whatsapp" Y from_me==false — las notas también llevan from_me=false e inflarían el conteo de no respondidos. Mira Cosas que saber.

16

Resumir conversaciones a demanda

Resume toda la conversación con ACME Corp. ¿Cuáles son sus puntos de dolor?
Dame un brief de un párrafo sobre cada chat que aún no he respondido.
How it works

trae GET /chats/{id}/messages, filtra a .data.messages[] donde message_type=="whatsapp" (las notas meterían los garabatos previos de tu propio agente en el resumen como si fueran frases del cliente — mira Cosas que saber), luego deja que OpenClaw resuma. Pagina con ?page=N para hilos largos.

17

Enriquecer tu CRM con la actividad de WhatsApp

Para cada chat nuevo de esta semana, busca el número en HubSpot. Si es un contacto, etiqueta el chat con su etapa de deal. Si no, crea el contacto.
How it works

combina las lecturas de TimelinesAI con la API de tu CRM. Escribe los resultados de vuelta al chat con POST /chats/{id}/labels y POST /chats/{id}/notes.

$ # 1. Pull recent chats with their phone numbers. /chats returns 50 per
$ # page under data.chats — paginate with ?page=N if you need more.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    "https://app.timelines.ai/integrations/api/chats?page=1" \
  | jq -r '.data.chats[] | "\(.id)\t\(.phone)"'

# 12345678   +15550100
# 12345679   +15550200
# ...

$ # 2. For each phone, look up your CRM (HubSpot/Pipedrive/Close).
$ #    Pseudo-code:
$ #      contact = hubspot.search_by_phone(phone)
$ #      stage   = contact.deal_stage if contact else "unknown"

$ # 3. Add the deal stage label WITHOUT wiping existing labels.
$ # POST /labels is REPLACE semantics (full set), so GET current labels,
$ # append the new one, and POST the combined list.
$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    https://app.timelines.ai/integrations/api/chats/12345678/labels \
  | jq -c '.data.labels + ["hubspot/qualified-lead"] | unique | {labels: .}' \
  > /tmp/labels.json
$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/labels.json \
    https://app.timelines.ai/integrations/api/chats/12345678/labels
18

Puntuar leads a partir del contenido de la conversación

Puntúa cada chat etiquetado inbound-lead del 1 al 10 por encaje y urgencia. Escribe la puntuación como una nota.
How it works

razonamiento LLM sobre GET /chats/{id}/messages — filtra a message_type=="whatsapp" primero, de lo contrario tus propias notas previas de lead-score se leerán como palabras del cliente y la puntuación derivará en cada pasada. Escribe el resultado con POST /chats/{id}/notes con un prefijo predecible (p.ej. "lead_score: fit=8 urgency=6") para que tu siguiente pasada pueda encontrarlo y reemplazarlo.


Operaciones

Escalado y handoff

Patrones para flujos human-in-the-loop, enrutado multi-agente y memoria de conversación. Las cuatro capacidades de abajo están diseñadas para coexistir con humanos trabajando los mismos chats desde la bandeja compartida de TimelinesAI.

19

Redactar respuestas para revisión humana en lugar de enviarlas

Para cada mensaje entrante nuevo, redacta una respuesta y guárdala como nota. No envíes — yo las reviso y las mando.
How it works

POST /chats/{id}/notes con el texto del borrador en lugar de /messages. La nota aparece en la misma vista de chat que tu equipo ya usa.

20

Pasar a un humano cuando el agente se atasca

Si la conversación pasa de 5 turnos sin resolución, o el cliente pide un humano, etiqueta escalate y deja de responder hasta que yo limpie la etiqueta.
How it works

cuenta los turnos con GET /chats/{id}/messages, comprueba etiquetas de stop-reply con GET /chats/{id}/labels antes de cada envío. Si se dispara el escalado, POST /chats/{id}/labels con escalate y sal.

$ # 1. Count outbound 'whatsapp' messages (skip notes) in this chat.
$ # Messages nest under data.messages — not a flat .data[] array.
$ TURNS=$(curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    "https://app.timelines.ai/integrations/api/chats/12345678/messages?page=1" \
  | jq '[.data.messages[] | select(.from_me==true and .message_type=="whatsapp")] | length')
$ echo $TURNS
8

$ # 2. Past the threshold? Append 'escalate' to the chat's labels and stop.
$ # POST /labels REPLACES the full set, so read existing first, combine,
$ # then POST — otherwise you wipe every other label on the chat.
$ if [ "$TURNS" -ge 5 ]; then
    curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
      https://app.timelines.ai/integrations/api/chats/12345678/labels \
    | jq -c '.data.labels + ["escalate"] | unique | {labels: .}' \
    > /tmp/labels.json
    curl -sS -X POST \
      -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
      -H "Content-Type: application/json" \
      --data-binary @/tmp/labels.json \
      https://app.timelines.ai/integrations/api/chats/12345678/labels
  fi

$ # 3. Before every future send, GET /chats/{id}/labels and bail
$ #    if .data.labels contains 'escalate' or 'needs-human'.
21

Correr varios agentes especializados en una sola bandeja

El agente de Ventas atiende preguntas de precio, el de Soporte atiende preguntas de producto. Enruta por intención; si los dos dudan, escálame.
How it works

dos skills especialistas en un mismo workspace de OpenClaw (uno de ventas, uno de soporte) más un tercer skill clasificador de intención que corre primero en cada mensaje entrante, etiqueta el chat con el especialista elegido y sale. Cada especialista lee la etiqueta de intención antes de responder y aborta si apunta al otro — así sólo un skill dispara por mensaje.

22

Recordar conversaciones anteriores con el mismo cliente

La semana pasada me dijiste que ibas a viajar — ¿qué tal te fue?
How it works

la propia memoria de OpenClaw más GET /chats/{id}/messages para todo el historial de WhatsApp. El historial sobrevive entre invocaciones porque vive en TimelinesAI.


Testing

Prueba tu agente end-to-end con un segundo número de WhatsApp

Después del setup puedes ver las capacidades listadas aquí, pero no puedes ver tu agente en acción hasta que un cliente real le escribe. Ese es un mal loop para iterar. Aquí tienes uno mejor: conecta un segundo número de WhatsApp al mismo workspace de TimelinesAI, úsalo como un cliente scripted y observa a tu skill real manejar la conversación end-to-end. Sin mocks, sin webhooks falsos — es tu agente real, tu API real, solo con un segundo número jugando el papel del humano al otro lado.

Prerequisito — dos números de WhatsApp conectados. Este patrón necesita dos números en tu workspace de TimelinesAI: uno actuando como persona cliente, uno corriendo tu stack real de agente. Si ahora mismo solo tienes un número conectado, salta a la capability #19 (Borradores para revisión humana) en su lugar — te da un loop de iteración más lento pero igualmente útil donde tu agente escribe borradores como notas y tú los apruebas en el dashboard.

El patrón

TimelinesAI permite que varios números de WhatsApp vivan en un solo workspace y rutea sus webhooks entrantes por el mismo receptor. El truco es el campo whatsapp_account_id en cada payload de webhook — lleva el JID del número que recibió el mensaje. Léelo, haz switch sobre él, y despacha al paso persona (cuando tu agente acaba de responderle al cliente) o al paso agent (cuando el cliente acaba de escribirle a tu agente). Cada lado envía vía el POST /messages o POST /chats/{id}/messages normales desde la perspectiva del otro.

Arquitectura

  [+1 555 0100]                   [+1 555 0200]
     persona                    agent under test
        |                              |
        |    one TimelinesAI workspace |
        |                              |
        +------->  Public API  <-------+
                        |
                 message:received:new
                        |
                        v
           +--------------------------+
           |  Your test receiver      |
           |  switch (accountJid) {   |
           |    persona_jid -> next   |
           |    agent_jid   -> skill  |
           |  }                       |
           +--------------------------+

Persona sender

El lado persona es un cliente scripted: una lista de líneas para decir a continuación. Cuando es hora del siguiente turno, POST a /messages con el número de teléfono del agente como destinatario. TimelinesAI maneja el ruteo porque ambos números están en un mismo workspace.

// persona-sender.js — send the next scripted customer turn from the
// persona WhatsApp number to the agent-under-test number. TimelinesAI
// routes the send because both numbers share one workspace.

const PERSONA_SCRIPT = [
  "hi I saw your ad",
  "we want to reduce customer churn",
  "we are 25 people",
  "we need to roll out in Q3",
];
let idx = 0;

export async function sendNextPersonaTurn() {
  if (idx >= PERSONA_SCRIPT.length) return;
  const text = PERSONA_SCRIPT[idx++];

  await fetch("https://app.timelines.ai/integrations/api/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.TIMELINES_AI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: Buffer.from(JSON.stringify({
      phone: process.env.AGENT_UNDER_TEST_PHONE,
      text,
    }), "utf-8"),
  });
  console.log(`[persona] sent: ${text}`);
}

Receptor con jid-switch

Un receptor, un branch por lado. Todo lo demás que ya conoces del Setup — la auth con ?secret=, el filtro from_me de echoes, el fallback flat-vs-nested del payload — se aplica aquí sin cambios.

// api/webhook.js — route events by which number received them
export default async function handler(req, res) {
  if (req.query.secret !== process.env.WEBHOOK_SECRET) return res.status(404).end();
  const { event_type, data } = req.body || {};
  if (event_type !== "message:received:new") return res.status(200).end();
  if (data?.from_me === true) return res.status(200).end();
  res.status(200).json({ ok: true });

  const accountJid = data.whatsapp_account_id ?? data.chat?.whatsapp_account_id;
  if (accountJid === process.env.AGENT_UNDER_TEST_JID) {
    // Customer just messaged the agent — run your agent-under-test skill
    await handleAgentTurn(data);
  } else if (accountJid === process.env.PERSONA_JID) {
    // Agent just replied to the persona — advance the scripted scenario
    await advancePersona(data);
  }
}

Esos dos snippets son la idea core. El harness completo — scripts de persona, cargador de escenarios, kickoff por CLI, logger split-pane con colores, config de Vercel — vive en examples/test-harness/ en el repo compañero, con su propio README que te guía por el deploy, los env vars, el registro del webhook y correr tu primer escenario end-to-end.

Mirándolo pasar

El harness corre autónomo — no necesita que apruebes cada respuesta. Tu supervisión sucede en el dashboard de TimelinesAI: abre el chat entre tu número persona y tu número agente en otra pestaña del navegador y cada turno aparece en vivo. Si el agente dice algo tonto, envía una respuesta manual desde la bandeja del agente para redirigir la conversación. Si necesitas cambiar el comportamiento en medio de un run, añade una label o escribe una nota en el chat — ambas son visibles para el skill del agente en su siguiente turno. Para el run borrando el webhook registrado o apagando la función de Vercel; el escenario se detiene inmediatamente.


Ejemplo

Ejemplo: una vuelta completa

Un loop concreto de cinco pasos que enseña cómo se ve la API en la práctica — cómo tu agente envía un saliente, confirma la entrega, procesa la respuesta del cliente y manda un follow-up. Los números de teléfono, chat IDs y message UIDs de abajo son placeholders — sustitúyelos por los tuyos.

Placeholders usados a lo largo de este ejemplo:

Your business number   → +1 555 0100 (JID: 15550100@s.whatsapp.net)
Your customer's number → +1 555 0200
API token              → $TIMELINES_AI_API_KEY
1

Confirma que tu token funciona y lista tus números conectados

$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    https://app.timelines.ai/integrations/api/whatsapp_accounts

{"status":"ok","data":{"whatsapp_accounts":[
  {"id":"15550100@s.whatsapp.net","phone":"+15550100",
   "status":"active","account_name":"Your Business"}
]}}

Deberías ver una lista de tus números conectados. Si el estado no es active, aréglalo en el dashboard de TimelinesAI antes de continuar.

2

Envía un mensaje saliente

Escribe el payload a un archivo con codificación UTF-8 y luego pasáselo a curl. Este es el patrón para todos los envíos.

$ cat > /tmp/send.json <<'JSON'
{"phone":"+15550200",
 "text":"Hi - your order shipped. Tracking: ABC123."}
JSON

$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/send.json \
    https://app.timelines.ai/integrations/api/messages

{"status":"ok","data":{"message_uid":"OUTBOUND-UID-PLACEHOLDER"}}

La respuesta es un recibo, no una confirmación de entrega. Guárdate el message_uid para el siguiente paso.

3

Verifica el estado de entrega

$ curl -sS -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    .../messages/OUTBOUND-UID-PLACEHOLDER/status_history

{"status":"ok","data":[
  {"status":"Sent",     "timestamp":"2026-04-12 12:28:40 +0000"},
  {"status":"Delivered","timestamp":"2026-04-12 12:28:41 +0000"}
]}

Sent → Delivered suele ser cuestión de un segundo en un número activo. El estado Read aparece más tarde, cuando el destinatario abre el chat.

4

El cliente responde y tu webhook se dispara

Cuando el cliente responde, TimelinesAI hace POST a tu URL de webhook registrada:

{
  "event_type": "message:received:new",
  "data": {
    "chat_id": 12345678,
    "message_uid": "INBOUND-UID-PLACEHOLDER",
    "sender_phone": "+15550200",
    "sender_name": "Customer",
    "text": "Thanks! When will it arrive?",
    "timestamp": "2026-04-12 12:29:20 +0000"
  }
}

Tu receptor responde 200 inmediatamente y luego pasa el payload a tu skill de OpenClaw.

5

Responde de vuelta dentro del mismo chat

$ cat > /tmp/reply.json <<'JSON'
{"text":"Estimated delivery is 2-3 business days. You'll get tracking updates to this chat."}
JSON

$ curl -sS -X POST \
    -H "Authorization: Bearer $TIMELINES_AI_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @/tmp/reply.json \
    https://app.timelines.ai/integrations/api/chats/12345678/messages

{"status":"ok","data":{"message_uid":"REPLY-UID-PLACEHOLDER"}}

Como estás enviando dentro de un chat existente con /chats/{id}/messages, el remitente es automáticamente el número de WhatsApp dueño del chat — tú no lo eliges, lo elige el chat. Ese es el motivo por el que no envías accidentalmente desde el número equivocado en workspaces multi-número.


Límites

Límites y advertencias

Lo que esta guía NO cubre, y por qué.

!
Los números personales se banean por cold outreach. Los broadcasts no solicitados desde números personales no son un caso de uso soportado. El baneo se aplica en la infraestructura de WhatsApp, no en el gateway de TimelinesAI, así que no podemos protegerte de eso.
!
La ventana de servicio al cliente de 24 horas de WhatsApp. Puedes responderle a un cliente libremente durante 24 horas después de su último mensaje. Fuera de esa ventana, los mensajes requieren opt-in y plantillas — territorio Business API.
!
Entrega asíncrona. POST /messages devuelve un message_uid (un recibo), no una confirmación de entrega. Usa /status_history para confirmar la entrega real.
!
Las URLs de adjuntos expiran rápido. Descarga los media inline en el handler del webhook, no desde un worker diferido.
!
Solo texto, media, reacciones y metadatos — sin llamadas de voz/video, sin estado de broadcast, sin Channels o Stories de WhatsApp.

Bloques de construcción

Bundle de skills compañero en GitHub →

4 skills funcionando, un receptor de webhook en Vercel, docs de cumplimiento y un mirror completo de esta guía. Licencia MIT.

Docs de la API de TimelinesAI · Docs de los Skills de OpenClaw · release v0.1.0

Guía de capacidades · 2026 · URL canónica timelines.ai/guide/openclaw-whatsapp-skills