📖 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:

🌐 Base URL

Producción:

https://api-production-bb7c.up.railway.app

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):

# 1. Iniciar una sesión de WhatsApp curl -X POST "$URL/api/v1/session/connect" \ -H "x-api-key: $KEY" \ -H "Content-Type: application/json" \ -d '{}' # → { "accountId": "uuid-...", "status": "connecting" } # 2. Pollear el QR cada 2s hasta que aparezca ACC="uuid-..." curl -H "x-api-key: $KEY" "$URL/api/v1/session/$ACC/qr.png" > qr.png # 3. Escanear con WhatsApp (Configuracion → Dispositivos vinculados) # 4. Pollear status hasta connected=true curl -H "x-api-key: $KEY" "$URL/api/v1/session/$ACC/status" # → { "status": "CONNECTED", "phone": "...", "connected": true } # 5. Enviar mensaje curl -X POST "$URL/api/v1/messages/send" \ -H "x-api-key: $KEY" \ -H "Content-Type: application/json" \ -d '{"accountId":"'$ACC'","to":"5491155554444","content":{"text":"Hola"}}'

👤 Crear cuenta (registro público)

POST/api/v1/auth/register

Cualquiera puede crearse una cuenta. La nueva cuenta arranca con plan=FREE y roles=[USER].

// Request { "email": "[email protected]", "name": "Tu Nombre", "password": "al-menos-6-chars" } // Response 201 { "accessToken": "eyJhbG...", "tokenType": "Bearer", "expiresIn": 3600, "user": { "id": "uuid", "email": "[email protected]", "plan": "FREE", "roles": ["USER"], "apiKey": "abcd-1234-..." // ← guardala, es tu credencial para la API } }

También podés crearte desde el formulario web.

🔑 Login web (JWT)

POST/api/v1/auth/login

Para acceder al dashboard. Devuelve un JWT que también se setea como cookie cw_session (httpOnly).

// Request { "email": "[email protected]", "password": "tu-pwd" } // Response 200 — mismo shape que /register { "accessToken": "...", "user": { ... } }
POST/api/v1/auth/logout

Limpia la cookie cw_session.

GET/api/v1/auth/me

Devuelve 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?

¿Cómo se usa?

En cada request, agregá el header:

x-api-key: tu-uuid-personal-aqui

Métodos de autenticación equivalentes

La mayoría de endpoints aceptan los 3:

MétodoHeaderPara qué
API Keyx-api-key: <uuid>Scripts, n8n, curl, integraciones server-to-server
JWT BearerAuthorization: Bearer <jwt>Apps que ya hicieron login con email+password
Session CookieCookie: cw_session=<jwt>El browser lo manda solo después del login web
⚠ Seguridad: tu API Key da acceso completo a tu cuenta. No la pongas en código del cliente (browser), no la subas a git, ni la compartas. Si se filtra, regenerala desde el dashboard: botón 🔑 al lado de tu user en el admin, o llamando a POST /api/v1/users/<tuId>/regenerate-key.

🎭 Roles y Planes

Son conceptos independientes:

ConceptoValoresDecide
planFREE · PREMIUM · INTERNAL_UNLIMITEDCuotas de mensajes mensuales
roles (array)USER · ADMINAcceso a paneles UI

Cuotas por plan

PlanMensajes/mesCómo obtenerlo
FREE100Default al registrarse
PREMIUM10.000Stripe Checkout (ver Billing)
INTERNAL_UNLIMITEDSin límiteSolo asignable por admin

📱 Conectar sesión de WhatsApp

POST/api/v1/session/connect

Inicia una nueva sesión. Podés crear N sesiones en paralelo (cada una representa un número de WhatsApp distinto).

// Request (todos opcionales) { "accountId": "uuid-opcional" } // Sin accountId, se genera un UUID nuevo. // Response 200 { "status": "connecting", "accountId": "uuid-...", "owner": "hostname-xxxxxxxx", // instancia que tiene el socket "websocket": { "instructions": "Connect to /ws ...", "events": ["session:qr", "session:status", "message:received"] } }
✓ Auto-reattach: si la sesión queda huérfana tras un deploy o crash del servidor, un cron interno la recupera automáticamente en ≤60 segundos. No necesitás reconnectar a mano.

📷 Obtener QR (HTTP polling)

GET/api/v1/session/{accountId}/qr

Devuelve el QR cacheado como string crudo + PNG inline. Polleá cada 2-3 segundos mientras la sesión esté en estado CONNECTING.

// Query params opcionales // format=raw|image|both (default: both) // 200 — QR disponible { "connected": false, "qr": "2@abc123...", // raw, para librerías QR "qrImage": "data:image/png;base64,...", // listo para <img src> "hint": "QR rotates every ~20s. Re-poll if user hasn't scanned." } // 200 — ya conectado { "connected": true, "phone": "549...", "name": "..." } // 200 — sesion arrancando, todavia sin QR { "qr": null, "message": "Session is starting..." } // 404 — no existe sesion activa
GET/api/v1/session/{accountId}/qr.png

Variante: devuelve directo la imagen PNG. Útil para <img src>, abrir desde curl, o pipe.

curl -H "x-api-key: $KEY" "$URL/api/v1/session/$ACC/qr.png" > qr.png && open qr.png

🔑 Vincular por código (sin QR)

POST/api/v1/session/{accountId}/pair

Alternativa 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.

// Request { "phoneNumber": "5491155554444" } // E.164 sin "+" ni espacios // Response 200 { "code": "ABCD-1234", "rawCode": "ABCD1234", "expiresIn": 60, "hint": "Open WhatsApp → Settings → Linked Devices → Link with phone number" }

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

GET/api/v1/session/{accountId}/status
{ "accountId": "...", "status": "CONNECTED", // CONNECTING | CONNECTED | DISCONNECTED "phone": "5491155554444", "name": "Mi Numero", "connected": true, // el socket esta vivo "sessionExists": true, "hasQr": false, "ownedByThisNode": true, "lastUpdate": "2026-05-18T..." }

📋 Listar mis sesiones

GET/api/v1/session/mine
{ "count": 2, "data": [ { "id": "...", "status": "CONNECTED", "phone": "...", "name": "..." }, { "id": "...", "status": "DISCONNECTED", "phone": null } ] }

🔌 Desconectar / Eliminar sesión

DEL/api/v1/session/{accountId}

Desconecta el socket pero deja la cuenta en DB. Idempotente: si la cuenta ya no existe devuelve { status: "already_gone" }.

DEL/api/v1/session/{accountId}/force

Hard-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

EventoDirecciónDescripción
session:join→ ServerUnirse a la sala de una sesión: { accountId }
session:joined← ServerConfirma join + manda status actual
session:qr← ServerQR string crudo cuando se genera/rota
session:status← Server{ status, phone, name } — status: connected · disconnected · logged_out · failed
message:received← ServerMensaje entrante: { from, text, messageId, timestamp }
session:leave→ ServerDejar de recibir eventos para una sesión
// Cliente: Socket.IO en browser const sock = io('/ws', { query: { apiKey: KEY } }); sock.on('connect', () => sock.emit('session:join', { accountId: ACC })); sock.on('session:qr', ({ qr }) => render(qr)); sock.on('session:status', ({ status, phone }) => log(status)); sock.on('message:received', (msg) => handle(msg));

✉️ Enviar mensaje

POST/api/v1/messages/send

El mensaje se encola y se envía asincrónicamente (delay aleatorio <400ms anti-ban). Te devolvemos un messageId para trackear su estado.

// Request { "accountId": "uuid-de-tu-sesion", "to": "5491155554444", // sin "+", solo digitos "type": "text", // default: text "content": { "text": "Hola mundo" } } // Response 202 — encolado { "messageId": "uuid", "status": "queued", "to": "[email protected]", "accountId": "..." }

🎨 Tipos de mensaje

// Imagen con caption { "type": "image", "content": { "url": "https://...", "caption": "Mi foto" } } // Video { "type": "video", "content": { "url": "https://...", "caption": "Demo" } } // Audio (incluye nota de voz si mimetype es audio/ogg) { "type": "audio", "content": { "url": "https://...", "mimetype": "audio/mp4" } } // Documento (PDF, Excel, etc) { "type": "document", "content": { "url": "https://...", "filename": "reporte.pdf", "mimetype": "application/pdf" } } // Ubicación { "type": "location", "content": { "latitude": -34.6037, "longitude": -58.3816, "name": "Obelisco BA" } } // Contacto { "type": "contact", "content": { "displayName": "Juan Perez", "contacts": [{ "vcard": "BEGIN:VCARD..." }] } }

📨 Envío masivo Premium+

POST/api/v1/messages/bulk-send
// Request { "accountId": "...", "recipients": ["54911...", "54911...", "54911..."], "type": "text", "content": { "text": "Promo!" } } // Response { "total": 3, "queued": 3, "failed": 0, "results": [{ "recipient": "...", "messageId": "..." }, ...] }

📜 Historial con filtros

GET/api/v1/messages/history/{accountId}

Lista los mensajes entrantes y salientes de una sesión.

// Query params (todos opcionales) // page=1 paginacion // limit=50 tope 200 // direction=INBOUND solo entrantes — omitir devuelve todos // direction=OUTBOUND solo salientes // search=549115 substring contra "to" y "fromJid" curl -H "x-api-key: $KEY" \ "$URL/api/v1/messages/history/$ACC?direction=INBOUND&limit=50" // Response { "data": [ { "id": "...", "direction": "INBOUND", "to": "[email protected]", "fromJid": "[email protected]", "waMessageId": "3EB0...", "type": "text", "status": "RECEIVED", "content": { "text": "hola", "raw": {...} }, "createdAt": "2026-05-18T..." } ], "meta": { "total": 42, "page": 1, "limit": 50, "totalPages": 1 } }
📅 Retención: los mensajes se borran automáticamente a los 7 días (configurable vía 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:

QUEUED → PROCESSING → SENT → DELIVERED → READ ↓ FAILED (si tras 3 retries con backoff sigue fallando)

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).

PATCH/api/v1/users/{tuId}
{ "webhookUrl": "https://tu-app.com/cloudwapp-events", "webhookSecret": "un-secret-largo-y-aleatorio" // opcional pero recomendado }

El campo webhookSecret nunca aparece en respuestas (te confirmamos con hasWebhookSecret: true).

📡 Eventos disponibles

EventoCuándo se disparaPayload
session.qrQR listo para escanear{accountId, reference, qr}
session.connectedSesión completó pareo{accountId, reference, phone, name}
session.disconnectedSesión cerrada / desvinculada{accountId, reference, reason}
message.receivedMensaje entrante{accountId, reference, from, text, waMessageId, timestamp}
message.sentMensaje enviado a WhatsApp{accountId, messageLogId, waMessageId}
message.deliveredConfirmación ✓✓ gris{accountId, waMessageId}
message.readConfirmación ✓✓ azul{accountId, waMessageId}
message.failedTras 3 reintentos fallidos{accountId, messageLogId, error}
// Body del webhook (siempre el mismo wrapper) { "event": "message.received", "userId": "...", "timestamp": "2026-05-18T22:00:00.000Z", "data": { ... } // payload especifico del evento }

🔏 Firma HMAC

Si configuraste webhookSecret, cada request incluye los headers:

X-CloudWapp-Event: message.received X-CloudWapp-Delivery: job-id-de-bullmq X-CloudWapp-Signature: sha256=a3f4b2... // HMAC-SHA256(body, secret) User-Agent: CloudWapp-Webhook/1.0

Verificá la firma en tu endpoint para evitar requests fraudulentos. Ejemplo Node.js:

const { createHmac, timingSafeEqual } = require('crypto'); app.post('/webhook', (req, res) => { const raw = req.rawBody; // el body sin parsear const expected = 'sha256=' + createHmac('sha256', SECRET).update(raw).digest('hex'); const got = req.headers['x-cloudwapp-signature']; if (!timingSafeEqual(Buffer.from(expected), Buffer.from(got))) { return res.status(401).end(); } // procesar el evento... res.status(200).end(); });

🤝 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

// 1) Crear una sesión para tu cliente, etiquetada con TU id POST /api/v1/session/connect { "reference": "cliente-42" } // → { accountId: "uuid-...", reference: "cliente-42", status: "connecting" } // Guardá el accountId, o simplemente usá tu reference en los webhooks. // 2) Mostrarle el QR a tu cliente (REST polling, sin WebSocket) GET /api/v1/session/{accountId}/qr // devuelve qr (string) + qrImage (PNG data URL) // …o esperá el webhook session.qr — llega { accountId, reference, qr } // 3) Cuando escanea, te llega por webhook: // session.connected → { accountId, reference: "cliente-42", phone, name } // 4) Enviar mensajes en nombre de ese cliente POST /api/v1/messages/send { "accountId": "uuid-...", "to": "+52155...", "content": { "text": "Hola!" } } // 5) Mensajes entrantes → webhook, ya identificado por tu reference: // message.received → { accountId, reference: "cliente-42", from, text, ... } // 6) Si tu cliente desvincula su WhatsApp, te enterás al instante: // session.disconnected → { accountId, reference: "cliente-42", reason: "logged_out" } // → mostrale el QR de nuevo (volvé al paso 1 con el mismo accountId)

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

  1. Registrá el hostname con tu API key (devuelve verificationToken + instrucciones DNS)
  2. Pediles a tu cliente que configure en su DNS (Cloudflare, Hostinger, GoDaddy…):
    • Un TXT en _cloudwapp-verify.<dominio> con el valor del token
    • Un CNAME apuntando a proxy.cloudwapp.io (o el anchor que tengas configurado), SIN proxy de Cloudflare (DNS-only / nube gris)
  3. CloudWapp verifica automáticamente cada 5 minutos (o forzá la verificación con un POST a /verify)
  4. Listo — al primer hit Caddy emite el cert vía on-demand TLS y empieza a proxiar el tráfico

Registrar hostname

POST/api/v1/domains
{ "domainName": "app.mi-cliente.com", "targetType": "URL", // URL = proxy a backend HTTP del cliente "targetUrl": "https://api.mi-cliente-saas.com", // a dónde proxiamos "injectHeaders": { // headers que inyectamos a CADA request "X-Tenant-Id": "abc-123", "Authorization": "Bearer s3cret-tenant-key" }, "preserveHost": false // true = forwardea el Host original al backend }

Respuesta:

{ "id": "uuid-...", "domainName": "app.mi-cliente.com", "verificationToken": "7c4f8a...", "verificationState": "PENDING", "isVerified": false, "dnsInstructions": { "step1_txt": { "type": "TXT", "host": "_cloudwapp-verify.app.mi-cliente.com", "value": "7c4f8a..." }, "step2_cname": { "type": "CNAME", "host": "app.mi-cliente.com", "value": "proxy.cloudwapp.io" } } }

Listar / detalle / forzar verificación

GET/api/v1/domains/mine
GET/api/v1/domains/mine/:id
POST/api/v1/domains/mine/:id/verify — corre la verificación AHORA en vez de esperar al cron
PATCH/api/v1/domains/mine/:id
DELETE/api/v1/domains/mine/:id

El 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:

Setup del SaaS (una sola vez)

  1. En el DNS de saas.com agregar: *.saas.com CNAME → proxy.cloudwapp.io (DNS-only)
  2. Mantener saas.com apuntando A → tu backend (NO pasa por CloudWapp)

Por cada cliente nuevo (desde tu backend, por API)

// Tu SaaS hace 1 ó 2 calls a CloudWapp cuando da de alta un cliente nuevo: // 1) Subdominio tuyo await fetch('https://api.cloudwapp.io/api/v1/domains', { method: 'POST', headers: { 'x-api-key': SAAS_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ domainName: 'cliente1.saas.com', targetType: 'URL', targetUrl: 'https://saas.com/web/cliente1', // path preservado! injectHeaders: { 'X-Tenant-Id': 'cliente1', 'X-Internal-Auth': 'secret-shared-token-cw-↔-saas', }, preserveHost: false, }), }); // 2) Y opcionalmente, el dominio propio del cliente (cuando lo soliciten) await fetch('https://api.cloudwapp.io/api/v1/domains', { method: 'POST', headers: { 'x-api-key': SAAS_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ domainName: 'app.cliente1.com', // dominio del CLIENTE targetType: 'URL', targetUrl: 'https://saas.com/web/cliente1', // MISMO target — mismo tenant injectHeaders: { 'X-Tenant-Id': 'cliente1' }, }), });

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:

https://saas.com/web/cliente1/dashboard?tab=stats // ← /web/cliente1 + /dashboard?tab=stats

Headers que CloudWapp inyecta a tu backend

Aparte de los injectHeaders que vos configures, CloudWapp agrega siempre estos:

X-Forwarded-Host: cliente1.saas.com // hostname original que pidió el browser X-Forwarded-Proto: https X-Forwarded-For: <IP del visitante> X-CloudWapp-Tenant: <userId del SaaS dueño del Domain> X-CloudWapp-Domain-Id: <uuid del registro Domain>

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

targetUrl: "https://saas.com/web/cliente1" // Tu backend (Express/Next/etc) ya tiene una ruta /web/:tenant app.get('/web/:tenant/*', (req, res) => { const tenantId = req.params.tenant; // "cliente1" // renderizar la página del tenant... });

B) Por header inyectado — la más limpia

targetUrl: "https://saas.com/portales" injectHeaders: { "X-Tenant-Id": "cliente1" } // Tu backend resuelve el tenant del header, no del URL app.use((req, res, next) => { req.tenantId = req.headers['x-tenant-id']; // también podés validar X-Internal-Auth para asegurar que viene de CloudWapp if (req.headers['x-internal-auth'] !== process.env.CW_SHARED_SECRET) { return res.status(403).end(); } next(); });

C) Por Host original — útil si ya tenés multi-tenant por subdomain

targetUrl: "https://saas.com" preserveHost: true // fwd cliente1.saas.com al backend // Tu backend lee el subdominio del Host original app.use((req, res, next) => { const host = req.headers['x-forwarded-host'] || req.headers.host; const tenantId = host.split('.')[0]; // "cliente1" req.tenantId = tenantId; next(); });

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:

1. Cliente browser → https://cliente1.saas.com/dashboard?tab=stats 2. Caddy edge → on_demand_tls "ask" → GET /auth/v1/domains/check?domain=cliente1.saas.com 3. NestJS API → 200 (si isVerified=true && sslActive=true) 4. Caddy → emite cert con Let's Encrypt (cacheado, no se vuelve a pedir) 5. Caddy → rewrite /dashboard?tab=stats → /_proxy/dashboard?tab=stats → api:3000 6. ProxyController → lookup hostname → fetch https://saas.com/web/cliente1/dashboard?tab=stats + inyecta X-Tenant-Id, X-Forwarded-Host, etc. 7. Stream response → cliente browser

📈 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

  1. POST /api/v1/analytics/sites con { name, domain } → te devuelve trackingId + snippet
  2. Pegá el snippet en el <head> del HTML del sitio del cliente
  3. Empezás a ver datos en el dashboard (vista 📈 Analytics)
  4. Al vencer el trial → activá la suscripción con POST /api/v1/billing/analytics/checkout
// Snippet auto-generado al crear el site: <script async defer src="https://cloudwapp.com/pixel.js" data-cw-site="cw_abc123"></script>

Custom events desde JS del cliente

El pixel expone una API global window.cwTrack(eventName, props) para trackear clicks, conversions, signups, etc.:

window.cwTrack('signup', { plan: 'pro', source: 'landing' }); window.cwTrack('button_click', { id: 'cta-hero' });

Endpoints

POST/api/v1/analytics/sites — crear site (arranca con trial)
GET/api/v1/analytics/sites — listar mis sites
GET/api/v1/analytics/sites/:id/stats?range=24h|7d|30d
DELETE/api/v1/analytics/sites/:id
POST/api/v1/billing/analytics/checkout — activar plan post-trial

Privacy

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 appConnect GitHub → elegís el repo de un dropdown → Crear app. Cada git push redeploya al instante (webhook). Sin pegar URLs ni tokens.

  1. Connect GitHub (1 vez): autorizás y elegís qué repos compartir
  2. Elegís el repo → nombre y branch se autocompletan → Crear app
  3. Primer build automático (~2-5 min según el stack)
  4. Listo: tu app queda en https://<slug>.apps.tudominio.com con SSL
  5. 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):

{ "name": "Mi API", "repoUrl": "https://github.com/usuario/mi-api.git", "repoToken": "ghp_...", // solo repos privados — se cifra at-rest "branch": "main", "buildType": "AUTO", // AUTO | DOCKERFILE | NIXPACKS "port": 3000, // puerto interno que escucha tu app "envVars": { "NODE_ENV": "production" }, // se cifran at-rest "autoDeploy": true }

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

RecursoDefault (editable desde admin)
RAM por app512 MB (hard limit)
CPU por app0.5 vCPU
Apps por usuario2
Timeout de build15 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

POST/api/v1/apps — crear app
GET/api/v1/apps — listar mis apps
POST/api/v1/apps/:id/deploy — deploy ahora
POST/api/v1/apps/:id/restart · /stop
GET/api/v1/apps/:id/logs?tail=200 — logs runtime
GET/api/v1/apps/:id/deployments — historial de builds
GET/api/v1/apps/:id/deployments/:depId/logs — log de un build
POST/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

  1. POST /api/v1/mail/mailboxes con { address, password } (o vista 📧 Correo del dashboard)
  2. Configurá los 4 registros DNS que devuelve la respuesta (MX, SPF, DKIM, DMARC)
  3. El MX se verifica solo cada 10 min (o forzá con POST /:id/verify-mx)
  4. Conectá tu cliente de correo con la config IMAP/SMTP que te da el panel
// Config típica del cliente de correo (Thunderbird/Outlook/móvil): IMAP: mail.cloudwapp.com : 993 (SSL/TLS) usuario = la dirección completa SMTP: mail.cloudwapp.com : 465 (SSL/TLS) usuario = la dirección completa

Deliverability (que tus correos NO caigan en spam)

Endpoints

POST/api/v1/mail/mailboxes — crear buzón
GET/api/v1/mail/mailboxes — listar
GET/api/v1/mail/mailboxes/:id — detalle (DNS + config cliente)
POST/api/v1/mail/mailboxes/:id/password — cambiar password
POST/api/v1/mail/mailboxes/:id/verify-mx — verificar MX ahora
DELETE/api/v1/mail/mailboxes/:id — eliminar (borra correos)
POST/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.

# Instalación npm install -g cloudwapp # alias corto: cw # Conectarse a tu instancia (la API key está en el dashboard → Configuración) cloudwapp login # Apps con CI/CD cloudwapp apps create --name "Mi API" --repo https://github.com/yo/mi-api.git --deploy cloudwapp apps builds mi-api # historial de builds cloudwapp apps logs mi-api # logs runtime cloudwapp apps env mi-api --set DATABASE_URL=postgres://... # Custom hostnames cloudwapp domains add app.cliente.com --target https://saas.com/web/cliente --header X-Tenant-Id=cli1 cloudwapp domains verify app.cliente.com # Correo cloudwapp mail create [email protected] --password "s3gur0pass" # WhatsApp cloudwapp wa send +5215512345678 "Hola!" --account <accountId> # Analytics cloudwapp analytics stats app.cliente.com --range 7d

Para CI sin TTY: exportá CLOUDWAPP_URL y CLOUDWAPP_API_KEY como env vars en lugar de usar login.

💳 Stripe Checkout

POST/api/v1/billing/checkout
{ "successUrl": "https://tu-app.com/success", "cancelUrl": "https://tu-app.com/cancel" } // Response: { "checkoutUrl": "https://checkout.stripe.com/..." }

📊 Estado de plan

GET/api/v1/billing/status
{ "plan": "FREE", "messagesSentThisMonth": 23, "messageLimit": 100, "monthResetAt": "2026-06-01T...", "hasStripeCustomer": false, "hasSubscription": false }

👥 Gestión de usuarios Rol ADMIN

MétodoEndpointDescripción
POST/api/v1/usersCrear usuario (acepta roles y password opcionales)
GET/api/v1/users/meMi perfil (cualquier rol)
GET/api/v1/usersListar 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-keyGenerar nueva API Key
DEL/api/v1/users/{id}Eliminar (cascada de sesiones/mensajes)

📊 Estadísticas de cola Rol ADMIN

GET/api/v1/messages/queue/stats
{ "waiting": 0, "active": 1, "completed": 1247, "failed": 3, "delayed": 0 }

❤️ Health Check

GET/health (sin prefix)
{ "status": "ok", "timestamp": "...", "database": "connected" }

📊 Métricas Prometheus

GET/metrics

Endpoint 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

StatusSignificadoCómo resolverlo
400Bad Request — body inválido o DTO mal formadoRevisá el campo "message" del error
401API Key inválida, ausente, JWT expiradoRevisá tu header / cookie / token
403Plan o rol insuficiente (ej. ADMIN required)Tu cuenta no tiene ese permiso
404Recurso no encontradoVerificá el ID
409Conflicto (email/dominio duplicado, sesión owned por otro nodo)Retry o usá distinto identificador
422Validación de class-validator fallóMirá "message" array en el response
429Rate limit (10/seg, 120/min, 5k/hora por user)Esperá unos segundos
500Error internoReportalo

📖 Swagger UI interactivo

Explorá la API ejecutando requests desde el browser en /docs (Swagger autogenerado).