Cómo crear contactos por la API de WhatsApp (y no mandar el mensaje sin querer)
POST /api/v2/contacts guarda el teléfono, el nombre y las etiquetas. No le escribe a la persona. Si el número ya existe, la API responde 409. El mensaje es otro pedido.
Respuesta corta: para crear un contacto en la API pública de Waspy hacés POST https://api.waspytech.com/api/v2/contacts con phoneNumber en formato E.164, header Authorization: Bearer wspy_... y el scope contacts:write. La respuesta feliz es 201. Si ese teléfono ya existe en la cuenta, es 409 CONTACT_ALREADY_EXISTS. Este pedido no envía un WhatsApp.
Sirve para el caso de una tienda de Palermo que exporta los clientes del sistema de gestión y quiere tenerlos etiquetados (vip, mayorista, no-molestar) antes de cualquier campaña. La referencia está en docs.waspytech.com/api-reference/contacts y en la guía de contactos. Donde esa guía y el servidor no coinciden, abajo va lo que acepta el código.
La API está en los planes Scale y Enterprise. El detalle comercial está en Planes y en la página de la API.
Qué guarda y qué no
Un contacto público tiene id, phoneNumber, name, email, tags, customFields, optedIn, lastMessageAt y createdAt. No se devuelve el origen interno ni notas de la bandeja.
| Querés | Método | Scope |
|---|---|---|
| Listar o buscar | GET /api/v2/contacts | contacts:read |
| Ver uno | GET /api/v2/contacts/:id | contacts:read |
| Ver sus chats | GET /api/v2/contacts/:id/conversations | contacts:read |
| Crear | POST /api/v2/contacts | contacts:write |
| Actualizar | PATCH /api/v2/contacts/:id | contacts:write |
DELETE. Crear una plantilla tampoco es un POST /api/v2/templates: las plantillas se listan y se envían, y se arman en el panel. Eso está en API para desarrolladores.
Crear la ficha de una clienta
Un celular argentino, para WhatsApp, va como +549 más los 10 dígitos (código de área y número). +5491140001122 cumple el chequeo de la API: + y entre 7 y 15 dígitos. Un 11 15 4000-1122 sin el + no.
curl -sS -X POST https://api.waspytech.com/api/v2/contacts \
-H "Authorization: Bearer $WASPY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "+5491140001122",
"name": "Laura Pérez",
"email": "laura@example.com",
"tags": ["vip", "palermo"],
"customFields": { "local": "Palermo", "ultimaCompra": "2026-10-02" }
}'
phoneNumber es el único campo obligatorio. email, si va, tiene que ser un email. tags es un array. customFields es un objeto.
Si la cuenta tiene un solo número de WhatsApp activo, el contacto queda asociado a ese canal. Si tiene cero o más de uno, se crea igual, sin esa asociación: el log del servidor lo deja asentado y el mensaje de después tiene que indicar phoneNumberId.
La respuesta 201 trae data.id (un UUID) y meta.requestId. optedIn sale en true porque así está el default de la tabla. El body no acepta optedIn. Ese true no es un consentimiento: para una campaña seguís necesitando el opt-in de verdad, como está en cómo pedirlo.
Si ya existía
No hay upsert. El segundo POST del mismo teléfono responde:
{
"error": {
"code": "CONTACT_ALREADY_EXISTS",
"message": "Contact with this phone number already exists",
"status": 409
}
}
Un teléfono inválido es 400 INVALID_PHONE (Phone number must be in E.164 format). Sin phoneNumber, 400 MISSING_REQUIRED_FIELD.
El patrón para sincronizar una base:
GET /api/v2/contacts?search=%2B5491140001122(el+va codificado).- Si
meta.totales 0,POST. - Si ya está,
PATCH /api/v2/contacts/{id}con lo que cambió.
POST /api/v2/contacts no toma el header Idempotency-Key. Ese header existe en POST /api/v2/messages y guarda la respuesta 24 horas. En contactos, el 409 es el freno del duplicado.
Al crear, Waspy emite el webhook contact.created si tenés una suscripción a ese evento. Los cuatro eventos posibles son message.received, message.status_changed, conversation.created y contact.created. El alta del webhook está en webhooks con Node y Python.
Buscar y etiquetar
GET /api/v2/contacts pagina con page (default 1) y pageSize (default 20, máximo 100). search mira nombre, teléfono y email.
El filtro de etiquetas es el query tags, varias separadas por coma:
curl -sS "https://api.waspytech.com/api/v2/contacts?tags=vip,palermo&pageSize=50" \
-H "Authorization: Bearer $WASPY_API_KEY"
En la guía publicada el ejemplo usa tag. El handler y el OpenAPI del repo leen tags. Un tag=vip solo no filtra. Con tags=vip,palermo entra quien tenga al menos una de esas etiquetas (el filtro es solapamiento, no un AND de las dos).
Para cambiar la ficha:
curl -sS -X PATCH "https://api.waspytech.com/api/v2/contacts/UUID-DEL-CONTACTO" \
-H "Authorization: Bearer $WASPY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tags": ["vip", "mayorista"],
"customFields": { "local": "Palermo", "ultimaCompra": "2026-10-18", "lista": "verano" }
}'
tags reemplaza la lista anterior. Si mandás solo ["mayorista"], vip se pierde.
customFields también reemplaza el objeto entero. La guía dice que hace merge: el PATCH del servidor pisa la columna. Para no borrar local, leé el contacto y reenviá todas las claves.
Un id que no es de tu cuenta responde 404 CONTACT_NOT_FOUND.
Los chats de esa persona salen de GET /api/v2/contacts/{id}/conversations, con status en open o closed, limit hasta 100 y cursor. Cada ítem trae serviceWindowExpiresAt: si esa fecha ya pasó, un texto libre no sale.
Scopes, 403 y el tope de pedidos
Los scopes de una clave, en el servidor, son: messages:read, messages:write, contacts:read, contacts:write, conversations:read, conversations:write, templates:read, channels:read, account:read, media:read, media:write, webhooks:read, webhooks:write y social:read. La lista está en scopes. media:write figura, y subir un archivo por POST /api/v2/media no está expuesto: el límite está en limitaciones.
Una clave sin scopes entra a todo. Una clave con scopes, si le falta el que pide la ruta, recibe 403 FORBIDDEN y el mensaje API key missing required scope: contacts:write. La página de scopes muestra un código INSUFFICIENT_SCOPE; el requireScope del API responde FORBIDDEN.
El tope de la API v2 es 120 pedidos por minuto por cuenta, no por clave. Al pasarlo, 429 RATE_LIMITED, con Retry-After.
Mandar el mensaje es otro pedido
Si el número no existía y hacés POST /api/v2/messages, el envío crea el contacto pelado (teléfono, sin nombre ni etiquetas). Por eso conviene cargar la ficha antes, o hacer el PATCH después del 409.
Un texto, con la ventana de 24 horas abierta:
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: laura-turno-2026-10-19" \
-d '{
"phoneNumberId": "UUID-DEL-CANAL",
"to": "+5491140001122",
"type": "text",
"text": { "body": "Laura, te quedó listo el arreglo. Pasá cuando quieras, de 10 a 18." }
}'
phoneNumberId es el id de GET /api/v2/channels, no el id de teléfono de Meta. Hace falta messages:write. Si la ventana está cerrada, un texto responde 403 SERVICE_WINDOW_EXPIRED y hay que usar una plantilla. Fotos y PDF están en enviar media.
Ese texto, si se entrega, es un mensaje de servicio. Desde el 1 de octubre de 2026 cada número tiene 1.000 por mes sin cargo y, desde el 1.001, en Argentina Meta cobra alrededor de ARS 37,68 (~US$0,026). Crear el contacto sale cero. El precio está en cuánto cuesta la API y en la rate card de Meta.
Cloud API, Twilio, 360dialog y Z-API
| Dónde | Dónde vive la ficha del cliente |
|---|---|
| Cloud API de Meta | No hay /contacts. Escribís a to. Referencia |
| Waspy | /api/v2/contacts, y el mismo número se ve en la bandeja del equipo |
| Twilio | Otra API, con su propio precio encima de Meta. Waspy vs Twilio |
| 360dialog | Proxy de la Cloud API: el contacto no es un recurso de CRM. Waspy vs 360dialog |
| Z-API | No es la API oficial. Waspy vs Z-API |
Un asistente (Claude, Cursor, Codex, Grok) puede buscar estos contactos por el MCP, con los mismos scopes, si le das una clave de solo lectura. Esa conexión está en Claude, ChatGPT y Cursor. El mensaje al cliente lo confirma una persona, salvo un aviso que tu sistema manda a propósito con plantilla.
Para probar la clave, el alta está en Configuración → Claves de API. Diez días de prueba, sin tarjeta, en waspytech.com.
