Cómo listar chats de WhatsApp por API, marcarlos leídos y mostrar que estás escribiendo
GET /api/v2/conversations lista los chats. Marcar leído y el “escribiendo…” no le mandan un WhatsApp. El estado que acepta el servidor es open o closed.
Respuesta corta: para listar los chats de una cuenta hacés GET https://api.waspytech.com/api/v2/conversations con Authorization: Bearer wspy_... y el scope conversations:read. El estado que el servidor acepta es open o closed. Para el doble tilde azul, POST /api/v2/conversations/:id/read. Para el “escribiendo…”, POST /api/v2/conversations/:id/typing con {"status":"on"}. Ninguno de los dos le manda un mensaje al cliente.
Sirve para el caso de una software house de Córdoba que mete la bandeja de una tienda de zapatillas en su propio panel: quiere los chats abiertos, encontrar el de +549351… aunque el CRM lo tenga guardado como 54351…, y marcar leído cuando una persona ya lo vio. La referencia está en docs.waspytech.com/api-reference/conversations. Donde esa página y el servidor no coinciden, abajo va lo que acepta el código de apps/api/src/routes/v2/conversations.ts.
La API está en los planes Scale y Enterprise. El detalle comercial está en Planes y en la página de la API.
Donde la doc y el servidor no dicen lo mismo
| Tema | Página de docs | Lo que hace el servidor |
|---|---|---|
status en el listado y en el PATCH | open, pending, resolved, closed | Solo open y closed. Otro valor: 400 INVALID_FILTER |
limit por defecto del listado | 50 | 20 (máximo 100) |
Scope de GET /conversations/:id/messages | conversations:read | messages:read |
| Contacto en el listado | incluye id | name, phoneNumber y email. Sin id |
by-phone y el prefijo 15 | dice que lo elimina | Se queda con los dígitos. El 15 no se recorta |
status de la base también es solo open o closed. No es un filtro a medias.
Listar los chats abiertos
curl -sS "https://api.waspytech.com/api/v2/conversations?status=open&limit=20" \
-H "Authorization: Bearer $WASPY_API_KEY"
La respuesta es 200. data trae, por chat: id, contactId, phoneNumberId, status, assignedTo, lastMessageAt, lastMessagePreview, lastMessageDirection, lastMessageStatus, serviceWindowExpiresAt, createdAt y contact. meta trae requestId, cursor y hasMore.
El orden es el más reciente primero. Si hasMore es verdadero, pasá meta.cursor tal cual en la página siguiente. El cursor es opaco: no lo armes a mano. search filtra por nombre, teléfono o email del contacto. No busca adentro del texto de los mensajes.
phoneNumberId acá es el UUID del canal en Waspy (el id de GET /api/v2/channels), no el id de número de Meta. En el recurso de canales, el id de Meta viene en otro campo que también se llama phoneNumberId. Si se los mezclás, POST /api/v2/messages responde 400 INVALID_CHANNEL.
Una clave sin scopes entra a todo. Si la clave tiene scopes y le falta conversations:read, la respuesta es 403 FORBIDDEN con el mensaje API key missing required scope: conversations:read. La lista de scopes está en docs.waspytech.com/scopes.
El 54 y el 549
Un celular de Argentina, para WhatsApp, va como +549 más el código de área y el número, sin el 15. GET /api/v2/conversations/by-phone/5493514000112 saca todo lo que no sea dígito y busca variantes: con y sin el 9, con y sin +.
curl -sS "https://api.waspytech.com/api/v2/conversations/by-phone/%2B5493514000112" \
-H "Authorization: Bearer $WASPY_API_KEY"
Si no hay chat, la respuesta igual es 200 y data es null. No es un 404. Devuelve el chat más reciente de esas variantes.
Si el CRM tiene teléfono, Instagram o email y no sabe el id, está GET /api/v2/conversations/resolve. line y channel (whatsapp, instagram o email) son preferencias, no un filtro que descarte el resto. Si no hay match, conversationId es null. phoneHasWhatsApp dice si alguno de los matches es un chat de WhatsApp de ese teléfono.
Marcar leído
curl -sS -X POST "https://api.waspytech.com/api/v2/conversations/CONV_ID/read" \
-H "Authorization: Bearer $WASPY_API_KEY"
Scope: conversations:write. El 200 trae conversationId, markedMessageId y deliveredToMeta.
Waspy pone el no leído en cero y, si hay un entrante con waMessageId, llama a la Cloud API de Meta (status: "read" sobre ese id). Si no hay entrante para anclar, deliveredToMeta es false y reason es no_inbound_to_anchor. Otros motivos: phone_not_found, meta_rejected, meta_unreachable. El no leído local igual queda en cero.
Este pedido no crea un mensaje. No consume el cupo de 1.000 mensajes de servicio por número y por mes. El header Idempotency-Key es de POST /api/v2/messages, no de este recurso: llamarlo dos veces puede pegarle a Meta dos veces.
Si el id no existe, o la clave es de una persona que no puede ver ese chat, la respuesta es 404 CONVERSATION_NOT_FOUND. No es un 403: así no se entera de que el chat existe.
“Escribiendo…”
curl -sS -X POST "https://api.waspytech.com/api/v2/conversations/CONV_ID/typing" \
-H "Authorization: Bearer $WASPY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "on" }'
Meta ata el indicador al último entrante con waMessageId y lo apaga solo a los ~25 segundos o cuando sale un mensaje. status: "off" se acepta y no llama a Meta: la respuesta trae deliveredToMeta: true y reason: "typing_off_noop". La página de docs dice que ese caso va con deliveredToMeta: false. El servidor manda true.
Si no hay entrante para anclar, reason es no_inbound_to_anchor. Si Meta rechaza el llamado, meta_rejected. Tampoco es un mensaje cobrable.
Cerrar y asignar
curl -sS -X PATCH "https://api.waspytech.com/api/v2/conversations/CONV_ID" \
-H "Authorization: Bearer $WASPY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "closed", "assignedTo": null }'
assignedTo es el UUID de un usuario de esa cuenta, o null para soltarlo. Si el usuario no está en la cuenta, es 400 VALIDATION_ERROR. Cerrar el chat no impide recibir ni enviar: es el estado de la bandeja. Para contestarle al cliente, el pedido sigue siendo POST /api/v2/messages, explicado en API para desarrolladores. Si la ventana de 24 horas está cerrada y el tipo no es una plantilla, ese otro pedido responde 403 SERVICE_WINDOW_EXPIRED.
Leer los mensajes
curl -sS "https://api.waspytech.com/api/v2/conversations/CONV_ID/messages?limit=50" \
-H "Authorization: Bearer $WASPY_API_KEY"
Scope: messages:read. Vienen del más viejo al más nuevo dentro de la página. El cursor apunta al más viejo de esa página para pedir los anteriores. El máximo de limit es 100. Fotos y PDF de esos mensajes se bajan por GET /api/v2/media/:id, como está en enviar fotos y PDF.
Qué no es este recurso
| Producto | Qué te da |
|---|---|
| Cloud API de Meta | No hay conversación. El leído es un POST al número de Meta con el wamid. Documentación de precios |
| 360dialog | Acceso a la API oficial para desarrolladores, sin esta bandeja. Waspy vs 360dialog |
| Twilio | API programable aparte. Waspy vs Twilio |
| Z-API | Conexión por QR, sesión de WhatsApp Web, no este recurso. Waspy vs Z-API y API no oficial |
conversations:read. Esa conexión está en conectar WhatsApp con Claude y Cursor. El mensaje al cliente lo confirma una persona, salvo un aviso que tu sistema manda a propósito con una plantilla.
Cuánto cuesta mirar la bandeja
Listar, leer, marcar leído y mostrar “escribiendo…” no son un mensaje entregado. Meta no los cobra. Lo que sí cobra es el texto, la foto o la plantilla que después mandás. En Argentina, desde el 1 de octubre de 2026: marketing alrededor de ARS 89,56 (US$0,0618) y utilidad alrededor de ARS 37,68 (US$0,026). Las respuestas de servicio tienen 1.000 por número y por mes; desde la 1.001, la tarifa de servicio. El desglose está en cuánto cuesta la API.
Una tienda de zapatillas en Córdoba que responde 1.400 veces en el mes, desde un solo número, paga las 400 que pasan del cupo: 400 × ARS 37,68 ≈ ARS 15.072 (400 × US$0,026 = US$10,40). Mirar los chats y marcarlos leídos no entra en esa cuenta.
Para probar la clave, el alta está en Configuración → Claves de API. Diez días de prueba, sin tarjeta, en waspytech.com.
Preguntas frecuentes
¿Marcar leído le escribe al cliente? No. No es un mensaje de servicio.
¿Puedo usar status resolved? No. Solo open y closed.
¿Qué scope piden los mensajes del chat? messages:read.
¿Meta tiene el mismo recurso? No. El leído de la Cloud API va con el wamid, sin id de conversación de Waspy.
¿El 15 del celular se borra solo? No en by-phone. Mandá el número en +549, sin el 15.
