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

## Endpoints

```
POST   /api/v1/ecf                emitir
POST   /api/v1/ecf/validate       validar sin emitir
GET    /api/v1/ecf/{id}           estado y detalle
GET    /api/v1/ecf/{id}/xml       XML firmado (?rfce=true para el RFCE)
GET    /api/v1/ecf/{id}/trackids  trackIds registrados en la DGII
GET    /api/v1/ecf/{id}/attempts  historial de envíos a la DGII
GET    /api/v1/ecf?...            listado y búsqueda paginados
POST   /api/v1/ecf/{id}/retry     reencolar un envío failed, review o rejected (si la DGII no consumió el e-NCF)
```

Todos se autentican con el header `X-API-Key` (ver
[Autenticación rápida](/empezando/autenticacion/)). El detalle de
`/validate` está en [Validar sin emitir](/emision/validar-sin-emitir/), y el
de los `GET` en [Consultar un comprobante](/emision/consultar-comprobante/).
Esta página cubre la semántica de `POST /api/v1/ecf`. El reenvío y el historial
de envíos se explican en [Rechazos y reintentos](/emision/rechazos-y-reintentos/).

## Pausa y tipos permitidos

Desde Configuración puedes poner dos frenos a tu propia emisión (tu organización también puede
fijarlos para todos sus contribuyentes):

| Situación                                  | Respuesta                                                 |
| ------------------------------------------ | --------------------------------------------------------- |
| La emisión está pausada                    | `409` `Ecf.EmissionPaused`, con el motivo que escribiste. |
| El tipo de comprobante no está en tu lista | `403` `Ecf.TypeNotAllowed`.                               |

En ambos casos no se consume ningún e-NCF ni queda una clave de idempotencia pendiente, así que
puedes reintentar con la misma clave cuando levantes el freno. La pausa solo frena emisiones
nuevas: lo que ya firmaste se sigue enviando a la DGII, y la validación y las consultas siguen
funcionando.

## Ambientes que incluye tu plan

Los planes gratuitos (Developer y Developer + Soporte) solo permiten el ambiente de pruebas. Con
uno de ellos, `POST /ecf` en certificación o en producción responde `409`
`Billing.EnvironmentNotIncluded`, y tampoco puedes crear una API key de esos ambientes. No se
consume ningún e-NCF ni queda una clave pendiente. Para usar certificación o producción sube a un
plan de pago. Lo que emitas en pruebas y en certificación no cuenta contra el cupo de tu plan: solo
cuenta lo aceptado en producción.

## Tope de sobreconsumo de la organización

Si tu organización fijó un tope de sobreconsumo, `POST /ecf` responde `409` `Billing.SpendCapReached`
cuando el excedente del período (lo que pasa del cupo incluido en el plan) llega a ese monto. No se
consume ningún e-NCF ni queda una clave pendiente. La emisión se reanuda en el período siguiente o
cuando la organización sube el tope.

## Topes de emisión

Desde Configuración puedes limitar tu propia emisión (tu organización también puede fijar los
topes para todos sus contribuyentes):

| Situación                                   | Respuesta                                                                      |
| ------------------------------------------- | ------------------------------------------------------------------------------ |
| Alcanzaste el tope mensual de Producción    | `409` `Ecf.MonthlyLimitReached`, hasta el mes siguiente o hasta subir el tope. |
| Superaste tu máximo de emisiones por minuto | `429` `Ecf.RateLimited`, con el header `Retry-After` en segundos.              |

El tope mensual solo cuenta los comprobantes de Producción; los de pruebas no cuentan. En los dos
casos no se consume ningún e-NCF ni queda una clave de idempotencia pendiente, así que puedes
reintentar con la misma clave. El límite del plan de tu organización sigue rigiendo por encima de
tu máximo por minuto.

## Ráfagas y saturación

NovaFE atiende un número limitado de emisiones a la vez. Si envías una ráfaga (por ejemplo, 40
comprobantes de golpe), las que sobran esperan su turno y se atienden en orden, sin que tengas que
hacer nada: solo tardan un poco más. Si la ráfaga es tan grande que también se llena la cola de
espera, la API responde `503` con el header `Retry-After` (en segundos). Es un aviso de saturación,
no un error de tu comprobante: no consume ningún e-NCF ni deja una clave pendiente, así que
reintenta con la misma `Idempotency-Key` pasado ese tiempo.

| Situación                         | Respuesta                                        |
| --------------------------------- | ------------------------------------------------ |
| Hay demasiadas emisiones en curso | `503`, con `Retry-After` en segundos. Reintenta. |

## Reintentos seguros con `Idempotency-Key`

El header `Idempotency-Key` es **obligatorio** en cada `POST /ecf`: genera un
valor único por comprobante (cualquier string, por ejemplo un UUID). Si falta,
la API responde `400` `Ecf.IdempotencyKeyRequired`; si lo mandas en el cuerpo
JSON en vez del header, responde `400` `Ecf.IdempotencyKeyInBody`. La razón es
que un e-CF no se puede borrar: si una llamada se corta por timeout y
reintentas sin clave, se emitirían dos comprobantes. Con la clave, repetir el
mismo request es seguro:

| Situación                                                    | Qué pasa                                                                   |
| ------------------------------------------------------------ | -------------------------------------------------------------------------- |
| Misma clave, mismo cuerpo                                    | `200 OK` con la respuesta del comprobante original. No se emite dos veces. |
| Misma clave, cuerpo distinto                                 | `409` `Ecf.IdempotencyKeyConflict`.                                        |
| Misma clave, la petición anterior todavía se está procesando | `409` `Ecf.RequestInProgress`.                                             |

<Aside type="note" title="Qué cuenta como 'el mismo cuerpo'">
  NovaFE compara el cuerpo ya interpretado (sin el `Idempotency-Key`), no los
  bytes crudos del request: el orden de los campos o los espacios en blanco no
  importan, pero cambiar cualquier valor sí cuenta como un cuerpo distinto.
</Aside>

Si tu integración reintenta antes de que termine el `RequestInProgress`,
espera unos segundos y reintenta con la misma clave.

Si la emisión falla sin crear el comprobante (por ejemplo, no hay rango de
secuencia disponible), la clave se libera y puedes reintentar de inmediato,
con la misma clave y el cuerpo corregido.

## Deduplicación por `internalNumber`

Si mandas `internalNumber` (tu propio número de factura) y ya existe un
comprobante con ese mismo número para tu tenant, NovaFE no emite uno nuevo:
te devuelve `200` con el comprobante existente. Es un mecanismo aparte del
`Idempotency-Key`, pensado para cuando tu sistema reintenta una operación
sin recordar qué clave de idempotencia usó la primera vez.

## Detección de duplicados por huella

Una tercera capa, independiente de las dos anteriores, para el caso en que
tu integración arma dos comprobantes distintos (con distinto
`internalNumber` y distinto `Idempotency-Key`) que en la práctica son el
mismo. Se activa por configuración de tu tenant
(`ecf.duplicate_detection_mode`, apagada por defecto):

- **`off`** (por defecto): no corre ningún chequeo.
- **`observar`**: no bloquea nada. Si detecta una posible duplicación,
  emite el comprobante de todas formas y además dispara el webhook
  `ecf.duplicate_suspected`, para que decidas tú qué hacer.
- **`bloquear`**: si detecta una posible duplicación, responde `409`
  `Ecf.DuplicateSuspected` en vez de emitir.

La huella compara comprador, tipo de comprobante, monto total, fecha de
emisión y ambiente, contra los comprobantes emitidos en una ventana
reciente (5 minutos por defecto, configurable entre 30 segundos y una
hora).

<Aside type="caution" title="Necesita un comprador identificado">
  Este chequeo solo corre si el comprador tiene RNC o cédula. Si emites a
  "Consumidor Final" sin identificar al comprador, la huella no se evalúa: de lo
  contrario, dos ventas distintas a consumidores finales por el mismo monto el
  mismo día se marcarían como duplicadas por error.
</Aside>

## El recorrido de una emisión

`POST /ecf` resuelve tu emisor y ambiente desde la API key, revisa
idempotencia y duplicados, asigna la secuencia, arma y calcula el
comprobante, lo firma con tu certificado, y **persiste y encola todo en una
sola transacción** antes de tocar la DGII. Desde ahí hay dos modos, según la
configuración de tu cuenta:

| Modo                             | Qué hace `POST /ecf`                                                                                                                                                                                                                                        | Cómo conoces el resultado                                                              |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Con espera (el modo por defecto) | Intenta el envío dentro del propio request, durante unos 8 segundos. Si la DGII responde a tiempo, la respuesta ya trae el estado final y el bloque `dgii`. Si tarda más, responde con `submitted` o `signed` y un proceso de fondo termina el seguimiento. | En la respuesta, o después por webhook o consultando el comprobante.                   |
| Sin espera (asíncrono)           | Responde de inmediato con el comprobante firmado: `status: "signed"` y `dgii: null`. El envío a la DGII ocurre en segundo plano.                                                                                                                            | Siempre por webhook (`ecf.submitted`, `ecf.accepted`...) o consultando el comprobante. |

El modo sin espera es útil para sistemas intermedios con muchas cajas
emitiendo en paralelo, que prefieren no mantener peticiones abiertas. Se
activa con el ajuste `submission.wait_for_result`, que puede fijar tu
organización o tu contribuyente, y que NovaFE puede desactivar de forma
global ante un incidente. Ver [Ajustes del contribuyente](/cuenta/ajustes/).

<Aside type="tip" title="El POST no depende de que la DGII responda a tiempo">
  Una vez que tu comprobante está firmado y guardado, `POST /ecf` no vuelve a
  fallar aunque la DGII esté lenta o no disponible: la respuesta llega con un
  estado intermedio (ver [Respuesta y estados](/emision/respuesta-estados/)) y
  conoces el resultado final por webhook o consultando el comprobante. Tu
  integración debe estar preparada para recibir `signed` en la respuesta, sin
  importar el modo.
</Aside>

## Errores

Todo error de la API (de negocio, de validación, o inesperado) responde
`application/problem+json`, con esta forma base:

```json
{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "El e-NCF solicitado no existe.",
  "status": 404,
  "traceId": "b1f6c3e2-8a2d-4e10-9c7a-4f2e6d1a9b30"
}
```

Con un solo error, la descripción va en `title` y no hay ningún otro campo
con el motivo. Con más de uno a la vez, se agrega `errors`: una lista de
`{ code, description }` para errores de negocio, o un diccionario
`{ campo: [mensajes] }` cuando es un error de validación de formulario.

`traceId` identifica la petición de punta a punta y es el dato a dar si
necesitas soporte. Por defecto lo genera NovaFE, pero puedes fijar el tuyo
mandando el header `X-Trace-Id`: la respuesta lo devuelve tal cual, así
correlacionas tus propios logs sin depender del valor que generó la API.