Invoices
Tres endpoints alrededor del recurso invoice:
| Método | Path | Para qué |
|---|---|---|
POST | /invoices | Enviar un nuevo e-CF |
GET | /invoices/:trackOrId | Consultar estado |
GET | /invoices/:trackOrId/xml | Obtener el XML generado/firmado |
GET | /invoices | Listar historial paginado |
Todos requieren Authorization: Bearer <JWT>.
"Invoices" cubre TODA la familia de e-CFs DGII, no solo facturas.
El tipo de documento se determina por el campo TipoeCF dentro del
XML/JSON que mandas — no por el endpoint o un parámetro de URL.
Tipos de e-CF soportados
POST /invoices acepta cualquiera de los siguientes tipos vía el campo
TipoeCF del header (Encabezado.IdDoc.TipoeCF en JSON Digimart):
| Código | Tipo de documento | Uso típico |
|---|---|---|
31 | Factura de Crédito Fiscal | B2B con derecho a crédito ITBIS |
32 | Factura de Consumo | B2C — consumidor final |
33 | Nota de Débito Electrónica | Aumento de un e-CF previamente emitido |
34 | Nota de Crédito Electrónica | Devolución / descuento sobre un e-CF previo |
41 | Compras | Registro de compras a no-emisores (gastos informales) |
43 | Gastos Menores | Operaciones < umbral DGII |
44 | Régimen Especial | Sectores con tratamiento fiscal específico |
45 | Gubernamental | Operaciones con el Estado |
46 | Comprobante Pago Exterior | Pagos a proveedores fuera del país |
47 | Régimen Especial Pago Exterior | Combinación de 44 y 46 |
Otros documentos también soportados
POST /invoices también acepta Resumen de Facturas de Consumo (RFCE)
y Aprobaciones Comerciales (ACECF / ANECF). El endpoint del cliente
es el mismo — el worker auto-detecta el root element del XML/JSON y
rutea al endpoint DGII correcto bajo el capó:
| Root element | Tipo de documento | Endpoint DGII destino |
|---|---|---|
<ECF> | e-CF estándar (tipos 31-47) | ecf.dgii.gov.do/{env}/recepcion/api/FacturasElectronicas |
<RFCE> | Resumen Facturas Consumo | fc.dgii.gov.do/{env}/recepcionfc/api/recepcion/ecf |
<ACECF> | Aprobación Comercial (emisor → receptor) | ecf.dgii.gov.do/{env}/aprobacionComercial/api/AprobacionComercial |
<ANECF> | Aprobación de Notas | ecf.dgii.gov.do/{env}/aprobacionComercial/api/AprobacionComercial |
Si mandas format: 'digimart' con un JSON cuyo root es RFCE, el worker
lo convierte a XML con el tag correcto, lo firma, y lo manda al host
CF de DGII. Tú no tienes que hacer nada distinto — un solo POST,
el routing es invisible.
POST /invoices
Envía un comprobante para procesamiento async. Responde 202 con un identificador que puedes usar para consultar estado.
POST /api/v1/fe/invoices
Authorization: Bearer <JWT>
Content-Type: application/jsonScope requerido: invoices:write
Request body
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
format | "xml" | "digimart" | ✅ | Forma del payload |
xml | string | Si format="xml" | XML completo del e-CF |
payload | object | Si format="digimart" | JSON estructurado |
clientRequestId | string | ❌ | Idempotency key opcional |
asUser | string (correo) | ❌ | Emitir con el certificado de un usuario específico (ver Certificado y usuarios) |
asUserId | string (UUID) | ❌ | Igual que asUser pero por ID. Si mandas ambos, gana asUserId |
Certificado y usuarios
Antes de emitir en producción necesitas un certificado digital (.p12) cargado. Tienes dos formas de subirlo desde el portal:
- A nivel de empresa (recomendado) — en Configuración → Certificado
digital subes un solo
.p12+ su clave. Se usa para todas tus emisiones. Ideal si tienes una sola identidad fiscal. - A nivel de usuario — en Usuarios creas actores (p. ej. por
sucursal o RNC) y le subes a cada uno su propio
.p12. Eliges cuál usar conasUser/asUserIden la solicitud.
Debes tener al menos un certificado (de empresa o de usuario) para
emitir. Si no, la emisión queda en status: "error" con un mensaje
indicándote que subas el .p12 en el portal.
¿Cómo elige el worker qué certificado usar?
- Si mandas
asUser/asUserId→ usa el certificado de ese usuario (y suemisorOverride, si tiene). Recomendado cuando manejas varios certificados. - Si NO mandas
asUsery tienes un certificado de empresa → lo usa. - Si NO mandas
asUser, no hay cert de empresa, y hay un solo usuario con certificado → lo selecciona automáticamente. - Si NO mandas
asUsery hay varios usuarios con certificado (y sin cert de empresa) → la emisión falla pidiéndote indicar cuál víaasUser.
¿Solo tienes una identidad fiscal? Sube el .p12 en Configuración y
omite asUser. Especifica asUser únicamente cuando manejes múltiples
certificados (p. ej. varias sucursales o RNC).
format: "xml"
Le pasas el XML del e-CF tal cual lo construiste. Lo firmamos y enviamos.
{
"format": "xml",
"clientRequestId": "factura-001-2026-06-15",
"xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ECF xmlns:xsi=\"...\">...</ECF>"
}format: "digimart"
JSON estructurado que sigue el esquema DGII. Tienes dos modos de uso del mismo formato:
Modo A — auto-fill (recomendado)
Omites 4 campos y nosotros los rellenamos por ti:
| Campo omitido | De dónde sale |
|---|---|
Encabezado.IdDoc.TipoeCF | Los 2 dígitos después del prefix E en tu eNCF (ej. E31... → "31") |
Encabezado.IdDoc.FechaVencimientoSecuencia | Lookup de tu config NCF (la fecha que DGII te autorizó). Configúrala vía los endpoints de NCF de esta API o manualmente en el portal |
Encabezado.Emisor | Construido de los datos fiscales de tu empresa (RNC, razón social, dirección, etc.) configurados en el portal |
Encabezado.Totales | Calculado de DetallesItems.Item[] agrupando por IndicadorFacturacion |
Modo B — payload completo (tal cual DGII)
Si prefieres control total — porque ya tienes el cálculo de Totales por tu lado, quieres fijar un Emisor distinto al del tenant, o estás migrando desde un sistema que ya construye el shape DGII — manda los 4 campos explícitamente. Los respetamos sin cuestionar.
Ambos modos son el mismo format: 'digimart' — la diferencia es solo si
incluyes esos 4 campos o los omites. Puedes mezclar: enviar Emisor
explícito y dejar que calculemos Totales, o cualquier combinación.
{
"format": "digimart",
"clientRequestId": "factura-001-2026-06-15",
"payload": {
"Encabezado": {
"Version": "1.0",
"IdDoc": {
"eNCF": "E310000000001",
"IndicadorMontoGravado": "0",
"TipoIngresos": "01",
"TipoPago": "1"
},
"Comprador": {
"RNCComprador": "101672919",
"RazonSocialComprador": "Cliente B2B SRL"
}
},
"DetallesItems": {
"Item": [
{
"NumeroLinea": 1,
"IndicadorFacturacion": "1",
"NombreItem": "Servicio profesional",
"CantidadItem": 1,
"PrecioUnitarioItem": 10000,
"MontoItem": 10000
}
]
}
}
}Puedes sobrescribir cualquiera de los 4 auto-derivados si los envías explícitamente — respetamos tu valor. Útil cuando quieres fijar un Emisor diferente al del tenant o ya calculaste los Totales por tu lado.
IndicadorFacturacion de cada item se valida strict. Solo
aceptamos: "1" (ITBIS 18%), "2" (ITBIS 16%), "3" (ITBIS 0%),
"4" (Exento). Cualquier otro valor → 400 con la línea exacta del
item ofensivo. Si lo omites en un item, lo tratamos como exento.
clientRequestId (idempotency)
Si tu sistema reintenta la misma factura (timeout de red, retry interno,
etc.), envía el mismo clientRequestId en cada intento. Detectamos
la duplicación y devolvemos el envío original en vez de procesar dos veces.
- Recomendación: usa un id determinístico desde TU sistema
(ej.
tu_factura_id+ fecha). - Longitud: 1-120 caracteres.
- Si lo omites, no hay protección contra retries duplicados.
Response — 202 Accepted
{
"dgiiLogId": "a3f7b8c9-d1e2-3f4g-5h6i-7j8k9l0m1n2o",
"status": "queued",
"environment": "ecf",
"billable": true,
"message": "Invoice queued for processing"
}| Campo | Descripción |
|---|---|
dgiiLogId | UUID del registro. Úsalo para consultar estado (ver abajo). |
status | Siempre "queued" inicialmente. Cambia a "Aceptado", "Rechazado", etc. después de la respuesta de DGII. |
environment | Ambiente al que se envía (determinado por la key). |
billable | true solo si environment === "ecf". Pruebas no facturan. |
Response — 400 Bad Request
{
"message": "xml is required when format=\"xml\"",
"statusCode": 400
}Otros errores 400 comunes:
payload is required when format="digimart"format must be one of: xml, digimart
Idempotency response
Si reutilizas un clientRequestId:
{
"dgiiLogId": "a3f...",
"status": "queued",
"deduped": true,
"message": "Existing submission returned (idempotency key matched)"
}El deduped: true te dice que NO procesamos de nuevo — te devolvimos el
envío original.
GET /invoices/:trackOrId
Consulta el estado de un comprobante. Acepta tanto el dgiiLogId (que
te dimos al hacer POST) como el trackId (que DGII asigna después).
GET /api/v1/fe/invoices/{trackOrId}
Authorization: Bearer <JWT>Scope requerido: invoices:read
Response — 200 OK (procesado)
{
"dgiiLogId": "a3f7b8c9-...",
"trackId": "DGII-2026-06-15-12345",
"environment": "ecf",
"status": "Aceptado",
"qrCodeUrl": "https://ecf.dgii.gov.do/ConsultaTimbre?...",
"securityCode": "abc123",
"ncfExpiration": "31-12-2026",
"fechaHoraFirma": "2026-06-15T14:23:11Z",
"response": { /* respuesta completa de DGII */ },
"submittedAt": "2026-06-15T14:23:10Z",
"updatedAt": "2026-06-15T14:23:13Z",
"clientRequestId": "factura-001-2026-06-15"
}| Campo | Descripción |
|---|---|
status | "Aceptado", "Aceptado Condicional", "Rechazado", "En Proceso", o null (todavía no procesado) |
qrCodeUrl | URL del QR oficial para incluir en tu PDF de factura |
securityCode | Código de seguridad asignado por DGII |
ncfExpiration | Fecha en que expira la secuencia NCF |
response | Respuesta cruda de DGII (para debugging) |
Estados posibles
| Estado | Significado | ¿Es facturable? |
|---|---|---|
"Aceptado" | DGII aceptó el e-CF sin observaciones. | ✅ Sí |
"Aceptado Condicional" | DGII aceptó con observaciones (revisa mensajes). | ✅ Sí |
"Rechazado" | DGII rechazó. Mira response.mensajes para detalles. | ❌ No |
"En Proceso" | DGII todavía procesando. Vuelve a consultar en ~30s. | ❌ Todavía no |
null o "queued" | Aún no enviado a DGII. Vuelve a consultar en ~5s. | ❌ Todavía no |
Response — 200 OK (no encontrado)
{
"found": false
}Significa que el trackOrId no corresponde a ningún envío tuyo.
GET /invoices/:trackOrId/xml
Devuelve el XML que generamos y/o firmamos para un envío específico. Sirve para dos cosas concretas:
- Debug de rechazos "XML Inválido" — DGII responde con código 400 y un mensaje genérico cuando el XSD no valida. Pidiendo este endpoint ves exactamente el documento que mandamos por ti y puedes detectar el campo problemático (formato de fecha mal, monto fuera de rango, NCF con prefijo equivocado, etc.).
- Archivar el comprobante firmado — el XML con la sección
<Signature>es la prueba legal del e-CF. Para tus copias internas o un sistema documental, este endpoint te devuelve esa pieza.
GET /api/v1/fe/invoices/{trackOrId}/xml
Authorization: Bearer <JWT>Scope requerido: invoices:read
Path params
| Campo | Tipo | Descripción |
|---|---|---|
trackOrId | string | dgiiLogId (UUID que devolvió POST) o trackId (id de DGII). |
Response — 200 OK
{
"dgiiLogId": "9e5c8010-fa9a-4190-9c3c-d727aa99b26c",
"environment": "testecf",
"status": "error",
"source": "signed",
"xml": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ECF ...>...</ECF>"
}| Campo | Descripción |
|---|---|
source | "signed" si el worker ya firmó (caso normal); "original" si mandaste format="xml" y aún no se ha firmado; null si todavía está queued. |
xml | El documento XML completo como string. null cuando source = null. |
status | Mismo status que devuelve GET /invoices/:trackOrId, replicado por conveniencia. |
Para format="digimart" sólo existe signedXml (el JSON lo
convertimos a XML y lo firmamos en un solo paso). Si todavía está en cola
(status="queued") el xml será null — vuelve a pedirlo en unos
segundos.
Response — 200 OK (no encontrado)
{
"found": false
}GET /invoices
Lista paginada del historial de envíos del tenant. Solo retorna envíos
hechos a través de esta API pública (source = 'fe-api').
GET /api/v1/fe/invoices?page=1&limit=50&from=2026-06-01&to=2026-06-30
Authorization: Bearer <JWT>Scope requerido: invoices:read
Query params
| Param | Tipo | Default | Descripción |
|---|---|---|---|
page | number | 1 | Página (1-indexed) |
limit | number | 50 | Items por página (max 200) |
from | ISO date | — | Filtro: createdAt >= from |
to | ISO date | — | Filtro: createdAt <= to |
status | string | — | (reservado para uso futuro) |
Response — 200 OK
{
"page": 1,
"limit": 50,
"total": 247,
"items": [
{
"dgiiLogId": "a3f...",
"trackId": "DGII-...",
"environment": "ecf",
"status": "Aceptado",
"qrCodeUrl": "...",
"submittedAt": "2026-06-15T14:23:10Z",
"updatedAt": "2026-06-15T14:23:13Z",
"clientRequestId": "factura-001-2026-06-15"
}
/* ... más items ... */
]
}Ordenado por submittedAt DESC (más recientes primero).