Para desarrolladores
API de ViaWA
Enviá y programá mensajes de WhatsApp desde tu propio sistema (un CRM, una agenda de turnos, un e-commerce, un script), sin entrar a la app. Una API REST con JSON y una clave por sistema.
Qué es y qué no es
- Los mensajes salen desde el WhatsApp que vinculaste en tu cuenta de ViaWA, no desde un número de empresa.
- Es la misma cola que usa la app: un mensaje por API se programa como cualquiera, así que tiene los mismos límites y protecciones (tope diario, período de calentamiento de las cuentas nuevas, detección de envíos masivos y chequeo de que el número exista en WhatsApp).
- No es la API oficial de WhatsApp Business ni un producto de Meta. Usa el mismo protocolo que WhatsApp Web, por lo que rige lo que explicamos en Seguridad y datos sobre el riesgo de uso.
- Necesitás tu WhatsApp vinculado y una suscripción activa (o acceso por invitación).
Empezar en 3 pasos
- Entrá a la app, andá a Configuración → Acceso por API y creá una clave con el nombre del sistema que la va a usar.
- Copiala en ese momento: se muestra una sola vez. Nosotros solo guardamos un hash, no podemos recuperarla.
- Mandá tu primer mensaje con el ejemplo de abajo.
Autenticación
Todos los pedidos van a https://viawa.onrender.com y llevan la clave en el header
Authorization. Las claves empiezan con
viawa_sk_. La sesión de la app no sirve en la API, ni al revés.
Authorization: Bearer viawa_sk_TU_CLAVE Enviar o programar un mensaje
POST /v1/messages
| Campo | Tipo | Descripción |
|---|---|---|
to | string | Número del destinatario, con código de país (+54 9 11 5145-6236; espacios y guiones se ignoran). Sin “+”, se interpreta como número argentino. Si todavía no es un contacto tuyo, se agrega. Enviá to o contactId, no los dos. |
contactId | string | Id de un contacto existente (lo obtenés de GET /v1/contacts). |
firstName, lastName | string, opcional | Solo con to cuando el número es nuevo: nombre con el que se crea el contacto. |
text | string, obligatorio | El mensaje, de 1 a 1000 caracteres. Acepta {{nombre}} y {{apellido}}, y las variables personalizadas que creaste en la app (por ejemplo {{turno}}): se reemplazan con los datos del contacto. Una variable que no existe da 400; una que existe pero el contacto no tiene cargada se omite del texto. |
sendAt | string ISO 8601, opcional | Cuándo enviarlo, con zona horaria (2026-10-01T09:30:00-03:00 o en UTC con Z). Tiene que ser futuro. Resolución de minuto: los segundos se descartan. Sin sendAt, se envía en el próximo minuto (puede demorar unos segundos más). |
confirmBulk | boolean, opcional | Confirma un envío masivo. Ver más abajo. |
Ejemplo con curl:
curl -X POST https://viawa.onrender.com/v1/messages \
-H "Authorization: Bearer viawa_sk_TU_CLAVE" \
-H "Content-Type: application/json" \
-d '{
"to": "+54 9 11 5145-6236",
"text": "Hola {{nombre}}, te esperamos mañana a las 10.",
"sendAt": "2026-10-01T09:30:00-03:00"
}'
Con JavaScript:
const res = await fetch("https://viawa.onrender.com/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VIAWA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+54 9 11 5145-6236",
text: "Tu pedido salió de camino.",
// sendAt es opcional: sin él, se envía en el próximo minuto.
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data.id, data.status); // "pending"
Con Python:
import os, requests
res = requests.post(
"https://viawa.onrender.com/v1/messages",
headers={"Authorization": f"Bearer {os.environ['VIAWA_API_KEY']}"},
json={
"to": "+54 9 11 5145-6236",
"text": "Tu turno es mañana a las 10.",
"sendAt": "2026-10-01T09:30:00-03:00",
},
timeout=15,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data["id"], data["status"]) # pending
Respuesta 201 (el objeto mensaje):
{
"id": "cmubvadd000018z7t5gaqnaq9",
"to": "5491151456236",
"contactId": "cmtz0kpve037jg92mkt4aoikq",
"status": "pending",
"sendAt": "2026-10-01T12:30:00.000Z",
"sentAt": null,
"attemptCount": 0,
"lastError": null,
"lastErrorAt": null,
"nextRetryAt": null,
"createdAt": "2026-09-21T23:19:20.100Z"
}
Consultar el estado de un mensaje
GET /v1/messages/:id devuelve el mismo objeto de arriba, actualizado.
curl https://viawa.onrender.com/v1/messages/ID_DEL_MENSAJE \
-H "Authorization: Bearer viawa_sk_TU_CLAVE" | status | Significado |
|---|---|
pending | Programado, todavía no salió (o está reintentando). |
sent | Enviado. sentAt tiene el momento. |
failed | No se pudo enviar. lastError tiene el motivo. |
- Si un envío falla por un error de WhatsApp, se reintenta hasta 3 veces con 10 minutos entre intentos;
attemptCountynextRetryAtlo reflejan. Si el número no está en WhatsApp, falla directamente. - Si tu WhatsApp está desconectado en ese momento, el mensaje espera como
pendingy sale cuando vuelva la conexión. - Consultá el estado con calma: con el ritmo de la cola no hace falta más de una vez cada varios segundos.
GET /v1/messages?status=pending|sent|failed&limit=25&cursor=… lista mensajes por estado (por defecto pending; hasta 100 por página). La respuesta trae items y nextCursor (null si no hay más): pasalo como cursor para la página siguiente.
curl "https://viawa.onrender.com/v1/messages?status=failed&limit=25" \
-H "Authorization: Bearer viawa_sk_TU_CLAVE" Contactos
GET /v1/contacts?search=…lista tus contactos;searchfiltra por nombre, apellido o teléfono. Cada uno traeid,firstName,lastName,fullName,description,phone(solo dígitos, con código de país),formattedPhone,createdAtyvariables(los valores de tus variables personalizadas para ese contacto, por ejemplo{ "turno": "martes 10hs" }). Los valores se cargan desde la app, en cada contacto.POST /v1/contactscrea uno:phoneyfirstNameobligatorios;lastNameydescriptionopcionales. Devuelve201, o409 contact_existssi el número ya está.
# Listar / buscar
curl "https://viawa.onrender.com/v1/contacts?search=fuschino" \
-H "Authorization: Bearer viawa_sk_TU_CLAVE"
# Crear
curl -X POST https://viawa.onrender.com/v1/contacts \
-H "Authorization: Bearer viawa_sk_TU_CLAVE" \
-H "Content-Type: application/json" \
-d '{ "phone": "+54 9 11 2222-3333", "firstName": "Ana", "lastName": "Ruiz" }' Límites
- 60 pedidos por minuto por clave. Al pasarte, la API responde
429con el headerRetry-After(segundos). Cada clave tiene su propio cupo. - Hasta 5 claves activas por cuenta.
- Los límites de la cuenta (tope diario de mensajes y calentamiento de las cuentas nuevas) valen igual que en la app. Un mensaje que los excede se reprograma en vez de enviarse de golpe.
Envíos masivos
Si mandás el mismo mensaje a 11 contactos distintos o más en menos de una hora, ese patrón se parece al de un
envío masivo y aumenta el riesgo para tu cuenta de WhatsApp. Para protegerte, la API responde
409 broadcast_confirm_required hasta que confirmes explícitamente que sabés lo que hacés,
repitiendo el pedido con "confirmBulk": true. Si lo que necesitás es una difusión, la app
tiene un flujo pensado para eso (ritmo escalonado y vista previa).
curl -X POST https://viawa.onrender.com/v1/messages \
-H "Authorization: Bearer viawa_sk_TU_CLAVE" \
-H "Content-Type: application/json" \
-d '{ "to": "+5491122223333", "text": "Aviso importante", "confirmBulk": true }' Errores
Todos los errores tienen la misma forma y un código estable para que lo trates en tu programa:
{
"error": {
"code": "bad_request",
"message": "La fecha y hora de envío deben ser futuras."
}
} | HTTP | code | Cuándo |
|---|---|---|
| 400 | bad_request | Datos inválidos: número mal formado, sendAt que no es ISO 8601 o está en el pasado, texto vacío o de más de 1000 caracteres, o una variable {{…}} que no existe. |
| 401 | invalid_api_key | Falta la clave, es inválida o fue revocada. No se distingue cuál de los tres para no dar pistas. |
| 402 | subscription_required | La cuenta no tiene una suscripción activa. |
| 403 | legal_acceptance_required | Hay una versión nueva de los Términos o de la Política de Privacidad y todavía no la aceptaste. Entrá a la app y aceptala: la API vuelve a funcionar al instante. |
| 404 | not_found | El mensaje no existe o no es de tu cuenta. |
| 409 | not_on_whatsapp | El número no está registrado en WhatsApp. |
| 409 | broadcast_confirm_required | Es el mismo mensaje a 11 contactos distintos o más en menos de 1 hora. Ver “Envíos masivos”. |
| 409 | contact_exists | Al crear un contacto: ya existe uno con ese número (el id viene en details.id). |
| 429 | rate_limited | Superaste los 60 pedidos por minuto de esa clave. El header Retry-After indica cuántos segundos esperar. |
| 500 | internal | Error nuestro. Reintentá; si persiste, escribinos. |
Buenas prácticas
- Guardá la clave como un secreto: en una variable de entorno o un gestor de secretos, nunca en el código ni en el navegador de tus clientes. Quien la tenga puede enviar mensajes desde tu cuenta.
- Una clave por sistema, con un nombre que lo identifique. Así, si hay que cortar el acceso de uno, no afecta a los demás.
- Revocala ante cualquier sospecha desde Configuración: deja de funcionar al instante y los mensajes ya programados no se cancelan.
- Llamá a la API desde tu servidor, no desde el frontend de una web pública.
- Tratá los códigos de error, no el texto del mensaje: el texto puede mejorar con el tiempo, el
codeno cambia. - Mandá solo a personas que esperan tu mensaje: es lo que más cuida a tu número de WhatsApp.
¿Algo no funciona o falta?
Escribinos y lo vemos. Contá qué endpoint usaste y el code del error (nunca nos mandes tu clave).