> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Recepción de eventos de Midaz

> Compara las vinculaciones directas a RabbitMQ y el Streaming Hub gestionado para recibir eventos del ledger de Midaz, y elige el transporte adecuado para tu integración.

Midaz emite un flujo de eventos de dominio. Emite uno cada vez que ocurre algo importante en el ledger. El ledger registra una transacción, un saldo entra en overdraft o creas una cuenta. Tu sistema puede reaccionar a esos cambios para conciliación, notificaciones, proyecciones derivadas o analítica. Para reaccionar, consume el flujo de eventos en lugar de hacer polling a la API REST.

Esta guía cubre las dos formas admitidas de recibir eventos de Midaz y cómo elegir entre ellas:

1. **RabbitMQ directo** — vincula tu propia cola a los exchanges AMQP de Midaz.
2. **Streaming Hub** — suscríbete a través del servicio gestionado de fan-out de Lerian.

## Las dos formas en resumen

***

Midaz publica eventos de dominio por dos transportes:

* Una publicación en **RabbitMQ (AMQP)** hacia topic exchanges que el despliegue de Midaz posee.
* Una publicación en un **bus de eventos interno** que el Streaming Hub consume y distribuye (fan-out) a los suscriptores por tenant.

La cobertura varía según el evento. Los eventos de ciclo de vida de transacción y de sobregiro se publican en **ambos** transportes durante la ventana de migración. Las entradas del log de auditoría van solo a RabbitMQ. El resto de los eventos del catálogo — de entidades, rutas, holders, instruments, fees, límites y reglas — van solo al bus de eventos interno.

Esto te da dos superficies de integración para los *mismos* eventos subyacentes:

| Evento        | RabbitMQ directo                      | Streaming Hub                                        |
| ------------- | ------------------------------------- | ---------------------------------------------------- |
| Transporte    | AMQP 0-9-1                            | Webhook / Pull (HTTP) / SQS / RabbitMQ / EventBridge |
| Te conectas a | El broker de Midaz                    | Una API REST del plano de control                    |
| Acoplamiento  | Fuerte (infra de Midaz)               | Débil (edge gestionado)                              |
| Ideal para    | Despliegues self-hosted / co-ubicados | Integradores externos / SaaS                         |

<Note>
  Si no operas tu propio broker de Midaz, usa el Streaming Hub. RabbitMQ directo es para integradores que se ejecutan dentro o junto al despliegue de Midaz y son dueños del broker.
</Note>

## Opción A — RabbitMQ directo

***

### Cómo funciona

El ledger de Midaz publica eventos a través de un productor interno hacia un conjunto de **topic exchanges**. Un consumidor vincula su propia cola al exchange correspondiente con un patrón de routing key. Luego el consumidor lee los mensajes sobre AMQP.

Datos del publicador (del servicio de ledger):

* **Content type:** `application/json`
* **Delivery mode:** persistente
* **Headers:** el productor inyecta el contexto de traza de OpenTelemetry en cada mensaje
* **Tenancy:** el productor es multi-tenant y publica los mensajes en un **vhost específico por tenant**

Exchanges de eventos y sus routing keys:

| Exchange                                  | Patrón de routing key                                               | Emite                                        |
| ----------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------- |
| `transaction.transaction_events.exchange` | `midaz.transaction.<STATUS>` (p. ej. `midaz.transaction.APPROVED`)  | Envelopes del ciclo de vida de transacciones |
| `transaction.overdraft_events.exchange`   | `midaz.balance.overdraft.<action>` (`drawn` / `repaid` / `cleared`) | Eventos de overdraft                         |
| `audit.append_log.exchange`               | `audit.append_log.key`                                              | Entradas del log de auditoría (append-log)   |

<Note>
  **Midaz habilita estos exchanges por defecto.** Midaz trata cada variable como habilitada salvo que la definas explícitamente en `false`. La configuración de ejemplo incluida establece las tres variables en `false`. Un stack que arranca desde ese ejemplo no emite nada hasta que sobrescribas las variables.
</Note>

### Para operadores self-hosted

Una variable de entorno dedicada controla cada exchange. Midaz trata una variable como habilitada salvo que la definas explícitamente en `false`. La configuración de ejemplo incluida establece las tres variables en `false`:

| Exchange                                  | Variable de entorno                   |
| ----------------------------------------- | ------------------------------------- |
| `transaction.transaction_events.exchange` | `RABBITMQ_TRANSACTION_EVENTS_ENABLED` |
| `transaction.overdraft_events.exchange`   | `RABBITMQ_OVERDRAFT_EVENTS_ENABLED`   |
| `audit.append_log.exchange`               | `AUDIT_LOG_ENABLED`                   |

En un stack que arranca desde la configuración de ejemplo, define la variable en `true` en la configuración de tu despliegue de Midaz. También puedes eliminar el valor `false`. Esto mantiene activo el exchange correspondiente.

### Estructura del mensaje

Los eventos de transacción envuelven el objeto de dominio en un envelope:

```json theme={null}
{
  "source": "midaz",
  "eventType": "transaction",
  "action": "APPROVED",
  "timestamp": "2026-07-06T12:00:00Z",
  "version": "<midaz build version>",
  "organizationId": "...",
  "ledgerId": "...",
  "payload": { "...full transaction JSON..." }
}
```

La routing key codifica la acción, por ejemplo `midaz.transaction.APPROVED` o `midaz.balance.overdraft.drawn`. Puedes vincular de forma acotada y evitar un filtro en el código.

### Ejemplo de configuración

Los parámetros de conexión provienen de la configuración de RabbitMQ del despliegue de Midaz. Incluyen `RABBITMQ_HOST`, `RABBITMQ_PORT_HOST` (el puerto AMQP con el que se conecta al broker — `3003` en la infraestructura incluida; `RABBITMQ_PORT_AMQP` es el puerto de gestión, a pesar de su nombre), un usuario consumidor como `RABBITMQ_CONSUMER_USER` y `RABBITMQ_VHOST`. En despliegues multi-tenant el productor resuelve un vhost por tenant; un despliegue single-tenant usa el único `RABBITMQ_VHOST` estático. Para TLS, define `RABBITMQ_TLS=true` en modo multi-tenant, o `RABBITMQ_URI=amqps` en modo single-tenant.

```python theme={null}
import pika

params = pika.URLParameters("amqps://consumer:***@midaz-rabbitmq:5671/<tenant-vhost>")
conn = pika.BlockingConnection(params)
ch = conn.channel()

# Los exchanges de transacción y sobregiro vienen en las definiciones de
# RabbitMQ incluidas en Midaz; decláralos de forma pasiva o replica su
# topología. El exchange de operación de saldo también viene ahí, pero es
# interno — alimenta el pipeline asíncrono de saldos de Midaz
# (RABBITMQ_TRANSACTION_BALANCE_OPERATION_*), así que no consumas de él.
# El exchange del log de auditoría NO está en esas definiciones — decláralo tú mismo.
exchange = "transaction.transaction_events.exchange"

# Vincula una cola durable propia a las routing keys que te interesan.
ch.queue_declare(queue="my-consumer.transactions", durable=True)
ch.queue_bind(
    queue="my-consumer.transactions",
    exchange=exchange,
    routing_key="midaz.transaction.*",   # todos los estados
)

def on_message(chan, method, props, body):
    handle(body)                          # application/json
    chan.basic_ack(method.delivery_tag)   # ack nativo, por mensaje

ch.basic_qos(prefetch_count=10)
ch.basic_consume(queue="my-consumer.transactions", on_message_callback=on_message)
ch.start_consuming()
```

### Cuándo usarlo

* **Operas o co-ubicas** el despliegue de Midaz y ya eres dueño del broker.
* Quieres **semántica AMQP nativa** — `ack`/`nack` por mensaje, prefetch/QoS, dead-letter exchanges que controlas, consumidores en competencia sobre una misma cola.
* Tu stack ya es nativo de RabbitMQ y quieres el salto de menor latencia, dentro del clúster.

### Contrapartidas

* Te acopla a la topología y las credenciales del broker interno de Midaz.
* La configuración de ejemplo incluida entrega los exchanges **deshabilitados**. Un stack que arranca desde ella necesita que el operador los vuelva a habilitar.
* Sin reintentos/dead-letter/auto-desactivación gestionados — la fiabilidad de entrega es tu responsabilidad del lado del consumidor.
* No es viable para un tercero que solo tiene acceso de red a un Midaz alojado.

## Opción B — Streaming Hub

***

### Cómo funciona

El Streaming Hub es el edge de entrega con fan-out de Lerian. Consume eventos del bus de eventos interno de Midaz y entrega cada evento coincidente a un **sink de suscriptor por tenant** que registras. Nunca tocas Kafka ni el broker. Registras una suscripción a través de un plano de control REST y eliges un método de entrega.

Tipos de sink admitidos:

* **`webhook`** — el hub hace POST de cada evento a tu endpoint HTTPS, **firmado con HMAC** con un secreto de firma por suscripción.
* **`pull`** — haces polling a `GET /v1/events`. La lectura es el acuse de recibo (cursor-as-ack), sin endpoint entrante.
* **`sqs`**, **`rabbitmq`**, **`eventbridge`** — el hub entrega en *tu* cola de AWS, *tu* broker de RabbitMQ o *tu* bus de EventBridge.

El hub añade fiabilidad de entrega sobre el flujo bruto. Ofrece un inbox durable con deduplicación de consumo único, reintentos por sink con backoff, dead-lettering al agotarse y auto-desactivación de endpoints que fallan.

### Eventos disponibles

Consulta el catálogo para ver los tipos de evento a los que puedes suscribirte:

```http theme={null}
GET /v1/catalog
Authorization: Bearer <jwt>
```

```json theme={null}
{
  "events": [
    {
      "eventType": "transaction.posted",
      "topic": "lerian.streaming.ledger_transaction.posted",
      "schemaVersion": "1.0.0",
      "schemaMajor": 1,
      "description": "A transaction was posted."
    }
  ]
}
```

Midaz publica un catálogo amplio con claves `<resource>.<event>` (todos en el esquema `1.0.0`), que incluye:

* `organization.*`, `ledger.*`, `account.*`, `asset.*`, `portfolio.*`, `segment.*` (`created` / `updated` / `deleted`)
* `operation-route.*`, `transaction-route.*` (`created` / `updated` / `deleted`)
* `balance.created`, `balance.config-changed`, `balance.deleted`
* `balance.overdraft-drawn`, `balance.overdraft-repaid`, `balance.overdraft-cleared`
* `transaction.posted`, `transaction.committed`, `transaction.canceled`, `transaction.reverted`

### Autenticación

El plano de control es **100% JWT de `lib-auth` (Bearer)**: el hub no emite credenciales propias. Presenta un JWT emitido por plugin-auth en cada llamada a `/v1`:

```http theme={null}
Authorization: Bearer <plugin-auth JWT>
```

El hub deriva el tenant únicamente de los claims validados del JWT (`tenantId`, y luego `owner` como alternativa), nunca del cuerpo de la solicitud. Un cliente de máquina obtiene su token del flujo de **client-credentials** de plugin-auth. Intercambia un id y un secreto de aplicación por un token de acceso de corta duración.

### Ejemplo de configuración — suscripción webhook

```http theme={null}
POST /v1/subscriptions
Authorization: Bearer <jwt>
X-Idempotency: <unique-key>
Content-Type: application/json

{
  "name": "my-webhook",
  "sink_kind": "webhook",
  "endpoint": "https://hooks.example.com/lerian",
  "event_types": ["transaction.posted", "transaction.committed"],
  "schema_major": 1,
  "plan_tier": "standard"
}
```

`201 Created` devuelve la suscripción **y el secreto de firma una sola vez**. Guárdalo de inmediato. Puedes rotar el secreto más tarde, pero nunca podrás volver a leerlo:

```json theme={null}
{
  "subscription": { "id": "0190b8e2-...", "verification_state": "active", "...": "..." },
  "signingSecret": "whsec_...ONCE..."
}
```

Verifica la firma HMAC en cada webhook entrante con ese secreto. Hazlo antes de confiar en el payload. Usa `POST /v1/subscriptions/:id/ping` para enviar un evento sintético firmado. Esto confirma que tu endpoint funciona.

### Ejemplo de configuración — suscripción pull (apta para serverless)

Créala con `"sink_kind": "pull"` (omite `endpoint` — el servidor lo sintetiza) y luego haz polling:

```http theme={null}
GET /v1/events?subscription_id=<uuid>&limit=100
Authorization: Bearer <jwt>
```

```json theme={null}
{
  "events": [
    { "seq": 42, "ceId": "...", "eventType": "transaction.posted",
      "receivedAt": "2026-07-06T12:00:00Z", "payload": { "...": "..." } }
  ],
  "next_cursor": 42
}
```

La lectura es el ack: el `seq` más alto devuelto avanza un cursor durable y monótono. Reenvía el `next_cursor` emitido por el servidor como `?after=` en la siguiente llamada. Cada evento lleva `ceId` para tu propia deduplicación.

### Sinks de cola (SQS / RabbitMQ / EventBridge)

Creas una suscripción de tipo cola sin credencial, y nace en `pending_verification`. No emite nada hasta que proporcionas una credencial de salida vía `PUT /v1/subscriptions/:id/credential`. El hub prueba esa credencial (conexión y autenticación) y, si tiene éxito, cambia la suscripción a `active`. Para un sink de RabbitMQ, define `endpoint` como `"<exchange>/<routingKey>"`. El host del broker vive en la credencial cifrada. Para sinks de AWS, obtén la política de confianza de IAM y `ExternalId` desde `GET /v1/subscriptions/:id/setup-artifacts`, y luego configura la concesión entre cuentas antes del PUT de la credencial.

### Cuándo usarlo

* Eres un **integrador externo** con solo acceso de red a un Midaz alojado.
* Quieres entrega **serverless** — un endpoint webhook o un bucle de pull HTTP, sin broker que operar.
* Quieres **fiabilidad gestionada** — deduplicación, reintentos/backoff, dead-lettering, auto-desactivación, webhooks firmados — sin construirla tú mismo.
* Quieres distribuir los mismos eventos hacia infraestructura **nativa de AWS** (SQS/EventBridge).

### Limitaciones

* Sin transporte SSE ni WebSocket — la entrega es push por webhook o pull HTTP (más los sinks de cola).
* El catálogo es **global** (idéntico sin importar qué tenant se autentique).
* Los consumidores pull son dueños de la posición de su cursor — un avance más allá del cursor omite ese hueco de forma permanente. Usa `?after=` para reproducir desde cualquier `seq` anterior.

***

## Tabla de decisión

| Criterio                     | RabbitMQ directo                                                     | Streaming Hub                                                            |
| ---------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Tu stack**                 | Ya es nativo de RabbitMQ / AMQP, dentro del clúster                  | Cualquier cosa que hable HTTPS (o AWS SQS/EventBridge)                   |
| **Modelo de despliegue**     | Operas / co-ubicas Midaz y eres dueño del broker                     | Midaz alojado / SaaS, integrador externo                                 |
| **Ack granular**             | ✅ `ack`/`nack` nativo por mensaje, prefetch, DLX                     | Parcial — 2xx del webhook o cursor-as-ack del pull; sin nack por mensaje |
| **Serverless**               | ❌ Requiere un consumidor AMQP de larga duración                      | ✅ Endpoint webhook o bucle de pull sin estado                            |
| **Simplicidad para empezar** | ❌ Creds del broker, vhost, exchange que hay que habilitar            | ✅ Un `POST /v1/subscriptions` autenticado                                |
| **Fiabilidad gestionada**    | ❌ Reintentos / DLQ / backoff a tu cargo                              | ✅ Deduplicación, reintentos/backoff, dead-letter, auto-desactivación     |
| **Latencia**                 | La más baja (salto dentro del clúster)                               | Ligeramente mayor (edge de fan-out)                                      |
| **Modelo de auth**           | Usuario/contraseña del broker (+ TLS opcional)                       | JWT de plugin-auth (Bearer); webhooks firmados                           |
| **Alcance de eventos**       | Exchanges de transacción / overdraft / auditoría (deben habilitarse) | Catálogo completo de Midaz vía `/v1/catalog`                             |
| **Distribuir a AWS**         | ❌ Constrúyelo tú mismo                                               | ✅ Sinks de SQS / EventBridge                                             |

**Regla general:** si eres dueño del broker y quieres control AMQP en crudo, usa **RabbitMQ directo**. En cualquier otro caso — integración externa, serverless, fiabilidad gestionada, entrega a AWS — usa el Streaming Hub.

***

## Consideraciones de seguridad

* **Credenciales de mínimo privilegio.** Para RabbitMQ directo, conéctate con un usuario **con alcance de consumidor** (no el usuario publicador/por defecto), restringido al vhost del tenant, y habilita TLS (`amqps://`). Para el Streaming Hub, limita la aplicación de plugin-auth al tenant que representa y rota el secreto de cliente.
* **Verifica las firmas de los webhooks.** Valida siempre la firma HMAC-v1 con tu secreto de firma por suscripción antes de actuar sobre un webhook. Trata una solicitud sin firma o con firma no coincidente como hostil.
* **Protege el secreto de firma.** El hub lo muestra una sola vez al crear o rotar, y nunca más. Guárdalo en un gestor de secretos y luego rótalo vía `POST /v1/subscriptions/:id/secret/rotate` (solapamiento de doble firma de 24 h) si el secreto se filtra.
* **Endpoints solo HTTPS.** Los sinks de webhook deben ser `https://`. El hub valida los endpoints contra SSRF al crear y en el PUT de credencial, y rechaza los destinos en texto plano o privados.
* **El aislamiento de tenant se deriva de los claims.** El hub lee la identidad del tenant solo de los claims validados del JWT (o `ce-tenantid` en la ingesta), nunca del cuerpo de la solicitud, así que nunca envíes un `tenant_id` en un payload.
* **Idempotencia y deduplicación.** Envía `X-Idempotency` en las llamadas mutantes del plano de control. En el plano de datos, deduplica por `ceId` (Streaming Hub), el id del mensaje o tu propia clave (RabbitMQ), porque ambos transportes son al-menos-una-vez.
* **No expongas las interioridades de Midaz.** Nunca compartas el exchange interno de operaciones de saldo ni las credenciales del broker con consumidores externos, y pon a los terceros detrás del Streaming Hub.
