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

# Arquitectura

> De una respuesta 2xx de la API a un POST firmado: las etapas de cola route y deliver, el libro mayor webhook_dispatch y la regla de publicación condicionada a suscriptores.

## Componentes

| Componente                                                           | Ruta (`backend/horizon-api`)                              |
| -------------------------------------------------------------------- | --------------------------------------------------------- |
| Publisher enganchado a cada respuesta `ok()`                         | `lib/webhook/outbound/webhook-out-route.publisher.ts`     |
| Caché de suscriptores activos (TTL 30 s)                             | `lib/webhook/outbound/webhook-subscription-cache.ts`      |
| Consumidor de la etapa route (`webhook-out-route`)                   | `lib/webhook/outbound/webhook-out-route.handler.ts`       |
| Resolutor de cuenta propietaria (`self` / `direct` / `via` / `none`) | `lib/webhook/outbound/webhook-account-resolver.ts`        |
| Consumidor de la etapa deliver (`webhook-out-deliver`)               | `lib/webhook/outbound/webhook-out-deliver.handler.ts`     |
| Helper de firma                                                      | `lib/webhook/outbound/webhook-signature.ts`               |
| Reglas de URL de callback (HTTPS, denylist)                          | `lib/webhook/outbound/webhook-callback-url.ts`            |
| Registro de colas y política de reintentos                           | `lib/webhook/outbound/webhook-out-queue-registrations.ts` |
| Catálogo de eventos generado                                         | `lib/webhook/catalogue/webhook-event-catalogue.data.ts`   |

## Flujo de una entrega

```mermaid theme={null}
flowchart LR
    subgraph api [Petición a horizon-api]
        route["Ruta v3 mutante<br/>respuesta 2xx"]
        publisher["publishApiTransactionEvent()"]
        cache["Caché de suscriptores activos"]
    end
    subgraph queue [Cola · lane webhook-out]
        routeTopic["webhook-out-route"]
        deliverTopic["webhook-out-deliver"]
    end
    subgraph ledger [PostgreSQL]
        dispatch[("webhook_dispatch")]
    end
    subscriber["Endpoint suscriptor<br/>HTTPS POST"]

    route --> publisher
    publisher -- "¿internal en caché?" --> cache
    cache -- sí --> routeTopic
    routeTopic --> resolve["Resolver cuenta · emparejar webhooks"]
    resolve -- "una fila por suscriptor" --> dispatch
    resolve --> deliverTopic
    deliverTopic --> deliver["Firmar · POST · registrar resultado"]
    deliver --> subscriber
    deliver --> dispatch
```

<Steps>
  <Step title="Publicar (dentro de la petición)">
    Toda ruta que pasa por el wrapper compartido `handler()`/`ok()` invoca
    `publishApiTransactionEvent` tras un `2xx`. El publisher deriva el
    internal del catálogo a partir del método y la ruta, y comprueba el
    conjunto en memoria de internals que hoy tienen al menos un suscriptor
    **activo**. Si nadie escucha, **no se encola nada** — las tablas de cola
    quedan vacías para las miles de mutaciones a las que nadie se suscribió.
    Los fallos aquí se registran y nunca llegan al cliente de la API.
  </Step>

  <Step title="Route (webhook-out-route)">
    El consumidor carga la fila del catálogo, resuelve la cuenta propietaria
    del recurso mutado (ver abajo) y selecciona los webhooks coincidentes:
    los globales siempre coinciden; los de cuenta solo cuando la cuenta
    resuelta es igual a su `account`. Se inserta una fila `webhook_dispatch`
    por coincidencia con `status = pending` y se envía un mensaje
    `webhook-out-deliver` por fila.
  </Step>

  <Step title="Deliver (webhook-out-deliver)">
    El consumidor construye el cuerpo fino, lo firma con el secreto del
    suscriptor, hace el `POST` con timeout de 10 s y `redirect: "error"`, y
    registra el resultado (`success`, `error` con `next_retry_at` o
    `exhausted`) en la fila del libro mayor. Los fallos reintentables se
    reprograman con el backoff propio; ver
    [Reintentos y fallos](/es/webhooks/retries-and-failures).
  </Step>
</Steps>

## Resolución de la cuenta propietaria

Las suscripciones por cuenta necesitan saber *a qué* cuenta pertenece la
fila mutada. El catálogo registra una de cuatro estrategias por evento:

| Tipo     | Significado                                          | Ejemplo                                                                                       |
| -------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `self`   | El recurso **es** la cuenta                          | `account.created` → `data.document_id`                                                        |
| `direct` | La tabla tiene columna `account`                     | `payment-debt.created` → `debt.account`                                                       |
| `via`    | La tabla referencia otra fila que sí tiene `account` | `subscription.history.created` → `subscription_history.subscription` → `subscription.account` |
| `none`   | Sin cuenta; solo coinciden suscriptores globales     | `general.address.updated`, todos los `*.purged`                                               |

El resolutor lee la fila una vez, en la etapa route; las entregas nunca
vuelven a tocar la tabla origen.

## Lanes y planificación

Ambos topics corren en el lane dedicado `webhook-out` (`vercel.json` →
`app/api/queue/consumers/webhook-out/route.ts`), de modo que una ráfaga de
entregas no puede dejar sin recursos a los jobs de negocio, ni un atasco de
negocio retrasar la notificación a un suscriptor. El topic deliver usa un
**calendario de reintentos propio** en lugar del predeterminado de la
plataforma: el backoff que ve el suscriptor forma parte del contrato público.

## Feature flag

La publicación está condicionada por `ENABLE_API_EVENT_QUEUE`. Mientras sea
`false` la API se comporta exactamente como antes: sin consulta al catálogo,
sin caché, sin tráfico de cola. Operaciones lo activa por entorno tras
sincronizar el catálogo y probar el primer suscriptor; ver
[Operación](/es/webhooks/operations).

<Warning>
  El cuerpo transporta identificadores a propósito. Nunca uses un webhook
  para mover datos autoritativos ni información personal; lee el recurso por
  la API con las credenciales del suscriptor.
</Warning>
