Saltar al contenido

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

  1. 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.
  2. Copiala en ese momento: se muestra una sola vez. Nosotros solo guardamos un hash, no podemos recuperarla.
  3. 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

CampoTipoDescripción
tostringNú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.
contactIdstringId de un contacto existente (lo obtenés de GET /v1/contacts).
firstName, lastNamestring, opcionalSolo con to cuando el número es nuevo: nombre con el que se crea el contacto.
textstring, obligatorioEl 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.
sendAtstring ISO 8601, opcionalCuá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).
confirmBulkboolean, opcionalConfirma 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"
statusSignificado
pendingProgramado, todavía no salió (o está reintentando).
sentEnviado. sentAt tiene el momento.
failedNo 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; attemptCount y nextRetryAt lo reflejan. Si el número no está en WhatsApp, falla directamente.
  • Si tu WhatsApp está desconectado en ese momento, el mensaje espera como pending y 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; search filtra por nombre, apellido o teléfono. Cada uno trae id, firstName, lastName, fullName, description, phone (solo dígitos, con código de país), formattedPhone, createdAt y variables (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/contacts crea uno: phone y firstName obligatorios; lastName y description opcionales. Devuelve 201, o 409 contact_exists si 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 429 con el header Retry-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."
  }
}
HTTPcodeCuándo
400bad_requestDatos 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.
401invalid_api_keyFalta la clave, es inválida o fue revocada. No se distingue cuál de los tres para no dar pistas.
402subscription_requiredLa cuenta no tiene una suscripción activa.
403legal_acceptance_requiredHay 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.
404not_foundEl mensaje no existe o no es de tu cuenta.
409not_on_whatsappEl número no está registrado en WhatsApp.
409broadcast_confirm_requiredEs el mismo mensaje a 11 contactos distintos o más en menos de 1 hora. Ver “Envíos masivos”.
409contact_existsAl crear un contacto: ya existe uno con ese número (el id viene en details.id).
429rate_limitedSuperaste los 60 pedidos por minuto de esa clave. El header Retry-After indica cuántos segundos esperar.
500internalError 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 code no 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).