Flujo OUTGOING (tu sistema → cliente WhatsApp)¶
Tu sistema decide enviar un msg al cliente
│
▼
POST http://qrsgen:3100/api/instances/<INSTANCE_NAME>/webhook
Headers: Content-Type: application/json
X-Qrsgen-Signature: sha256=<hex> (opcional, si WEBHOOK_HMAC_SECRET set)
Body: {
"event": "message_created",
"message_type": "outgoing",
"content": "Hola...",
...
}
│
▼
api.POST("/instances/:name/webhook"):
│ leer raw body
│ HMAC middleware (si secret configurado)
│ ¿mgr.IsConnected(instance)?
│
│ SI NO
│ │ │
│ ▼ ▼
│ HandleFor outbox.Enqueue → 202 {status:"queued", queue_id, expires_at}
│ │
│ ▼
│ bridge/outgoing.go HandleFor():
│ - skip si message_type != "outgoing"
│ - skip si private=true
│ - skip si source_id startswith "WAID:" (eco)
│ - skip si remoteJid startswith "qrsgen-qr-" (ops contact)
│ - dedup por msg_id
│ - spamguard.CheckAndRecord → si dup: emit "spam_blocked"
│ - banwatch.Record(success|failure)
│ - sender.SendText/SendMedia → whatsmeow → Meta
│ - PATCH source_id="WAID:..." en downstream
│ │
│ ▼
│ 200 {status:"sent"}
▼
Cliente WhatsApp recibe el msg
Outbox: el cor de "cero pérdida en restarts"¶
Cuando mgr.IsConnected(instance) == false (restart, blip, sesión
perdida), qrsgen persiste el payload crudo en bridge_outgoing_queue
con expires_at = NOW() + TTL (default 5 min) y devuelve
202 {status: "queued", queue_id: N, expires_at}.
Dos goroutines lo manejan:
- drainer (cada 5s): selecciona pending para instancias que
IsConnected()ahora y las entrega viabridge.Outgoing.HandleForRaw. Cada fail incrementaattempts; cuando llega a 5 → statusfailed+ audit entry. - expirer (cada 30s): marca como
expiredlas pending pasadas suexpires_aty emite el evento lifecycleoutgoing_expiredcon un preview del contenido.
Safety nets¶
Para evitar enviar mensajes erróneos a WhatsApp, HandleFor aplica
filtros que hacen no-op:
message_type != "outgoing".private == true(nota privada, no se envía a WhatsApp).source_idempieza con"WAID:"(eco del propio mensaje).remoteJidempieza con"qrsgen-qr-"(contacto sintético del panel ops).- Spamguard activado + contenido duplicado → emit
spam_blocked+ skip.
Per-instance MaxQueueDepth¶
outbox.MaxQueueDepth (default 200) limita el backlog por instancia.
Cuando se alcanza, nuevos POSTs devuelven 503 en lugar de encolar más.
Evita runaway buffering cuando una instancia está permanentemente muerta.
Glosario¶
Outgoing: mensaje que va de tu sistema al cliente WhatsApp.
Outbox pattern: patrón de diseño donde escrituras se persisten primero en una tabla "outbox" local, y un proceso async las despacha hacia el destino final. Garantiza que no se pierde nada en restarts.
Drainer: goroutine que periódicamente revisa la outbox y reentrega mensajes pending cuando su instancia ha vuelto a estar conectada.
Expirer: goroutine que marca como expired los mensajes pending
cuya expires_at ha pasado. Emite el evento lifecycle outgoing_expired.
TTL (Time To Live): tiempo máximo que un mensaje puede esperar en la outbox antes de expirar. Default 5 minutos en qrsgen.
Safety net: filtros defensivos que rechazan mensajes en estados peligrosos (notas privadas, ecos, contactos sintéticos). Devuelven 200 OK sin enviar nada.
Spamguard: filtro que descarta outgoings duplicados (mismo contenido
al mismo JID consecutivamente). Activo solo cuando
spamguard_enabled=true en la instancia.
Dedup por msg_id: mecanismo de idempotencia. Si el downstream
reintenta un POST con el mismo id, qrsgen lo detecta y devuelve OK
sin reenviar.
Banwatch.Record: hook que registra cada send (exitoso o fallido) para que el detector de ban-risk compute velocity / diversity / delivery_ratio.
MaxQueueDepth: tamaño máximo del backlog outbox por instancia (200 default). Evita acumulación infinita si una instancia muere permanentemente.
Runaway buffering: situación donde un componente sigue acumulando trabajo pendiente porque nunca llega a procesarlo, eventualmente consumiendo toda la memoria/disco. MaxQueueDepth lo previene.