Volver al blog
Desarrolladores

clientRef: reconciliar los estados de WhatsApp con tu ERP

clientRef es un texto de hasta 256 caracteres. Lo mandás en POST /messages y vuelve en message.status_changed y en GET /messages/:id. Al encolar, waMessageId es null.

· 12 min
clientRef: reconciliar los estados de WhatsApp con tu ERP

Respuesta corta: mandá clientRef (máximo 256 caracteres) en POST https://api.waspytech.com/api/v2/messages. Waspy lo devuelve igual en la respuesta, en GET /messages/:id y en cada webhook message.status_changed. Con eso tu ERP marca el pedido como entregado o leído sin guardar una tabla de ids. En la respuesta inicial, status es queued y waMessageId es null: el id de Meta aparece recién cuando el mensaje sale.

El lote de hasta 100 está en mensajes en lote. La clave que evita un doble envío es otra cosa: idempotencia y scopes. El payload del webhook está en eventos.

Qué problema resuelve

Tiendanube, o el ERP, tiene el pedido 1847. WhatsApp tiene un wamid que todavía no existe cuando encolás. Si guardás solo el UUID de Waspy, cada estado te obliga a buscar. Si mandás clientRef: "tn_1847_pago", el webhook trae ese texto y el estado nuevo. Actualizás la fila del pedido y listo.

phoneNumberId en el POST es el UUID del canal en Waspy, el id de GET /channels. No es el phoneNumberId de Meta. Ese otro campo también viene en el canal, con otro nombre.

Del pedido al webhook

  1. El pedido queda pagado en tu sistema.
  2. POST /messages con la plantilla y clientRef.
  3. La API responde 201. data.status es queued, data.waMessageId es null, data.clientRef es el tuyo, data.id es el UUID de Waspy.
  4. Llega message.status_changed con newStatus: "sent" y, ahí sí, waMessageId.
  5. Después delivered, read o failed. Siempre con el mismo clientRef.
curl -sS -X POST https://api.waspytech.com/api/v2/messages \
  -H "Authorization: Bearer $WASPY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: tn-1847-pago" \
  -d '{
    "phoneNumberId": "UUID-DEL-CANAL",
    "to": "+5491155554444",
    "type": "template",
    "clientRef": "tn_1847_pago",
    "template": {
      "name": "confirmacion_pago",
      "language": { "code": "es_AR" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "1847" }
          ]
        }
      ]
    }
  }'
{
  "data": {
    "id": "msg-uuid",
    "conversationId": "conv-uuid",
    "status": "queued",
    "waMessageId": null,
    "clientRef": "tn_1847_pago",
    "type": "template"
  },
  "meta": { "requestId": "..." }
}

El scope del POST es messages:write. La plantilla tiene que estar approved. Cómo listarla está en listar plantillas.

El webhook real

Cuando Meta acepta el envío, el cuerpo que llega a tu URL tiene esta forma (el sobre es el de eventos):

{
  "event": "message.status_changed",
  "webhookId": "uuid-de-tu-suscripcion",
  "deliveredAt": "2026-10-24T14:02:01.000Z",
  "attempt": 1,
  "data": {
    "id": "msg-uuid",
    "conversationId": "conv-uuid",
    "contactId": "uuid-contacto",
    "phoneNumberId": "uuid-canal",
    "direction": "outbound",
    "type": "template",
    "status": "delivered",
    "waMessageId": "wamid.HBgLNT...",
    "errorCode": null,
    "errorMessage": null,
    "origin": "external_api",
    "clientRef": "tn_1847_pago",
    "previousStatus": "sent",
    "newStatus": "delivered"
  }
}
CampoPara el ERP
data.clientRefTu clave. Si no la mandaste, null
data.newStatussent, delivered, read o failed
data.previousStatusEl estado anterior de este evento
data.idUUID de Waspy, por si también guardaste el 201
data.waMessageIdEl wamid de Meta. En queued todavía no está
data.errorCodeSi newStatus es failed. Ejemplo de Meta: 131026, 131049
Verificá la firma antes de tocar el pedido. El algoritmo está en verificar la firma.

Los eventos pueden llegar fuera de orden. Por cada clientRef guardá el estado más avanzado (read > delivered > sent) y tratá failed como terminal: un delivered tardío no revive un fallo. Si el mensaje no se entregó, Meta no lo cobra.

Si no querés un webhook

GET /api/v2/messages/:id con scope messages:read devuelve el mismo mensaje, con clientRef, status y waMessageId. El :id es el UUID del 201, no el wamid.

Polling de muchos pedidos se come el cupo de 120 pedidos por minuto por cuenta. El webhook es el camino. El GET sirve para un pedido puntual o para recuperar un waMessageId si se te pasó el evento sent.

El lote también lo devuelve

En POST /messages/batch cada ítem puede traer su clientRef. La respuesta es 202. Los que se encolan vuelven result: "queued" con el mensaje adentro, waMessageId: null y el clientRef repetido en el ítem.

{
  "index": 0,
  "clientRef": "tn_1847_pago",
  "result": "queued",
  "message": {
    "id": "msg-uuid",
    "status": "queued",
    "waMessageId": null,
    "clientRef": "tn_1847_pago"
  }
}

Un ítem skipped (opt-out) o failed (teléfono inválido, ventana cerrada) no tiene mensaje en camino: no esperes un delivered para ese clientRef. El header Idempotency-Key no aplica al lote. Si reintentás la tanda, los que ya estaban queued se encolan otra vez, con el mismo clientRef. Persistí el resultado antes de reintentar. El detalle del 202 está en el lote.

clientRef no es la clave de idempotencia

clientRefIdempotency-Key
DóndeBody, en el mensaje y en cada ítem del loteHeader, solo en POST /messages
Para quéQue el estado vuelva con tu idQue un reintento del mismo POST no mande dos veces
Tope256 caracteres256 caracteres, y dura 24 horas
En el webhookSí, en message.status_changedNo
Las dos se pueden usar juntas, como en el curl de arriba: la clave evita el duplicado y clientRef reconcilia. Un mismo clientRef en dos POST distintos son dos mensajes.

Qué cobra Meta cuando el estado es delivered

El delivered es el momento en que Meta puede facturar, según la categoría de la plantilla y el país del destinatario. En Argentina, desde el 1 de octubre de 2026:

CategoríaDólaresPesos
MarketingUS$0,0618ARS 89,56
Utilidad y autenticaciónUS$0,026ARS 37,68
Servicio, dentro de la ventana1.000 por número y por mes; después US$0,026después ARS 37,68
Waspy no suma markup. La rate card es la fuente. Un failed no entra en esa cuenta. El desglose está en cuánto cuesta la API.

Si el aviso es texto libre y la ventana de 24 horas está cerrada, el POST ni siquiera encola: responde 403 SERVICE_WINDOW_EXPIRED. Ahí no hay webhook de estado. La regla está en la ventana de 24 horas.

La referencia del campo está en messages. La API está en Scale y Enterprise: funcionalidades.

Tu equipo atendiendo por WhatsApp en minutos

CRM facil pero potente. En 10 minutos tenes a tu equipo trabajando — no necesitas hablar con nadie.