import { Aside } from "@astrojs/starlight/components";

## El sobre (envelope)

Toda entrega llega con la misma forma exterior, sin importar el evento:

```json
{
  "id": "evt_018f3c2a1e6b7d40a5c8e1d2b3a4f506",
  "object": "event",
  "type": "ecf.accepted",
  "apiVersion": "1",
  "createdAt": "2026-09-07T14:03:11-04:00",
  "data": {
    "object": { "...": "el recurso afectado" }
  }
}
```

| Campo         | Nota                                                                                                             |
| ------------- | ---------------------------------------------------------------------------------------------------------------- |
| `id`          | Único por evento. Es tu clave para deduplicar (ver [Verificar firma](/webhooks/verificar-firma/)).               |
| `object`      | Siempre `"event"`.                                                                                               |
| `type`        | El tipo de evento, por ejemplo `ecf.accepted`.                                                                   |
| `apiVersion`  | Versión del formato del sobre, hoy `"1"`.                                                                        |
| `createdAt`   | Hora dominicana, como todo lo que emite la API.                                                                  |
| `data.object` | El recurso afectado. Salvo que se indique lo contrario abajo, tiene la misma forma que el `GET` correspondiente. |

Te suscribes a un tipo exacto, a una categoría completa con comodín
(`ecf.*` cubre los 6 del ciclo de vida más `ecf.duplicate_suspected`), o a
todo con `*`.

## Ciclo de vida del e-CF

| Evento                     | Cuándo se dispara                                                                                   |
| -------------------------- | --------------------------------------------------------------------------------------------------- |
| `ecf.submitted`            | La DGII recibió el comprobante y devolvió un `TrackId`. Se repite si NovaFE reenvía el comprobante. |
| `ecf.accepted`             | La DGII lo aceptó: fiscalmente válido.                                                              |
| `ecf.accepted_conditional` | Aceptado con una observación, igualmente válido.                                                    |
| `ecf.rejected`             | La DGII lo rechazó de forma definitiva.                                                             |
| `ecf.review`               | La DGII no resolvió tras el ladder de consultas.                                                    |
| `ecf.failed`               | Falló el transporte tras agotar los reintentos.                                                     |

`ecf.rejected` solo se emite cuando el rechazo es definitivo. Si la DGII falla
de forma pasajera y NovaFE reenvía el comprobante por su cuenta, no se emite
mientras dure el reintento; se emite únicamente si los reintentos se agotan. Ver
[Rechazos y reintentos](/emision/rechazos-y-reintentos/).

`data.object` es el mismo `EcfDto` que devuelve
[`GET /ecf/{id}`](/emision/respuesta-estados/). No existe un evento
`ecf.signed`: ese estado ya lo tienes en la respuesta de `POST /ecf`. Si tu
cuenta no espera el resultado de la DGII al emitir, el primer evento que
recibirás de cada comprobante es `ecf.submitted`.

## Duplicado sospechado

| Evento                    | Cuándo se dispara                                                                                                          |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `ecf.duplicate_suspected` | Con el modo `observar` de detección de duplicados, el e-CF se emitió igualmente pero coincidió en huella con uno reciente. |

Este es uno de los dos eventos cuyo `data.object` **no** es directamente
el recurso, sino una estructura propia:

```json
{
  "ecf": { "...": "EcfDto" },
  "previousEcfId": "...",
  "previousEncf": "E310000000010"
}
```

Ver [Endpoints y semántica](/emision/endpoints-y-semantica/) para el
detalle completo de cómo funciona la detección por huella.

## Recepción B2B

| Evento                         | Cuándo se dispara                                                                                        |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `inbound_ecf.received`         | Recibiste un e-CF de otro contribuyente y le devolviste el ARECF firmado, lo hayas aceptado o rechazado. |
| `commercial_approval.received` | Un comprador externo aprobó o rechazó comercialmente un e-CF tuyo.                                       |

Ver [Recibir un e-CF](/recepcion-b2b/recibir/).

## Vencimientos

| Evento                 | Cuándo se dispara                                                             |
| ---------------------- | ----------------------------------------------------------------------------- |
| `certificate.expiring` | Tu certificado activo se acerca a su vencimiento (90, 30, 15 o 7 días antes). |
| `certificate.expired`  | Tu certificado venció.                                                        |
| `sequence.expiring`    | Un rango de e-NCF se acerca a su vencimiento (30 o 7 días antes).             |
| `sequence.expired`     | Un rango venció.                                                              |
| `sequence.low`         | Un rango cayó al 20 % o menos de su stock.                                    |
| `sequence.exhausted`   | Un rango se quedó sin números por entregar.                                   |

Ver [e-NCF y secuencias](/conceptos/secuencias/).

## Contingencia

| Evento                    | Cuándo se dispara                                           |
| ------------------------- | ----------------------------------------------------------- |
| `contingency.activated`   | NovaFE entra en modo de contingencia (automático o manual). |
| `contingency.deactivated` | Sale del modo de contingencia.                              |

`data.object` es `{ status: "active" | "inactive", changedAt, oldestPendingMinutes }`.
A diferencia del resto, no es un evento de tu tenant puntual: se emite a
todos los tenants suscritos por igual, porque la contingencia es un estado
de la plataforma completa, no de un comprobante o de una cuenta.

## Pausa de emisión

| Evento                    | Cuándo se dispara                                                |
| ------------------------- | ---------------------------------------------------------------- |
| `tenant.emission_paused`  | Tú o tu organización pausaron la emisión de comprobantes nuevos. |
| `tenant.emission_resumed` | Se levantó la pausa a mano.                                      |

`data.object` es `{ tenantId, paused, reason, until, source, changedBy, organizationId }`, donde
`source` es `tenant` u `organization`. Solo avisan cuando el estado efectivo cambia: cambiar
únicamente el motivo no genera otro evento, y una pausa que termina sola por su hora de
reanudación tampoco genera uno. Mientras la emisión está pausada, `POST /ecf` responde `409` con el
código `Ecf.EmissionPaused`; lo ya firmado, los reintentos, la validación y las consultas siguen
funcionando.

## Alertas

| Evento                   | Cuándo se dispara                                                                  |
| ------------------------ | ---------------------------------------------------------------------------------- |
| `alert.rejection_spike`  | El porcentaje de comprobantes rechazados en la última hora llegó a tu umbral.      |
| `alert.emission_silence` | Pasaron las horas laborales que configuraste sin que emitieras ningún comprobante. |

Las dos alertas están apagadas hasta que las configures (`alerts.rejection_rate_threshold_pct`,
`alerts.no_emission_hours` y `alerts.business_hours`). Se evalúan cada 15 minutos.

`alert.rejection_spike`: `data.object` es `{ tenantId, windowMinutes, total, rejected, ratePct,
thresholdPct }`. Necesita al menos 5 comprobantes en la última hora, para que un solo rechazo no
cuente como un 100 %, y se repite a lo sumo cada 6 horas mientras el pico siga en pie.

`alert.emission_silence`: `data.object` es `{ tenantId, lastEmissionAt, silentHours, thresholdHours }`.
Solo cuenta el tiempo dentro de tu horario laboral y avisa una sola vez por silencio: vuelve a avisar
únicamente después de que emitas un comprobante nuevo. No avisa fuera del horario laboral, mientras la
emisión está pausada, ni a un contribuyente que todavía no ha emitido nunca.

## Tope mensual de emisión

| Evento                         | Cuándo se dispara                                                           |
| ------------------------------ | --------------------------------------------------------------------------- |
| `tenant.monthly_limit_warning` | Tu emisión de Producción del mes llegó al 80 % del tope mensual.            |
| `tenant.monthly_limit_reached` | Llegó al 100 %: los comprobantes nuevos se rechazan hasta el mes siguiente. |

`data.object` es `{ tenantId, period, limit, used }`, donde `period` es el mes (`yyyy-MM`). Cada
umbral avisa una sola vez por mes. Mientras el tope está alcanzado, `POST /ecf` responde `409` con
el código `Ecf.MonthlyLimitReached`.

## Uso y facturación

| Evento                           | Cuándo se dispara                                                           |
| -------------------------------- | --------------------------------------------------------------------------- |
| `usage.threshold_reached`        | Tu organización consumió el 80 % de su cupo incluido de e-CF del período.   |
| `usage.limit_reached`            | Consumió el 100 % de e-CF.                                                  |
| `usage.tenant_threshold_reached` | El pico de tenants del período llegó al 80 % de tu cupo.                    |
| `usage.tenant_limit_reached`     | El pico de tenants llegó al 100 %.                                          |
| `usage.member_threshold_reached` | El pico de miembros de la organización llegó al 80 % de tu cupo.            |
| `usage.member_limit_reached`     | El pico de miembros llegó al 100 %.                                         |
| `billing.period_closed`          | Se cerró un período de facturación, con el resumen de las tres dimensiones. |

Los cuatro primeros son el mismo aviso proactivo de excedente que describe
[Planes y precios](/cuenta/planes-y-precios/), solo que por webhook en vez
de tener que consultarlo tú.

## El evento de prueba

`webhook.ping` no es un evento real de tu cuenta: es el que dispara
[`POST /webhooks/{id}/ping`](/webhooks/suscribirse/) para que pruebes un
endpoint. No te puedes suscribir a él, y nunca llega por ninguna otra vía.

<Aside type="note" title="Todavía no existen">
  `ecf.voided` (anulación de un e-CF ya emitido) y los eventos de contingencia
  Tipo 2 y 3 llegarán con sus respectivos módulos. Si te suscribes a un tipo que
  no existe todavía, la suscripción falla con `400` en vez de quedar
  silenciosamente inactiva.
</Aside>