Indicador «escribiendo…» y marcar como leído en la API de WhatsApp (Waspy)
POST /conversations/:id/typing con status on muestra «escribiendo…». Meta lo apaga a los ~25 s. off no llama a Meta. Marcar leído es otro POST y no se cobra como mensaje.
Respuesta corta: el “escribiendo…” es POST /api/v2/conversations/:id/typing con { "status": "on" }. Marcar leído es otro pedido: POST /api/v2/conversations/:id/read, o POST /api/v2/messages/:waMessageId/read si tenés el id de WhatsApp del webhook. Los dos piden el scope conversations:write. Ninguno le manda un texto al cliente y ninguno se cobra como mensaje.
La referencia está en conversations y en messages. El listado de chats y el PATCH de estado están en conversaciones por API. Cómo conseguir el conversationId desde el CRM está en abrir el chat sin UUID.
Cuándo usarlo
Tiene sentido mientras una persona, o tu sistema, busca el pedido y la respuesta real tarda unos segundos. El cliente ve “escribiendo…” en vez de un “ya te respondo” que después hay que limpiar.
No lo uses como keepalive. Meta apaga el indicador a los ~25 segundos o cuando sale un mensaje. Si la consulta tarda más, podés mandar on otra vez. Un loop cada dos segundos no se lee mejor y le pega a la Cloud API al pedo.
Tampoco reemplaza el mensaje. Si la ventana de 24 horas está cerrada, el texto libre no sale (POST /api/v2/messages responde 403 SERVICE_WINDOW_EXPIRED). El typing no abre esa ventana. El camino sigue siendo una plantilla, explicada en la API para desarrolladores.
Si el último saliente lo escribió una persona del equipo (origin panel), tu integración no debería mostrar que “está escribiendo” ni contestar encima. El criterio está en handoff.
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" }'
200: conversationId, status, deliveredToMeta y, si aplica, reason.
Waspy toma el último mensaje entrante con waMessageId y llama a la Cloud API con status: "read" y typing_indicator: { "type": "text" } sobre ese id. Por eso el cliente puede ver el doble tilde azul y el “escribiendo…” en el mismo paso. El unreadCount de la bandeja de Waspy no baja con este POST. Para bajarlo, marcá leído.
status: "off" se acepta y no llama a Meta. Si ya había un entrante para anclar y el canal existe, 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. Meta igual no ofrece un “dejar de escribir”: se apaga solo.
El off solo llega a ese no-op si antes encontró canal y entrante. Si no hay entrante, aunque pidas off, el reason es no_inbound_to_anchor.
reason (typing) | deliveredToMeta | Qué pasó |
|---|---|---|
| (vacío) | true | Meta aceptó el on |
typing_off_noop | true | Pediste off. No se llamó a Meta |
no_phone_number | false | La conversación no tiene canal de WhatsApp |
no_inbound_to_anchor | false | No hay un entrante reciente con waMessageId. Meta exige uno |
phone_not_found | false | El canal no existe en esta cuenta |
meta_rejected | false | Meta rechazó el llamado (ventana, permiso, id inválido) |
meta_unreachable | false | No se pudo llegar a Graph (red o token) |
Conversation not found y el código es NOT_FOUND.
Marcar leído
curl -sS -X POST "https://api.waspytech.com/api/v2/conversations/$CONV_ID/read" \
-H "Authorization: Bearer $WASPY_API_KEY"
Waspy pone unreadCount en 0 antes de hablar con Meta. Después manda status: "read" sobre el último entrante con waMessageId. Si Meta no lo acepta, el no leído local igual quedó en cero y deliveredToMeta viene false con un reason.
reason (read) | Qué pasó |
|---|---|
no_inbound_to_anchor | No hay entrante con waMessageId, o la conversación no tiene phoneNumberId. En el read, esos dos casos comparten este reason |
phone_not_found | El canal guardado no está en la cuenta |
meta_rejected | Meta rechazó el recibo |
meta_unreachable | Graph no respondió, o el token no se pudo usar |
typing_off_noop ni no_phone_number en este recurso.
Desde el webhook, si ya tenés el waMessageId y todavía no el conversationId:
curl -sS -X POST "https://api.waspytech.com/api/v2/messages/$WA_MESSAGE_ID/read" \
-H "Authorization: Bearer $WASPY_API_KEY"
Ese POST busca un mensaje entrante de tu cuenta con ese waMessageId. Si no está, o si es saliente, 404 NOT_FOUND (Inbound message not found). Si está, marca leída la conversación: el recibo que sale a Meta es el del último entrante, que puede ser más nuevo que el id que pasaste. markedMessageId en la respuesta te dice cuál se usó.
Los dos read comparten scope conversations:write, documentado en scopes. No aceptan Idempotency-Key: llamarlos dos veces puede pegarle a Meta dos veces. El header de idempotencia es de POST /api/v2/messages.
Una secuencia que no spamea texto
- Llega
message.received. Guardás elwaMessageId. - Si tu sistema va a tardar (consulta de stock, de Andreani, del ERP), un solo
typingconon. - Cuando la respuesta está lista,
POST /messagescon el texto. Meta apaga el indicador al enviar. POST /readpara que el contador de la bandeja quede en cero, si el typing no te alcanza porque no toca elunreadCount.
No mandes “un segundo”, “ya te digo” y “sigo acá” como tres textos. Cada uno, dentro de la ventana, es un mensaje de servicio: 1.000 por número y por mes desde el 1 de octubre de 2026 y, desde el 1.001, alrededor de ARS 37,68 (US$0,026) en Argentina. El typing y el leído no entran en esa cuenta. La rate card está en la documentación de precios de Meta.
Un asistente externo puede leer el chat por la API o por MCP. El “escribiendo…” y el mensaje al cliente los dispara tu sistema, con esta secuencia, o los confirma una persona. Waspy no contesta la tienda solo. La conexión está en Claude, ChatGPT y Cursor.
La clave está en Scale y Enterprise. El detalle está en Planes y en la API. Se puede probar 10 días sin tarjeta en el registro.
