📖 Visión general
CloudWapp es una API multi-tenant para WhatsApp construida sobre Baileys. Cada cliente (tenant) tiene su propia API Key y puede gestionar múltiples sesiones de WhatsApp en paralelo.
Lo que podés hacer:
- Conectar números de WhatsApp via QR escaneado o pair code
- Enviar mensajes de texto, imagen, video, audio, documento, ubicación, contactos
- Recibir mensajes entrantes via WebSocket en tiempo real o webhook HTTP
- Consultar historial con filtros (entrantes / salientes / búsqueda)
- Recibir confirmaciones de entrega y lectura (✓✓ DELIVERED / READ)
🌐 Base URL
Producción:
Todas las rutas de API empiezan con /api/v1. Las excepciones son /health, /metrics y los archivos estáticos del dashboard.
🚀 Quick Start
Flujo completo desde cero, asumiendo que ya tenés tu API Key (ver cómo obtenerla):
👤 Crear cuenta (registro público)
/api/v1/auth/registerCualquiera puede crearse una cuenta. La nueva cuenta arranca con plan=FREE y roles=[USER].
También podés crearte desde el formulario web.
🔑 Login web (JWT)
/api/v1/auth/loginPara acceder al dashboard. Devuelve un JWT que también se setea como cookie cw_session (httpOnly).
/api/v1/auth/logoutLimpia la cookie cw_session.
/api/v1/auth/meDevuelve el usuario actual del JWT/cookie. Útil para hidratar la UI tras un refresh.
🗝️ Obtener tu API Key
Cada usuario tiene una API Key única generada automáticamente al crear la cuenta. Es lo que vas a usar para todas las llamadas programáticas (curl, n8n, scripts).
¿Dónde la veo?
- Después de registrarte: en el response del
/register(campouser.apiKey) - En el dashboard: Login → menú Configuración → "Tu API Key" + botón Copiar
- Vía API:
GET /api/v1/users/me(autenticando con la cookie / JWT del login web)
¿Cómo se usa?
En cada request, agregá el header:
Métodos de autenticación equivalentes
La mayoría de endpoints aceptan los 3:
| Método | Header | Para qué |
|---|---|---|
| API Key | x-api-key: <uuid> | Scripts, n8n, curl, integraciones server-to-server |
| JWT Bearer | Authorization: Bearer <jwt> | Apps que ya hicieron login con email+password |
| Session Cookie | Cookie: cw_session=<jwt> | El browser lo manda solo después del login web |
POST /api/v1/users/<tuId>/regenerate-key.
🎭 Roles y Planes
Son conceptos independientes:
| Concepto | Valores | Decide |
|---|---|---|
plan | FREE · PREMIUM · INTERNAL_UNLIMITED | Cuotas de mensajes mensuales |
roles (array) | USER · ADMIN | Acceso a paneles UI |
USER→ puede usar/dashboard.html(sus propias sesiones, mensajes, dominios, billing)ADMIN→ puede usar/admin.html(todos los users, todas las sesiones, queue, health)- Un usuario puede tener ambos roles simultáneamente
Cuotas por plan
| Plan | Mensajes/mes | Cómo obtenerlo |
|---|---|---|
FREE | 100 | Default al registrarse |
PREMIUM | 10.000 | Stripe Checkout (ver Billing) |
INTERNAL_UNLIMITED | Sin límite | Solo asignable por admin |
📱 Conectar sesión de WhatsApp
/api/v1/session/connectInicia una nueva sesión. Podés crear N sesiones en paralelo (cada una representa un número de WhatsApp distinto).
📷 Obtener QR (HTTP polling)
/api/v1/session/{accountId}/qrDevuelve el QR cacheado como string crudo + PNG inline. Polleá cada 2-3 segundos mientras la sesión esté en estado CONNECTING.
/api/v1/session/{accountId}/qr.pngVariante: devuelve directo la imagen PNG. Útil para <img src>, abrir desde curl, o pipe.
🔑 Vincular por código (sin QR)
/api/v1/session/{accountId}/pairAlternativa al QR cuando no podés escanear (ej. WhatsApp en el mismo dispositivo). El usuario escribe un código de 8 caracteres en su WhatsApp.
El servidor espera hasta 10s a que la sesión esté lista antes de pedir el código (por si la llamás muy rápido tras connect).
📊 Estado de sesión
/api/v1/session/{accountId}/status📋 Listar mis sesiones
/api/v1/session/mine🔌 Desconectar / Eliminar sesión
/api/v1/session/{accountId}Desconecta el socket pero deja la cuenta en DB. Idempotente: si la cuenta ya no existe devuelve { status: "already_gone" }.
/api/v1/session/{accountId}/forceHard-delete: borra cuenta + auth state + history de mensajes en cascada. Útil cuando una sesión queda atascada y el disconnect normal no alcanza.
🔌 WebSocket — push tiempo real
Alternativa al polling para clientes que necesitan latencia mínima. Namespace: /ws · Autenticación: ?apiKey=TU_KEY
| Evento | Dirección | Descripción |
|---|---|---|
session:join | → Server | Unirse a la sala de una sesión: { accountId } |
session:joined | ← Server | Confirma join + manda status actual |
session:qr | ← Server | QR string crudo cuando se genera/rota |
session:status | ← Server | { status, phone, name } — status: connected · disconnected · logged_out · failed |
message:received | ← Server | Mensaje entrante: { from, text, messageId, timestamp } |
session:leave | → Server | Dejar de recibir eventos para una sesión |
✉️ Enviar mensaje
/api/v1/messages/sendEl mensaje se encola y se envía asincrónicamente (delay aleatorio <400ms anti-ban). Te devolvemos un messageId para trackear su estado.
🎨 Tipos de mensaje
📨 Envío masivo Premium+
/api/v1/messages/bulk-send📜 Historial con filtros
/api/v1/messages/history/{accountId}Lista los mensajes entrantes y salientes de una sesión.
MESSAGE_RETENTION_DAYS). Si necesitás archivar, exportá vía API y guardá en tu propio storage.
🚦 Estados de mensaje
Los mensajes salientes pasan por este flujo:
Los entrantes arrancan directamente en RECEIVED.
🪝 Configurar webhook
Configurá una URL de tu lado para recibir push HTTP por cada evento (alternativa al WebSocket, pero persistente — si tu servidor está caído, BullMQ reintenta 5 veces con backoff exponencial).
/api/v1/users/{tuId}El campo webhookSecret nunca aparece en respuestas (te confirmamos con hasWebhookSecret: true).
📡 Eventos disponibles
| Evento | Cuándo se dispara | Payload |
|---|---|---|
session.qr | QR listo para escanear | {accountId, reference, qr} |
session.connected | Sesión completó pareo | {accountId, reference, phone, name} |
session.disconnected | Sesión cerrada / desvinculada | {accountId, reference, reason} |
message.received | Mensaje entrante | {accountId, reference, from, text, waMessageId, timestamp} |
message.sent | Mensaje enviado a WhatsApp | {accountId, messageLogId, waMessageId} |
message.delivered | Confirmación ✓✓ gris | {accountId, waMessageId} |
message.read | Confirmación ✓✓ azul | {accountId, waMessageId} |
message.failed | Tras 3 reintentos fallidos | {accountId, messageLogId, error} |
🔏 Firma HMAC
Si configuraste webhookSecret, cada request incluye los headers:
Verificá la firma en tu endpoint para evitar requests fraudulentos. Ejemplo Node.js:
🤝 WhatsApp para tu app (multi-cliente)
Si tu aplicación necesita darle WhatsApp a cada uno de tus propios clientes
(una sesión/número por cliente), CloudWapp lo resuelve con UNA sola API key: creás N sesiones,
cada una etiquetada con tu id de cliente vía el campo reference,
y recibís todo por webhook ya identificado. No necesitás mantener un mapa accountId ↔ tu cliente.
Flujo por cada cliente tuyo
Listá las sesiones de un cliente puntual con GET /api/v1/session/mine?reference=cliente-42.
Por qué webhooks y no WebSocket
Tu app es un backend — abrir un WebSocket por cada sesión no escala. Los webhooks
cubren TODO el ciclo de vida (session.qr, session.connected,
session.disconnected, message.received) con reintentos automáticos
y firma HMAC. Configurá tu webhookUrl una vez (en tu perfil) y recibís los eventos
de las N sesiones en ese único endpoint, demultiplexando por reference.
Límites
Cada sesión de WhatsApp mantiene una conexión viva y consume ~150-300 MB de RAM. Hay un tope
configurable de sesiones por cuenta (whatsapp_max_sessions_per_user, default 25 —
el admin lo sube según la RAM del VPS). Para cientos de clientes, escalá la RAM del servidor
o repartí en varios nodos (CloudWapp ya soporta ownership distribuido de sesiones).
🌐 Custom Hostnames as a Service
CloudWapp funciona como un Cloudflare-SaaS-style proxy: tus clientes apuntan su propio
dominio (app.cliente.com o cliente.com raíz) vía CNAME a CloudWapp, y nosotros
emitimos SSL automático con Let's Encrypt y reverse-proxy a tu backend con los headers que vos definas
(multi-tenant identification, auth, etc).
Quickstart en 4 pasos
- Registrá el hostname con tu API key (devuelve
verificationToken+ instrucciones DNS) - Pediles a tu cliente que configure en su DNS (Cloudflare, Hostinger, GoDaddy…):
- Un
TXTen_cloudwapp-verify.<dominio>con el valor del token - Un
CNAMEapuntando aproxy.cloudwapp.io(o el anchor que tengas configurado), SIN proxy de Cloudflare (DNS-only / nube gris)
- Un
- CloudWapp verifica automáticamente cada 5 minutos (o forzá la verificación con un POST a
/verify) - Listo — al primer hit Caddy emite el cert vía on-demand TLS y empieza a proxiar el tráfico
Registrar hostname
/api/v1/domainsRespuesta:
Listar / detalle / forzar verificación
/api/v1/domains/mine/api/v1/domains/mine/:id/api/v1/domains/mine/:id/verify — corre la verificación AHORA en vez de esperar al cron/api/v1/domains/mine/:id/api/v1/domains/mine/:idEl patrón "SaaS-for-SaaS" (caso típico)
Tu SaaS está en saas.com y cada cliente tuyo tiene una página interna en
saas.com/web/cliente1, saas.com/web/cliente2, etc. Querés que tus clientes
puedan acceder a esa página desde:
- Un subdominio tuyo:
cliente1.saas.com - Su propio dominio:
cliente1.comoapp.cliente1.com - O ambos a la vez, sirviendo el mismo contenido
Setup del SaaS (una sola vez)
- En el DNS de
saas.comagregar:*.saas.comCNAME →proxy.cloudwapp.io(DNS-only) - Mantener
saas.comapuntando A → tu backend (NO pasa por CloudWapp)
Por cada cliente nuevo (desde tu backend, por API)
La respuesta del segundo (cliente1.com) viene con dnsInstructions para que tu cliente configure su DNS.
El primero (cliente1.saas.com) se verifica solo porque vos ya tenés el TXT preparado en
el wildcard *.saas.com.
El path del targetUrl se PRESERVA
Si targetUrl=https://saas.com/web/cliente1 y alguien visita
cliente1.saas.com/dashboard?tab=stats, CloudWapp hace fetch a:
Headers que CloudWapp inyecta a tu backend
Aparte de los injectHeaders que vos configures, CloudWapp agrega siempre estos:
3 formas de identificar el tenant en tu backend
Podés combinarlas. Elegí la que mejor encaje con tu arquitectura:
A) Por path del URL — la más simple
B) Por header inyectado — la más limpia
C) Por Host original — útil si ya tenés multi-tenant por subdomain
SSL automático + verificación
Caddy aprovisiona el certificado de Let's Encrypt la primera vez que alguien visita el hostname — cero config manual. El flujo interno:
📈 Analytics as a Service
Embed un pixel JS en sitios web de tus clientes y recibe analytics privacy-friendly:
pageviews, visitantes únicos, top pages, dispositivos, países. Sin cookies (no GDPR banner),
sin ralentizar la web del cliente (script async + sendBeacon).
Pricing: 30 días de trial gratis por site, después suscripción Stripe (precio editable desde admin).
Quickstart
POST /api/v1/analytics/sitescon{ name, domain }→ te devuelvetrackingId+snippet- Pegá el snippet en el <head> del HTML del sitio del cliente
- Empezás a ver datos en el dashboard (vista 📈 Analytics)
- Al vencer el trial → activá la suscripción con
POST /api/v1/billing/analytics/checkout
Custom events desde JS del cliente
El pixel expone una API global window.cwTrack(eventName, props) para trackear
clicks, conversions, signups, etc.:
Endpoints
/api/v1/analytics/sites — crear site (arranca con trial)/api/v1/analytics/sites — listar mis sites/api/v1/analytics/sites/:id/stats?range=24h|7d|30d/api/v1/analytics/sites/:id/api/v1/billing/analytics/checkout — activar plan post-trialPrivacy
El pixel NO usa cookies. Identifica visitantes únicos con sha256(ip + user-agent + siteId + salt-rotativo-diario) —
el salt cambia cada día, así que NO se pueden cross-trackear visitantes a lo largo del tiempo.
Cumple GDPR sin necesidad de banner de consentimiento.
🚀 Apps as a Service (PaaS)
Deploya aplicaciones desde un repo git — como Railway o Render, pero en tu propia nube CloudWapp.
CloudWapp clona el repo, lo buildea (con tu Dockerfile o autodetección via
nixpacks para Node/Python/Go/PHP/Rust…),
lo corre como container con límites de CPU/RAM, y lo expone en
<slug>.apps.tudominio.com con SSL automático.
Pricing: 30 días de trial gratis por app, después suscripción (precio editable desde admin).
Quickstart
La forma más fácil: dashboard → 🚀 Apps → + Nueva app →
Connect GitHub → elegís el repo de un dropdown → Crear app.
Cada git push redeploya al instante (webhook). Sin pegar URLs ni tokens.
- Connect GitHub (1 vez): autorizás y elegís qué repos compartir
- Elegís el repo → nombre y branch se autocompletan → Crear app
- Primer build automático (~2-5 min según el stack)
- Listo: tu app queda en
https://<slug>.apps.tudominio.comcon SSL - Cada
git push→ redeploy instantáneo
¿Preferís API o un repo de otro proveedor? También podés crear la app con la URL del repo directo (y un PAT para privados):
Importante: tu app debe escuchar en el puerto declarado y en 0.0.0.0
(no localhost). CloudWapp inyecta PORT como variable de entorno.
CI/CD automático
Con autoDeploy=true, CloudWapp chequea tu repo cada minuto (git ls-remote,
sin clonar). Si el SHA del branch cambió → rebuild + redeploy. Si el build falla, el container
anterior sigue corriendo — no hay downtime por un push roto. El error queda
en el historial de builds con el log completo.
Límites y planes
| Recurso | Default (editable desde admin) |
|---|---|
| RAM por app | 512 MB (hard limit) |
| CPU por app | 0.5 vCPU |
| Apps por usuario | 2 |
| Timeout de build | 15 min |
| Procesos (PIDs) | 256 |
Las apps corren en una red Docker aislada — sin acceso a la base de datos ni al Redis del
sistema CloudWapp. Para persistencia usá una DB externa (Neon, Supabase, PlanetScale…)
configurada via envVars.
Endpoints
/api/v1/apps — crear app/api/v1/apps — listar mis apps/api/v1/apps/:id/deploy — deploy ahora/api/v1/apps/:id/restart · /stop/api/v1/apps/:id/logs?tail=200 — logs runtime/api/v1/apps/:id/deployments — historial de builds/api/v1/apps/:id/deployments/:depId/logs — log de un build/api/v1/billing/apps/checkout — activar plan post-trial📧 Correo básico
Buzones IMAP/SMTP reales en el dominio de tu cliente ([email protected]),
servidos desde CloudWapp. Cada buzón incluye antispam (SPF/DKIM/DMARC), fail2ban contra
brute-force, y cuota configurable. Trial 30 días, después suscripción por buzón.
Quickstart
POST /api/v1/mail/mailboxescon{ address, password }(o vista 📧 Correo del dashboard)- Configurá los 4 registros DNS que devuelve la respuesta (MX, SPF, DKIM, DMARC)
- El MX se verifica solo cada 10 min (o forzá con
POST /:id/verify-mx) - Conectá tu cliente de correo con la config IMAP/SMTP que te da el panel
Deliverability (que tus correos NO caigan en spam)
- Configurá los 4 registros DNS — sin SPF + DKIM + DMARC, Gmail/Outlook te mandan a spam directo
- rDNS (PTR): el admin debe configurar el PTR de la IP del VPS →
mail.<dominio>en el panel del provider - Relay (recomendado): si el provider del VPS bloquea el puerto 25 saliente (Contabo lo hace por default),
el admin configura
MAIL_RELAY_*en el .env con credenciales de Amazon SES (~USD 0.10/1000 emails) o Resend. El envío sale por el relay con reputación impecable; la recepción y los buzones siguen en CloudWapp
Endpoints
/api/v1/mail/mailboxes — crear buzón/api/v1/mail/mailboxes — listar/api/v1/mail/mailboxes/:id — detalle (DNS + config cliente)/api/v1/mail/mailboxes/:id/password — cambiar password/api/v1/mail/mailboxes/:id/verify-mx — verificar MX ahora/api/v1/mail/mailboxes/:id — eliminar (borra correos)/api/v1/billing/mail/checkout — activar plan post-trial⌨️ CLI
Todo lo anterior también desde la terminal — ideal para developers y pipelines de CI.
Para CI sin TTY: exportá CLOUDWAPP_URL y CLOUDWAPP_API_KEY
como env vars en lugar de usar login.
💳 Stripe Checkout
/api/v1/billing/checkout📊 Estado de plan
/api/v1/billing/status👥 Gestión de usuarios Rol ADMIN
| Método | Endpoint | Descripción |
|---|---|---|
| POST | /api/v1/users | Crear usuario (acepta roles y password opcionales) |
| GET | /api/v1/users/me | Mi perfil (cualquier rol) |
| GET | /api/v1/users | Listar todos (paginado) |
| GET | /api/v1/users/{id} | Ver detalle |
| PATCH | /api/v1/users/{id} | Actualizar (email, name, plan, roles, isActive, password, webhookUrl, webhookSecret) |
| POST | /api/v1/users/{id}/regenerate-key | Generar nueva API Key |
| DEL | /api/v1/users/{id} | Eliminar (cascada de sesiones/mensajes) |
📊 Estadísticas de cola Rol ADMIN
/api/v1/messages/queue/stats❤️ Health Check
/health (sin prefix)📊 Métricas Prometheus
/metricsEndpoint público en formato Prometheus. Métricas custom: cw_http_requests_total, cw_sessions_active, cw_messages_sent_total, cw_messages_received_total, cw_webhooks_delivered_total, etc.
⚠️ Códigos de Error
| Status | Significado | Cómo resolverlo |
|---|---|---|
400 | Bad Request — body inválido o DTO mal formado | Revisá el campo "message" del error |
401 | API Key inválida, ausente, JWT expirado | Revisá tu header / cookie / token |
403 | Plan o rol insuficiente (ej. ADMIN required) | Tu cuenta no tiene ese permiso |
404 | Recurso no encontrado | Verificá el ID |
409 | Conflicto (email/dominio duplicado, sesión owned por otro nodo) | Retry o usá distinto identificador |
422 | Validación de class-validator falló | Mirá "message" array en el response |
429 | Rate limit (10/seg, 120/min, 5k/hora por user) | Esperá unos segundos |
500 | Error interno | Reportalo |
📖 Swagger UI interactivo
Explorá la API ejecutando requests desde el browser en /docs (Swagger autogenerado).