Volver al blog
Desarrolladores

Idempotency-Key y scopes de la API de Waspy

Idempotency-Key en POST /messages dura 24 horas. Un replay devuelve el mismo mensaje; un choque concurrente es 409. El lote no usa esa clave. Los scopes achican la key.

· 12 min
Idempotency-Key y scopes de la API de Waspy

Respuesta corta: para que un reintento no mande dos veces el mismo WhatsApp, POST https://api.waspytech.com/api/v2/messages lleva el header Idempotency-Key (hasta 256 caracteres). Dura 24 horas por cuenta. Si la primera llamada terminó bien, la repetición devuelve la misma respuesta con Idempotency-Replayed: true. Si la primera todavía está en curso, es 409 IDEMPOTENCY_CONFLICT. POST /messages/batch no usa ese header.

Los scopes son la otra mitad: una clave de un ERP que solo avisa despachos no tiene por qué leer Instagram, y un script que arma un reporte no tiene por qué poder enviar. La referencia de la clave está en idempotencia y scopes. El lote está en enviar mensajes en lote.

Qué hace el header

SituaciónQué responde la API
Primera vez, y el mensaje se guardó201, con data.id
La misma clave, después del 201El mismo cuerpo, header Idempotency-Replayed: true. meta.requestId es el de esta llamada
La misma clave, mientras la primera sigue409 IDEMPOTENCY_CONFLICT
La primera falló antes de guardar el mensajeEl lock cae a los 30 segundos. Después, la misma clave se procesa de nuevo
Pasaron más de 24 horasLa clave se puede reutilizar y es un envío nuevo
La comparación es el string entero, no un hash. orden-1847 y orden-1848 son dos envíos. La clave es única por cuenta: dos empresas pueden usar el mismo texto sin pisarse.

Waspy no compara el body. La misma clave con otro JSON devuelve la respuesta guardada de la primera. Si reutilizás orden-1847-confirmacion para el despacho, el cliente no recibe el despacho: recibís el id del mensaje de confirmación. La clave tiene que identificar la operación (orden-1847-confirmacion, orden-1847-despacho), no ser un UUID al azar.

El mensaje de conflicto que devuelve el servidor es Request with this idempotency key is already being processed. La página de idempotencia lo explica en español; el error.code es IDEMPOTENCY_CONFLICT en los dos lados. El sobre es error.code, error.message, error.status y meta.requestId, igual que en la guía para desarrolladores.

curl -sS -D - -X POST https://api.waspytech.com/api/v2/messages \
  -H "Authorization: Bearer $WASPY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: orden-1847-confirmacion" \
  -d '{
    "phoneNumberId": "UUID-DEL-CANAL",
    "to": "+5491126032641",
    "type": "template",
    "template": {
      "name": "confirmacion_pedido",
      "language": { "code": "es_AR" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "María" },
            { "type": "text", "text": "1847" }
          ]
        }
      ]
    },
    "clientRef": "orden-1847-confirmacion"
  }'

Si el ERP hace timeout, repetí la misma clave. No generes otra: eso anula el mecanismo y puede mandar el aviso dos veces. clientRef es otra cosa: viaja en el webhook message.status_changed para que reconcilies. No reemplaza al header.

El lote no es idempotente

POST /messages/batch no lee Idempotency-Key. Un reintento del mismo array vuelve a encolar los ítems que ya habían salido queued. La defensa es guardar clientRef y no reenviar los aceptados. Si cada aviso tiene que ser idempotente por sí mismo, mandalo con POST /messages, una clave por operación, y pagá el costo de una request por mensaje (el rate limit es por request: 120 por minuto por cuenta).

Scopes que existen

Una clave se crea en el panel, en Configuración → Claves de API. La crea un owner, un admin o un super admin. Otra clave de API no puede crear claves. El valor wspy_... se muestra una sola vez.

Si no marcás permisos, la clave puede llamar todo lo que la API permite. Si marcás una lista, solo esas rutas. El servidor, cuando falta un permiso, responde 403 FORBIDDEN con API key missing required scope: messages:write (o el scope que falte). La página de scopes muestra un ejemplo con código INSUFFICIENT_SCOPE; el requireScope del API responde FORBIDDEN.

ScopePara qué alcanza
account:readGET /account
channels:readGET /channels
contacts:read / contacts:writeLeer contactos / POST y PATCH
conversations:readListar chats y GET /conversations/:id/messages
conversations:writeActualizar el chat, marcar leído, typing, POST /messages/:waMessageId/read
messages:readGET /messages/:id
messages:writePOST /messages y POST /messages/batch
templates:readListar plantillas y ver una por id o por nombre
media:readGET /media/:id y GET /media/by-key
media:writeFigura en la lista. Subir por POST /media no está expuesto: limitaciones
webhooks:read / webhooks:writeLeer suscripciones / crearlas, rotar el secreto, probar y reenviar
social:readGET /api/v2/social (posts, comentarios, seguidores)
social:read está en el servidor (VALID_SCOPES) y la página de scopes todavía no lo lista. La tabla de esa página tampoco nombra POST /messages/batch dentro de messages:write; la referencia de messages sí dice que el lote usa el mismo scope. GET /conversations/:id/messages pide conversations:read. GET /messages/:id pide messages:read. Para un reporte de solo lectura, marcá los dos.

El conector MCP (Claude, ChatGPT, Cursor, Codex, Grok) usa contactos, conversaciones, mensajes y plantillas. Ahí existe templates:write, que crea una plantilla y la manda a revisión de Meta: en la API REST ese alta no está. social:read no está en el MCP. Cómo conectarlo está en Claude, ChatGPT y Cursor. Las métricas de Instagram, si las necesitás, van por REST: métricas por API.

Dos claves, dos trabajos

ERP que avisa el despacho

messages:write
templates:read
webhooks:write

messages:write encola el aviso y el lote. templates:read deja ver que aviso_despacho está approved antes de disparar (el filtro status va en minúscula). webhooks:write da de alta el message.status_changed para marcar el pedido como notificado cuando Meta confirma delivered o failed.

No le des contacts:write si el ERP no edita fichas. No le des social:read si no mira Instagram. No dejes la clave sin scopes “porque es más fácil”: esa clave también podría borrar un webhook o leer todos los chats.

Script que solo lee

conversations:read
messages:read
contacts:read
templates:read

Alcanza para armar un reporte o para que un asistente externo resuma la bandeja. Un POST /messages con esta clave responde 403. Es el conjunto razonable para pegarle el MCP a Cursor un lunes y no descubrir el martes que mandó una plantilla. El envío, si hace falta, es otra clave, y una persona confirma.

La guía de scopes muestra un ejemplo parecido para un dashboard (account:read, contacts:read, conversations:read, templates:read) y otro para notificaciones (messages:write, contacts:read, templates:read). Sumá messages:read si el script va a hacer polling de GET /messages/:id.

Los scopes no suben el plan. La API se habilita en Scale (US$150 por mes) y Enterprise. Starter (US$70) y Growth (US$100) no crean claves. El cupo de la plataforma está en Planes. Meta cobra aparte cada mensaje entregado: en Argentina, desde el 1 de octubre de 2026, marketing ARS 89,56 (~US$0,0618) y utilidad o servicio (después de 1.000 por número y por mes) ARS 37,68 (~US$0,026). La rate card es la fuente. Waspy no agrega markup.

Si la integración todavía no tiene número, abrí la cuenta: 10 días sin tarjeta, y la clave aparece cuando el plan es Scale. La referencia de envío está en la página de la API.

Tu equipo atendiendo por WhatsApp en minutos

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