Saltar a contenido

API de facturación electrónica — República Dominicana (e-CF / DGII)

Guía de integración para enviar comprobantes fiscales electrónicos a la DGII a través de esta API. Usted envía JSON; nosotros construimos el XML del e-CF, lo firmamos digitalmente, lo transmitimos a la DGII, resolvemos su estado y generamos la representación impresa.

No necesita conocer el formato XML de la DGII ni tener certificado digital propio: ambos los resolvemos nosotros. Y si su sistema ya genera el XML e-CF, envíelo tal cual: lo firmamos y lo transmitimos igual. Vea passthrough-xml.md.

Documentos disponibles

Tipo Documento Cuándo se usa Documentación
31 Factura de Crédito Fiscal Electrónica Ventas a contribuyentes (B2B). Sustenta crédito fiscal del comprador. ecf-31.md
32 Factura de Consumo Electrónica Ventas a consumidor final (B2C). ecf-32.md
33 Nota de Débito Electrónica Aumenta el valor de un comprobante ya emitido. ecf-33.md
34 Nota de Crédito Electrónica Anula, corrige o disminuye un comprobante ya emitido. ecf-34.md
41 Comprobante de Compras Electrónico Compras a proveedores que no emiten e-CF, con retención. ecf-41.md
43 Gastos Menores Electrónico Gastos de caja chica sin comprobante del vendedor. ecf-43.md
44 Regímenes Especiales Electrónico Ventas exentas a zonas francas, diplomáticos y similares. ecf-44.md
45 Gubernamental Electrónico Ventas al Estado. ecf-45.md
46 Comprobante de Exportaciones Electrónico Ventas al exterior, gravadas a tasa 0%. ecf-46.md
47 Pagos al Exterior Electrónico Pagos a beneficiarios extranjeros, con retención de ISR. ecf-47.md

Los diez tipos se validan con el mismo rigor: cuadratura de totales, totales condicionales según el indicador de facturación, coherencia del ISC y reglas de todos los bloques (subtotales informativos, descuentos y recargos globales, otra moneda, paginación). Si algo no cuadra, la respuesta es un 422 con el detalle por campo: no se crea ningún documento ni se envía nada a la DGII, y su número sigue libre para el comprobante corregido.

Documentos de apoyo: - campos-comunes.md — nodos y campos compartidos por todos los tipos. - catalogos.md — valores permitidos (tipos de ingreso, formas de pago, unidades…). - estados.md — códigos de estado internos y el ciclo de vida del e-CF. - Cómo responde la API — la forma única de toda respuesta, éxito o error: status, message y una sola carga (document o data), con un ejemplo real de cada caso. - tokens-y-permisos.md — el token, el RNC del emisor (la empresa en cuyo nombre actúa), las empresas que tiene habilitadas y los permisos. Léalo si recibe un 401, un 403 o un 422 al empezar. - consultas-dgii.md — consultas en vivo: estado de un e-CF, validez fiscal de lo que recibe, y los envíos de un e-CF. Todos los recursos de consulta, juntos, en «Consultar qué pasó con un documento». - mis-documentos.md — el detalle de un e-CF con su XML firmado y su PDF en base64, y el listado paginado (con filtros) de sus e-CF emitidos. Pensado para un portal de consulta o para una pantalla «Mis documentos» dentro de su propio sistema. - passthrough-xml.md — enviar su propio XML en vez del JSON canónico. - entrega-al-receptor.md — cómo llega el e-CF al receptor y qué responde. - recepcion.md — los e-CF que recibe de sus proveedores: el listado y la aprobación comercial. - directorio.md — a quién puede enviarle e-CF y por qué URL. - numeracion.md — la numeración la lleva su sistema: cómo enviar el e-NCF, qué validamos del número y qué respondemos si reenvía uno que ya existe. - anulacion-secuencias.md — anular rangos de e-NCF no utilizados. - La colección de Postman — lista para importar: todos los endpoints con sus cuerpos de ejemplo. Al emitir guarda el e-NCF y el identificador del documento en variables, así que las consultas funcionan sin copiar y pegar. - Los JSON de esta documentación, listos para copiar:

| Tipo | Mínimo | Completo / variantes |
|---|---|---|
| 31 Factura de Crédito Fiscal | [mínimo](ejemplos/ecf-31-minimo.json) | [completo](ejemplos/ecf-31-completo.json) |
| 32 Factura de Consumo | [mínimo](ejemplos/ecf-32-minimo.json) | [dos productos](ejemplos/ecf-32-dos-productos.json) · [varios productos con descuento](ejemplos/ecf-32-varios-productos-con-descuento.json) · [con bono](ejemplos/ecf-32-bono.json) |
| 33 Nota de Débito | [mínimo](ejemplos/ecf-33-minimo.json) | |
| 34 Nota de Crédito | [mínimo](ejemplos/ecf-34-minimo.json) | |
| 41 Compras | [mínimo](ejemplos/ecf-41-minimo.json) | |
| 43 Gastos Menores | [mínimo](ejemplos/ecf-43-minimo.json) | |
| 44 Regímenes Especiales | [mínimo](ejemplos/ecf-44-minimo.json) | |
| 45 Gubernamental | [mínimo](ejemplos/ecf-45-minimo.json) | |
| 46 Exportaciones | [mínimo](ejemplos/ecf-46-minimo.json) | |
| 47 Pagos al Exterior | [mínimo](ejemplos/ecf-47-minimo.json) | |

Todos los ejemplos de esta documentación se verifican automáticamente contra la API en cada ejecución de la suite de pruebas: si un ejemplo dejara de ser válido, la compilación falla. Puede copiarlos con la confianza de que funcionan.

Cada ejemplo trae un e-NCF de muestra (E310000000001…) y la fecha de vencimiento de su secuencia: cámbielos por su siguiente número antes de enviarlo. Si repite un e-NCF que ya envió, no se crea otro documento: se le responde con el que ya existe ("duplicate": true, ver numeracion.md).

Autenticación

Cada llamada lleva dos datos: quién llama (el token) y en nombre de qué empresa actúa (el RNC del emisor, que la propia petición declara).

  • Authorization: Bearer <su-token> — el token es de su integración (su ERP, su plataforma), no de una empresa: una misma integración puede atender a varias. Lleva declarado qué puede hacer: el que factura no tiene por qué poder cambiar el certificado de firma ni anular secuencias. Los permisos y los perfiles están en tokens-y-permisos.md.
  • El RNC del emisor — el RNC (9 dígitos) o la cédula (11) de la empresa a la que se refiere la petición. No hay ninguna cabecera para esto: se declara donde ya viaja el emisor, y la API comprueba que su token esté vinculado a esa empresa.
La petición es… El RNC va en…
un cuerpo JSON (emitir, anular secuencias, aprobación comercial, certificado) emisor.rnc
un cuerpo XML (passthrough) <RNCEmisor> del e-CF
una consulta sin cuerpo (GET) el parámetro emisor_rnc
la consulta de un documento suyo (GET /documents/{identificador}…) el prefijo del identificador de la ruta: no hace falta emisor_rnc (cómo se arma)

Es obligatorio en todas las llamadas que actúan en nombre de una empresa (todas las de la tabla de endpoints, salvo GET /api/v1/companies y GET /api/v1/statuses).

POST /api/v1/documents/31 HTTP/1.1
Host: endpoint-test.emision.do
Authorization: Bearer <su-token>
Content-Type: application/json
Accept: application/json

{ "emisor": { "rnc": "133802074" }, "id_doc": { "e_ncf": "E310000000001", … }, … }
GET /api/v1/documents/133802074E310000000001 HTTP/1.1
Host: endpoint-test.emision.do
Authorization: Bearer <su-token>
Accept: application/json

(Aquí no hay emisor_rnc: la empresa es 133802074, el prefijo del identificador. Una consulta que no es de un documento concreto —el listado, los recibidos…— sí lo lleva: GET /api/v1/documents?emisor_rnc=133802074.)

Qué responde si algo falla (todo ello antes de crear nada):

Código Cuándo Qué hacer
401 El token falta, está mal copiado o fue revocado. Revise la cabecera Authorization.
422 No se declara el RNC del emisor, no tiene forma de RNC, la petición declara dos distintos o el XML no se puede leer (ni admite DOCTYPE): «Falta indicar la empresa.» / «El RNC de la empresa no es válido.» / «La petición declara más de una empresa.» / «No se pudo leer el RNC de la empresa.» Declárelo una sola vez (emisor.rnc, RNCEmisor o emisor_rnc).
403 «Este token no puede actuar sobre esa empresa.»: esa empresa no está habilitada para ese token. Revise el RNC; si es correcto, pida a quien le entregó la credencial que habilite esa empresa.

Los mensajes completos están en tokens-y-permisos.md. Los tres llevan el mismo envelope que el resto de la API y explican el porqué en data.detalle (cómo leerlos).

En una petición con cuerpo, emisor.rnc ya era obligatorio (dice de quién es el comprobante); ahora además es el dato que identifica la empresa, así que no se manda dos veces. Un sistema que atiende a varias empresas no puede emitir por descuido bajo una con los datos de otra: el comprobante sale siempre bajo el RNC que declara, y sólo si su token está vinculado a él.

No hay endpoint de login. El token se lo entrega quien da de alta su integración. Para comprobar que el suyo funciona y ver qué empresas tiene habilitadas, GET /api/v1/companies: devuelve el RNC, la razón social y el ambiente de cada una, o 401 si el token no vale.

Ciclo de vida de un e-CF

  Su sistema                                 Esta API                          DGII
      │                                          │                                │
      ├── POST /documents/31 ───────────────────►│  (con SU e-NCF en id_doc)      │
      │                                          ├─ valida el JSON y el e-NCF     │
      │◄─ 202 received + identificador ──────────┤                                │
      │                                          ├─ construye + firma el XML      │
      │                                          ├───── transmite ───────────────►│
      │                                          │◄───── TrackID ─────────────────┤
      │                                          │   in_process_dgii              │
      │                                          ├── consulta el resultado ──────►│
      │                                          │◄── aceptado / rechazado ───────┤
      │                                          ├─ genera la representación impresa (PDF)
      ├── GET /documents/{identificador} ───────►│                                │
      │◄─ accepted_dgii + archivos ──────────────┤                                │

El envío es asíncrono: la respuesta 202 confirma que recibimos y validamos el comprobante y su número, y que quedó en cola; no que la DGII lo haya aceptado. Esa misma respuesta trae el identificador del documento: con él lo consulta (GET /api/v1/documents/{identificador}) hasta que tenga su estado final y, ya aceptado, recoge ahí mismo su XML firmado y su PDF (más abajo).

Una variante: la factura de consumo bajo RD$250.000 (tipo 32). Se transmite como resumen (RFCE) y la DGII contesta el resultado en la propia llamada, sin TrackID: el documento pasa de received directamente a accepted_dgii (o rejected_dgii), sin pasar por in_process_dgii en el caso normal, y su track_id queda null para siempre. Es lo esperado, no un fallo: se sigue por su identificador. Todos los demás tipos, y el 32 de RD$250.000 o más, reciben su TrackID al transmitirse —pasan a in_process_dgii con él— y lo conservan en los estados finales. Detalle en ecf-32.md.

La numeración la lleva su sistema

Esta API no numera: cada ERP o Facturador lleva su propia numeración por tipo de documento y envía el e-NCF completo en id_doc.e_ncf (E + tipo + 10 dígitos, p. ej. E310000000001), junto con id_doc.fecha_vencimiento_secuencia en los tipos que la llevan (todos menos el 32 y el 34). Antes de enviar a la DGII comprobamos lo básico —estructura y tipo, que no esté anulado ni vencido y, si usted registró su autorización, que caiga dentro de ella— y, si algo falla, responde un 422 sin crear nada.

Un folio tiene un solo documento. Si reenvía uno que ya existe no se crea otro ni se vuelve a transmitir: se le responde según el estado del que ya está (200 si ya se procesó o está en espera de la DGII, 409 si fue rechazado, 202 si se reintenta por un fallo nuestro), con "duplicate": true. Vale lo mismo si el folio lo envió antes otro sistema de la misma empresa: el documento es de la empresa. Ese reenvío devuelve el detalle del documento sin exigir documents:read (la idempotencia lo necesita; ver tokens-y-permisos.md). Todo el detalle, en numeracion.md.

El identificador del documento

El e-NCF sólo es único dentro de un emisor: cada empresa lleva su propia numeración, así que dos empresas pueden tener, las dos, su E310000000001. Para consultar un comprobante hace falta un dato que lo identifique sin ambigüedad: su identificador, que es el RNC (o la cédula) del emisor seguido del e-NCF, pegados y sin separadores.

{RNC o cédula del emisor}{e-NCF}
Emisor e-NCF Identificador
RNC 133802074 (9 dígitos) E320000000303 133802074E320000000303
Cédula 00112345678 (11 dígitos) E310000000042 00112345678E310000000042
  • Forma exacta: 9 u 11 dígitos, la E en mayúscula y 12 dígitos más (tipo y consecutivo del e-NCF): ^(?:[0-9]{9}|[0-9]{11})E[0-9]{12}$. Sin guiones ni espacios: 1-33-80207-4E320000000303 no es un identificador y la ruta responde 404.
  • Es el mismo esquema con que la DGII nombra los adjuntos de un e-CF (RNC + e-NCF). Por eso es también el nombre de los archivos que devuelve el detalle (133802074E320000000303.xml, 133802074E320000000303.pdf).
  • Usted lo conoce antes de enviar: es el RNC de su empresa y el e-NCF que usted mismo numeró. No hay que esperar ninguna respuesta ni guardar nada para poder consultarlo. Aun así, todas las respuestas que hablan de un documento lo traen en el campo identificador: el 202 de emitir (JSON y XML), el detalle, el reenvío de un folio que ya existe, cada elemento del listado y las respuestas de dgii-status y track-ids.
  • La empresa es el prefijo. Por eso GET /api/v1/documents/{identificador} y sus dos consultas a la DGII no piden emisor_rnc. Si lo manda igualmente, tiene que ser el mismo RNC; si es otro —aunque sea otra empresa de su integración— responde 422 «La petición declara más de una empresa.»: nunca se elige una en silencio. Su token tiene que seguir habilitado para esa empresa (tokens-y-permisos.md).

¿Venía consultando por el e-NCF a secas, con emisor_rnc? Esa forma ya no existe y responde 404: se consulta por identificador, que es el mismo e-NCF con el RNC del emisor delante. Y ya no hay recursos aparte para bajar el XML ni el PDF mediante una URL temporal: vienen en el propio detalle del documento.

Endpoints

Método Ruta Descripción
GET /api/v1/companies Las empresas que su token tiene habilitadas (permiso companies:read). No declara empresa.
POST /api/v1/documents/{tipo} Emite un e-CF. {tipo} = 31, 32, 33, 34, 41, 43, 44, 45, 46, 47.
GET /api/v1/documents Listado paginado de sus e-CF emitidos, con filtros.
GET /api/v1/documents/{identificador} Detalle de un e-CF emitido: su estado y, si ya tiene validez fiscal, su XML firmado y su PDF en base64.
GET /api/v1/received-documents e-CF que otros emisores le enviaron. En cada elemento, emisor_rnc es el RNC del proveedor que lo envió; su empresa es la del parámetro emisor_rnc de la consulta.
POST /api/v1/received-documents/{eNCF}/commercial-approval Acepta o rechaza comercialmente un e-CF recibido.
GET /api/v1/documents/{identificador}/dgii-status Estado del e-CF consultado en vivo a la DGII.
GET /api/v1/documents/{identificador}/track-ids Envíos (TrackID) de ese e-CF ante la DGII.
GET /api/v1/received-documents/{eNCF}/dgii-status Validez fiscal de un e-CF que usted recibió.
GET /api/v1/directory · /api/v1/directory/{rnc} Directorio de emisores y sus URL de recepción.
GET /api/v1/voided-sequences · POST Consulta y anulación de rangos de e-NCF.
GET /api/v1/certificate Estado de su certificado digital (sólo firma propia).
POST /api/v1/certificate Sube o reemplaza su certificado .p12.
GET /api/v1/statuses Catálogo de estados internos. No declara empresa.

Todas las rutas de esta tabla, salvo /api/v1/companies y /api/v1/statuses, actúan en nombre de una empresa y exigen que se declare su RNC (emisor.rnc, RNCEmisor o emisor_rnc: Autenticación); las que piden un documento suyo lo declaran con el prefijo de su identificador. Los permisos que pide cada una están en tokens-y-permisos.md.

Consultar qué pasó con un documento

Todos los recursos de consulta son GET, actúan en nombre de una empresa y sólo devuelven datos de esa empresa. Los de un documento suyo se piden por su identificador y la empresa es su prefijo; los demás llevan el RNC en el parámetro emisor_rnc (salvo los dos últimos de la tabla, que no declaran empresa). Un identificador que no existe, o que es de otra empresa, responde siempre 404.

Hay una sola forma de consultar un documento: por su identificador. El detalle trae los archivos del e-CF cuando ya es válido; no hay recursos aparte para descargar el XML ni el PDF, ni URL firmadas, ni un segundo paso.

Recurso Para qué sirve Permiso Cuándo usarlo
GET /api/v1/documents/{identificador} Estado y detalle de un e-CF emitido, según nuestro registro: estado, TrackID (si lo hay), errores de la DGII, entrega_al_receptor y correo_al_comprador; y, cuando el e-CF ya tiene validez fiscal, sus archivos en base64 (PDF, XML firmado y respuesta de la DGII). documents:read El de siempre, tras emitir: sondéelo hasta un estado final y, ya aceptado, recoja ahí sus archivos. No llama a la DGII. mis-documentos.md
GET /api/v1/documents Listado paginado de sus e-CF emitidos, con filtros from, to, status, type, buyer. documents:read Pantallas «Mis documentos», conciliaciones, buscar lo que quedó sin resolver (status=unresolved). mis-documentos.md
GET /api/v1/documents/{identificador}/dgii-status Le pregunta a la DGII en vivo por ese e-CF y la compara con nuestro registro (discrepancias). Sirve también para el resumen de la factura de consumo. documents:read Un documento en unresolved, o la duda de si lo que tenemos es lo que la DGII tiene. consultas-dgii.md
GET /api/v1/documents/{identificador}/track-ids Los TrackID que la DGII registra para ese e-CF (más de uno = se transmitió más de una vez). documents:read Auditar envíos duplicados. No aplica al resumen de la factura de consumo, que no tiene TrackID. consultas-dgii.md
GET /api/v1/received-documents Los e-CF que sus proveedores le enviaron (los últimos 100) y si se les acusó recibo. received:read Ver qué ha recibido. recepcion.md
GET /api/v1/received-documents/{eNCF}/dgii-status Si un e-CF recibido tiene validez fiscal ante la DGII (la firma no lo demuestra). received:read Antes de dar por bueno un crédito fiscal. consultas-dgii.md
GET /api/v1/voided-sequences Las anulaciones de rangos de e-NCF (ANECF) y si la DGII las aceptó. sequences:read Comprobar una anulación. anulacion-secuencias.md
GET /api/v1/statuses El catálogo de estados internos. No declara empresa ni exige permiso. — Programar contra los status. estados.md
GET /api/v1/companies Las empresas que su token tiene habilitadas. No declara empresa. companies:read Comprobar que el token funciona y qué RNC puede usar.

Ejemplo, tras emitir el E320000000043 de la empresa 133802074 (su identificador es 133802074E320000000043):

curl -s "https://endpoint-test.emision.do/api/v1/documents/133802074E320000000043" \
  -H "Authorization: Bearer <su-token>" \
  -H "Accept: application/json"

Cómo sondear. Consulte GET /api/v1/documents/{identificador} cada pocos segundos (por ejemplo, cada 3 a 5 s al principio y espaciando después) hasta que status sea final (estados.md): aceptado (accepted_dgii, conditionally_accepted_dgii), rejected_dgii, rejected_validation o unresolved. La DGII suele resolver en segundos. No espere un TrackID: llega en cuanto se transmite en la mayoría de los tipos (el documento entra en in_process_dgii con él y los estados finales lo conservan), y no existe nunca en la factura de consumo (32) por debajo de RD$250.000. Mientras el documento no es válido la respuesta es pequeña (archivos va vacío); al quedar aceptado trae los archivos. La representación impresa se genera unos segundos después de la aceptación: si archivos aún no trae el pdf, consulte de nuevo. Cuando ya tiene el XML y el PDF, deje de consultar y guárdelos: cada respuesta con archivos pesa lo que ellos (el base64 añade un tercio).

Recepción: usted también recibe comprobantes

Como emisor electrónico, la DGII le exige poder recibir e-CF de sus proveedores. Eso lo resolvemos nosotros: exponemos los servicios que el estándar define (/fe/recepcion/api/ecf y /fe/aprobacioncomercial/api/ecf), respondemos el Acuse de Recibo firmado y registramos lo recibido. Usted sólo consulta GET /api/v1/received-documents y, si quiere, aprueba o rechaza comercialmente la transacción:

POST /api/v1/received-documents/E310000000055/commercial-approval
// Authorization: Bearer <su-token>
{
  "emisor": { "rnc": "133802074" },                        // la empresa que aprueba o rechaza
  "estado": 2,                                             // 1 acepta, 2 rechaza
  "motivo_rechazo": "La mercancía no corresponde al pedido" // obligatorio si rechaza (máx. 250)
}

El acuse de recibo (automático) sólo dice que el comprobante llegó y es válido en forma. La aprobación comercial es su conformidad con la operación, y es opcional.

El listado, sus campos, los motivos de no recepción y la aprobación comercial, completos: recepcion.md.

Certificado digital

Si la empresa firma con su propio certificado, súbalo una vez (por cada empresa):

curl -X POST https://endpoint-test.emision.do/api/v1/certificate \
  -H "Authorization: Bearer <su-token>" \
  -F "emisor[rnc]=<rnc de la empresa>" \
  -F "[email protected]" \
  -F "password=<clave del .p12>"

Antes de aceptarlo verificamos que abra con esa contraseña y que esté vigente. El titular del certificado normalmente no es su empresa, sino la persona física que ustedes registren como "Firmante Autorizado" en su Oficina Virtual de la DGII (Delegación de Roles); esa autorización la valida la propia DGII al recibir cada e-CF, no nosotros. El archivo queda cifrado y nunca se devuelve; con GET /api/v1/certificate consulta su vigencia y cuántos días faltan para que venza.

Si firma con nuestro certificado de proveedor no tiene que subir nada: basta la Delegación de Roles ("Firmante Autorizado") a nuestro favor en su Oficina Virtual de la DGII.

Cómo responde la API

Toda respuesta JSON de /api/* tiene la misma forma: la que ya conoce del 202 de emitir. Un éxito y un error se leen igual, y da igual quién los produzca: nuestro código, la autenticación, el control de permisos o el servidor. No hay un formato distinto para «cuando falla».

{
  "status": "success",
  "message": "Consulta exitosa.",
  "data": { "…": "…" }
}

La regla

Clave Cuándo está Qué es
status Siempre "success" si el código HTTP es menor de 400; "error" si es 400 o más.
message Siempre Una frase en español, nunca vacía: qué pasó, en una línea para mostrar a una persona.
document Cuando la respuesta trata de un e-CF suyo: el 202 de emitir y su rechazo de validación, el detalle, el reenvío de un folio. El e-CF: su estado (status_code, status, status_description) y lo que corresponda.
data En todo lo demás: lecturas, operaciones y errores. La carga de la respuesta o, en un error, su explicación.
meta Sólo en el listado, junto a data. La paginación.
duplicate Sólo en el reenvío de un folio que ya existe. true, junto a document.

Como máximo una carga —document o data, nunca las dos— y nada más va suelto en la raíz: no hay un errors aparte, ni un 503 con un formato distinto del de un 404.

Cómo leer cualquier respuesta. Lea siempre status y message. Si es un error, la explicación está en data.detalle; y si la respuesta trae document (la validación de una emisión, un reenvío), en document.errors. Para decidir qué hacer, use el código HTTP y los status de estados.md, no el texto de message: las pocas frases que sí puede reconocer se dicen expresamente en esta guía (por ejemplo, «Documento enviado y aprobado anteriormente.»).

No confunda el status de la raíz (success / error: ¿salió bien la petición?) con document.status (el estado del e-CF: received, accepted_dgii…).

Qué trae cada respuesta, de un vistazo

Respuesta Código message Dónde está el detalle
Lectura correcta 200 «Consulta exitosa.» data (o document si es un e-CF suyo); meta en el listado
Emitir un e-CF 202 «e-CF recibido y encolado para envío a la DGII.» document
Reenvío de un folio que ya existe 200 / 202 / 409 Según el estado del documento document + "duplicate": true
Una operación (anular, subir el certificado, aprobar) 201 / 202 Propio de cada operación data
Token ausente o inválido 401 «No se pudo autenticar la petición.» data.detalle
Empresa no habilitada para el token 403 «Este token no puede actuar sobre esa empresa.» data.detalle
Falta un permiso 403 «El token no tiene permiso para esta operación.» data.detalle, data.permisos_requeridos, data.permisos_del_token
El documento no existe 404 «No se encontró el documento.» data.detalle
La ruta no existe 404 «La ruta solicitada no existe.» data.detalle
Método no admitido 405 «El método no está permitido para esta ruta.» data.detalle, data.metodos_permitidos (y la cabecera Allow)
No se declara la empresa, o el cuerpo no se lee 422 «Falta indicar la empresa.», «El cuerpo de la petición no es un JSON válido.»… data.detalle
Validación de emitir un e-CF 422 «Los datos enviados no son válidos.» o «El e-CF no se pudo recibir.» document.errors y document.detailed_errors
Validación de otra cosa (un filtro, una anulación, el certificado) 422 «Los datos enviados no son válidos.» data.detalle y data.errores (por campo)
Demasiadas peticiones 429 «Demasiadas peticiones.» data.detalle, data.reintentar_en_segundos (y Retry-After)
Error nuestro 500 «Ocurrió un error interno.» data.detalle
La DGII no responde 503 «El servicio de consulta de la DGII no está disponible en este momento.» data.detalle

Cada caso, con su respuesta real, a continuación.

Lectura — 200 OK

Toda consulta que no es de un e-CF concreto devuelve su carga en data y «Consulta exitosa.» como message. El listado añade meta con la paginación (mis-documentos.md).

GET /api/v1/companies HTTP/1.1
Authorization: Bearer <su-token>
{
  "status": "success",
  "message": "Consulta exitosa.",
  "data": [
    {
      "rnc": "133802074",
      "razon_social": "EMISOR DE PRUEBA",
      "nombre_comercial": null,
      "ambiente": "testecf",
      "habilitada": true
    }
  ]
}

Una consulta que no encuentra nada no es un error si la pregunta tiene respuesta: un listado vacío es "data": []; el directorio dice "receptor_electronico": false. Lo que no existe de verdad (un documento) es un 404.

Éxito — 202 Accepted

{
  "status": "success",
  "message": "e-CF recibido y encolado para envío a la DGII.",
  "document": {
    "document_id": 1234,
    "ecf_type_code": "31",
    "e_ncf": "E310000000001",
    "identificador": "133802074E310000000001",
    "track_id": null,
    "status_code": 1,
    "status": "received",
    "status_description": "El e-CF fue recibido, validado y encolado para su envío a la DGII."
  }
}

identificador es el RNC del emisor seguido del e-NCF (cómo se arma): es el dato con el que consulta este documento a partir de ahora.

track_id llega siempre null en el 202, en todos los tipos: la transmisión a la DGII ocurre en segundo plano, después de responderle, y en ese instante no puede existir todavía un TrackID. No lo espere para consultar: el documento se identifica por su identificador (GET /api/v1/documents/{identificador}). Cuando se transmite, los tipos que pasan por el servicio de recepción de e-CF reciben su TrackID: el documento entra en in_process_dgii con él y los estados finales lo conservan (si se transmite más de una vez hay varios, y todos se ven en GET /api/v1/documents/{identificador}/track-ids). En la factura de consumo (32) por debajo de RD$250.000 (resumen RFCE) el TrackID no existe nunca, ni siquiera con el documento ya aceptado: track_id sigue null y es lo normal (ecf-32.md).

Consulta de estado — 200 OK

El detalle de un e-CF suyo también es un document (no un data):

{
  "status": "success",
  "message": "Consulta exitosa.",
  "document": {
    "document_id": 1234,
    "ecf_type_code": "31",
    "e_ncf": "E310000000001",
    "identificador": "133802074E310000000001",
    "track_id": "a1b2c3d4-...",
    "status_code": 4,
    "status": "accepted_dgii",
    "status_description": "La DGII aceptó el e-CF: tiene validez fiscal.",
    "is_valid_fiscally": true,
    "security_code": "aB12cd",
    "issued_at": "27-07-2026",
    "signed_at": "27-07-2026 09:15:03",
    "total_amount": "1180.00",
    "total_itbis": "180.00",
    "sequence_reusable": null,
    "errors": null,
    "entrega_al_receptor": {
      "via": "printed",
      "descripcion": "El receptor no es electrónico: le corresponde la representación impresa.",
      "acuse_recibido": null,
      "mensajes": [],
      "entregado_en": null
    },
    "correo_al_comprador": {
      "estado": "sent",
      "destinatario": "[email protected]",
      "motivo": null,
      "enviado_en": "27-07-2026 09:15:40"
    },
    "archivos": [
      {
        "tipo": "pdf",
        "nombre": "133802074E310000000001.pdf",
        "mime": "application/pdf",
        "contenido_base64": "JVBERi0xLjQK"
      },
      {
        "tipo": "xml",
        "nombre": "133802074E310000000001.xml",
        "mime": "application/xml",
        "contenido_base64": "PD94bWwgdmVyc2lvbj0iMS4wIj8+"
      },
      {
        "tipo": "respuesta_dgii",
        "nombre": "133802074E310000000001-respuesta-dgii.json",
        "mime": "application/json",
        "contenido_base64": "eyJjb2RpZ28iOjEsImVzdGFkbyI6IkFjZXB0YWRvIn0="
      }
    ]
  }
}

(Los contenido_base64 del ejemplo están abreviados.)

track_id es null mientras no se haya transmitido y para siempre en la factura de consumo (32) por debajo de RD$250.000, que va como resumen y no tiene TrackID. En los demás tipos llega al transmitirse —el documento entra en in_process_dgii con él— y los estados finales lo conservan (estados.md). entrega_al_receptor y correo_al_comprador se explican en entrega-al-receptor.md.

archivos es una lista que sólo se llena cuando el documento tiene validez fiscal (accepted_dgii o conditionally_accepted_dgii); en cualquier otro estado es []. Cada elemento trae tipo, nombre, mime y contenido_base64, en este orden de tipos: pdf (la representación impresa), xml (el e-CF completo firmado, lo que usted debe conservar), resumen_rfce (sólo en la factura de consumo bajo RD$250.000: el resumen que se transmitió a la DGII) y respuesta_dgii (la respuesta final de la DGII, si consta). Todo en línea, en base64: no hay URL que caduque. Qué trae cada tipo, cuándo falta alguno y qué es archivos_no_disponibles, en mis-documentos.md.

Reenvío de un folio que ya existe — 200, 202 o 409

No es un error de validación: ese número ya tiene su documento. Se le responde con el estado del documento que existe, marcado con "duplicate": true, sin crear ni transmitir nada nuevo. Lleva document (el mismo detalle que la consulta de estado, sin los archivos), como cualquier respuesta que habla de un e-CF suyo, incluso cuando el código es un 409 y el status de la raíz es error:

{
  "status": "success",
  "message": "Documento enviado y aprobado anteriormente. El e-NCF E310000000001 ya fue procesado: la DGII lo aceptó y tiene validez fiscal. No se envió de nuevo.",
  "duplicate": true,
  "document": { "e_ncf": "E310000000001", "identificador": "133802074E310000000001", "status_code": 4, "status": "accepted_dgii", "…": "…" }
}

Y si el documento que ya existe fue rechazado (409), el reenvío no lo reactiva: use otro folio.

{
  "status": "error",
  "message": "El e-NCF E310000000030 ya fue procesado y la DGII lo rechazó (motivos en errors). Un folio tiene un solo documento, así que no se envió de nuevo: use otro folio para el comprobante corregido.",
  "duplicate": true,
  "document": {
    "e_ncf": "E310000000030",
    "identificador": "133802074E310000000030",
    "status_code": 6,
    "status": "rejected_dgii",
    "status_description": "La DGII rechazó el e-CF: es nulo para fines tributarios. Revise los mensajes de error. Si sequence_reusable es true, la DGII da la secuencia por no utilizada y puede anularla.",
    "…": "…"
  }
}

Cuando el documento que ya existe fue aprobado por la DGII (aceptado o aceptado condicional), el mensaje empieza siempre por «Documento enviado y aprobado anteriormente.»: puede usar esa frase para reconocer que el reenvío ya está resuelto. La respuesta es la misma lo haya enviado su sistema, otro sistema de la misma empresa o cualquier otra vía, y no dice quién lo envió.

El reenvío devuelve el detalle sin los archivos: para el XML y el PDF consulte el documento por su identificador. La tabla de qué se responde por cada estado está en numeracion.md.

El token o la empresa no valen — 401, 403 y 422 de empresa

Estos tres cortes ocurren antes de leer el comprobante: no se crea nada y no se consume ningún e-NCF. Se comprueban en este orden —token, empresa, permiso— y todos explican el porqué en data.detalle.

401 — el token falta, está mal copiado o fue revocado (con WWW-Authenticate: Bearer):

{
  "status": "error",
  "message": "No se pudo autenticar la petición.",
  "data": {
    "detalle": "Falta el token. Envíelo en la cabecera «Authorization: Bearer {token}»."
  }
}

422 — la petición no declara la empresa (o la declara mal, o dos veces distinta):

{
  "status": "error",
  "message": "Falta indicar la empresa.",
  "data": {
    "detalle": "Envíe en la consulta el parámetro «emisor_rnc» con el RNC (9 dígitos) o la cédula (11 dígitos) de la empresa en cuyo nombre actúa, por ejemplo «?emisor_rnc=133802074». El token identifica a quien llama, que puede atender a varias empresas; el RNC de la petición dice a cuál se refiere."
  }
}

403 — esa empresa no está habilitada para su token:

{
  "status": "error",
  "message": "Este token no puede actuar sobre esa empresa.",
  "data": {
    "detalle": "La empresa indicada no está habilitada para este token. Compruebe el RNC; si es correcto, solicite que habiliten esa empresa para su cuenta."
  }
}

403 — la empresa es suya, pero al token le falta un permiso; la respuesta dice cuál:

{
  "status": "error",
  "message": "El token no tiene permiso para esta operación.",
  "data": {
    "detalle": "Esta operación exige el permiso «directory:read». Solicite una credencial que lo incluya.",
    "permisos_requeridos": ["directory:read"],
    "permisos_del_token": ["documents:read"]
  }
}

Todos los textos y el porqué de cada uno, en tokens-y-permisos.md.

El documento o la ruta no existen — 404

Un identificador que no existe, o que es de otra empresa, responde siempre lo mismo: «No se encontró el documento.» (y, en un e-CF recibido, «No se encontró el e-CF recibido.»), con el texto largo en data.detalle:

{
  "status": "error",
  "message": "No se encontró el documento.",
  "data": {
    "detalle": "No se encontró un e-CF con el identificador [133802074E310000009999] para este emisor."
  }
}

Y una ruta que no existe (una versión que no es v1, un identificador mal formado, un tipo que no es uno de los diez, la forma antigua de consultar por e-NCF solo) también lleva el envelope, en español, con el recurso y el método que pidió:

{
  "status": "error",
  "message": "La ruta solicitada no existe.",
  "data": {
    "detalle": "No hay ningún recurso para «GET /api/v2/documents». Revise la dirección y el método; la guía de integración lista los recursos disponibles."
  }
}

Método no admitido — 405

Dice qué métodos sí admite la ruta, en el cuerpo (data.metodos_permitidos) y en la cabecera Allow:

{
  "status": "error",
  "message": "El método no está permitido para esta ruta.",
  "data": {
    "detalle": "Esta ruta no admite el método DELETE. Los métodos permitidos son: GET, HEAD.",
    "metodos_permitidos": ["GET", "HEAD"]
  }
}

Error de validación al emitir — 422 Unprocessable Entity

La validación de un e-CF es la única que responde con document (y no con data): el comprobante tiene un estado fiscal (rejected_validation) y es lo que se le devuelve. El e-CF no se transmitió y no se creó ningún documento: su número sigue libre. Corrija y reintente con el mismo e-NCF.

{
  "status": "error",
  "message": "Los datos enviados no son válidos.",
  "document": {
    "status_code": 2,
    "status": "rejected_validation",
    "status_description": "El e-CF no superó las validaciones previas al envío y no se transmitió a la DGII: su e-NCF no llegó a usarse ante ella.",
    "errors": [
      "El campo tipo de ingresos es obligatorio.",
      "El campo cantidad del ítem debe ser mayor que 0."
    ],
    "detailed_errors": {
      "id_doc.tipo_ingresos": ["El campo tipo de ingresos es obligatorio."],
      "items.0.cantidad": ["El campo cantidad del ítem debe ser mayor que 0."]
    }
  }
}
  • errors — lista plana de mensajes, en español. Útil para mostrar al usuario final.
  • detailed_errors — los mismos mensajes indexados por campo. Útil para resaltar campos en un formulario.

Los 422 que tienen que ver con el número —que esté vencido, anulado o fuera de la autorización que usted registró— llevan el mismo document con errors, y «El e-CF no se pudo recibir.» como message:

{
  "status": "error",
  "message": "El e-CF no se pudo recibir.",
  "document": {
    "status_code": 2,
    "status": "rejected_validation",
    "status_description": "El e-CF no superó las validaciones previas al envío y no se transmitió a la DGII: su e-NCF no llegó a usarse ante ella.",
    "errors": ["La secuencia del e-NCF E310000000042 venció el 31-12-2025."]
  }
}

(En el modo XML, el message es «El XML e-CF no se pudo recibir.»; el resto es igual.)

Error de validación de otra operación — 422

Un filtro inválido del listado, una anulación de secuencias mal armada, un certificado sin contraseña o una aprobación comercial con un estado que no existe no hablan de un e-CF, así que no llevan document (que mostraría un estado fiscal que no corresponde): llevan data, con detalle (el primer error y cuántos más hay) y errores, los mensajes por campo.

{
  "status": "error",
  "message": "Los datos enviados no son válidos.",
  "data": {
    "detalle": "El campo fecha desde no corresponde al formato Y-m-d. (y 1 error más: vea «errores».)",
    "errores": {
      "from": ["El campo fecha desde no corresponde al formato Y-m-d."],
      "per_page": ["El campo elementos por página debe ser un número entero."]
    }
  }
}

Una anulación que la DGII rechaza también es un 422 con data.detalle (el motivo que dio la DGII); ver anulacion-secuencias.md.

Demasiadas peticiones — 429

Si la API o la red que tiene por delante aplican un límite de peticiones, la respuesta dice cuánto esperar, en el cuerpo (data.reintentar_en_segundos) y en la cabecera Retry-After. Respételo: es una espera, no un fallo.

{
  "status": "error",
  "message": "Demasiadas peticiones.",
  "data": {
    "detalle": "Superó el límite de peticiones. Espere 30 segundos antes de reintentar.",
    "reintentar_en_segundos": 30
  }
}

Errores nuestros — 500 y 503

500 — algo falló de nuestro lado. El texto es siempre el mismo: nunca trae el detalle técnico del fallo (una consulta, una ruta de archivos, una traza); eso queda en nuestro registro, no en su respuesta.

{
  "status": "error",
  "message": "Ocurrió un error interno.",
  "data": {
    "detalle": "No se pudo completar la petición por un error de nuestro lado. Ya quedó registrado: reintente más tarde y, si persiste, contáctenos."
  }
}

Si el fallo ocurre al registrar un e-CF, la respuesta lo dice con el bloque document (estado system_error): «No se pudo registrar el e-CF por un error interno. No se consumió ningún e-NCF: reintente más tarde.» Reenvíe el mismo folio.

503 — la DGII no respondió o contestó algo ilegible. No es un error de su petición: reintente en unos minutos. El texto técnico de lo que dijo la DGII no sale (ni una URL, ni su cuerpo): sólo esto. El message cambia según la consulta (la del directorio dice «No se pudo consultar el directorio de la DGII en este momento.»); el detalle es el mismo.

{
  "status": "error",
  "message": "El servicio de consulta de la DGII no está disponible en este momento.",
  "data": {
    "detalle": "La DGII no respondió o contestó con un error. No es un fallo de su petición: reintente en unos minutos."
  }
}

Códigos HTTP

Código Significado
200 Una lectura correcta, o el reenvío de un folio que ya existe y no necesita nada más: ya se procesó o espera a la DGII ("duplicate": true).
201 Una operación que crea algo: anular secuencias (aceptada por la DGII), instalar el certificado.
202 Recibido y encolado (emitir; también la aprobación comercial) o, en un reenvío, reintentado tras un fallo nuestro.
400, 413, 415 La petición está mal formada, es demasiado grande o su Content-Type no se admite. Con el envelope y data.detalle.
401 Token ausente, inválido o revocado (con WWW-Authenticate: Bearer; el cuerpo dice cuál de los casos: tokens-y-permisos.md).
403 O la empresa no está habilitada para su token («Este token no puede actuar sobre esa empresa.»), o al token le falta un permiso (el cuerpo dice cuál).
404 El documento (o el e-CF recibido) no existe o no es de esa empresa; o la ruta no existe (un identificador mal formado, la forma antigua por e-NCF).
405 Esa ruta no admite el método usado; el cuerpo y la cabecera Allow dicen cuáles sí.
409 Reenvío de un folio cuyo documento fue rechazado ("duplicate": true): use otro folio.
422 Falta el RNC del emisor, está mal formado o se declaran dos distintos (también emisor_rnc junto a un identificador que lleva otro); el cuerpo no es un JSON válido; o los datos no pasaron las validaciones. No se creó nada.
429 Superó un límite de peticiones: espere data.reintentar_en_segundos.
500 Error interno nuestro, con un texto fijo. Si ocurre al registrar un e-CF, no se consumió ningún e-NCF.
503 La DGII no responde. No es un error de su petición: reintente.

Convenciones del JSON

Tema Convención
Nombres snake_case en español (precio_unitario, razon_social).
Emisor Se declara en emisor.rnc (obligatorio) y es también la empresa en cuyo nombre se actúa: su token tiene que estar vinculado a ese RNC.
e-NCF Se envía siempre, completo, en id_doc.e_ncf; y id_doc.fecha_vencimiento_secuencia en los tipos que la llevan. La numeración la lleva su sistema (numeracion.md).
Identificador {RNC o cédula del emisor}{e-NCF} (p. ej. 133802074E320000000303): el dato con el que se consulta un documento (cómo se arma).
Fechas dd-MM-aaaa (p. ej. 27-07-2026). Fecha y hora: dd-MM-aaaa HH:mm:ss.
Montos Número JSON con hasta 2 decimales y punto decimal (1180.50). Sin separador de miles ni símbolo de moneda.
Códigos de catálogo Siempre string, respetando ceros a la izquierda ("01", "002").
Campos opcionales Omítalos. Enviar null es equivalente a omitirlos.
Zona horaria America/Santo_Domingo (UTC−4, sin horario de verano).

Estructura general del JSON

{
  "emisor":               { "rnc": "133802074" /* obligatorio: identifica la empresa */ },
  "comprador":            { /* datos del cliente (según el tipo) */ },
  "id_doc":               { /* identificación del documento: ingresos, pago, plazos */ },
  "items":                [ /* líneas del detalle: 1 a 1.000 */ ],
  "totales":              { /* montos gravados, exentos, ITBIS, total */ },
  "referencia":           { /* SOLO notas de crédito/débito: NCF que modifica */ },
  "descuentos_recargos":  [ /* opcional: descuentos o recargos globales */ ],
  "otra_moneda":          { /* opcional: si factura en divisa */ },
  "subtotales":           [ /* opcional: subtotales informativos */ ],
  "informacion_adicional":{ /* opcional */ }
}

Detalle campo por campo en campos-comunes.md y en la página de cada tipo.

Topes y límites

Límite Valor
Líneas de detalle 1.000; 10.000 en Factura de Consumo (32) por debajo de RD$250.000
Formas de pago 7
Descuentos/recargos globales 20
Subtotales informativos 20
Decimales en montos 2
Umbral de la Factura de Consumo RD$250.000 (define el comportamiento del tipo 32)

Recomendaciones

  1. Guarde el identificador que devuelve el 202 (o arme el suyo: RNC + e-NCF, que usted ya tiene) y, si quiere, el document_id.
  2. Si no sabe si su envío llegó (un corte, un timeout), reenvíe el mismo folio: no se duplica. Se le responde según el estado del documento que ya exista, o se recibe si no había llegado.
  3. Consulte el estado por identificador, con espaciado creciente. La DGII suele resolver en segundos, pero puede tardar; nosotros consultamos automáticamente con reintentos progresivos. No espere un TrackID (llega null en el 202, aparece al transmitir y los estados finales lo conservan; no existe en el resumen de la factura de consumo).
  4. Un rejected_dgii es definitivo: el comprobante es nulo fiscalmente. Revise errors y emita uno nuevo con otro folio: el rechazado no se puede reenviar. Si sequence_reusable es true, la DGII da esa secuencia por no utilizada y puede anularla para no dejar un hueco en su numeración.
  5. system_error no es culpa de su JSON: es un fallo nuestro y se reintenta solo.