Emitir un e-CF
Un solo endpoint emite todos los tipos de comprobante: POST /api/v1/documents.
Le mandas las líneas y los totales; FiscalFlow arma el XML, lo firma con el certificado de la
empresa y lo transmite a la DGII.
Requiere un plan con emisión habilitada. Ver Planes y límites.
Una factura de crédito fiscal completa
/api/v1/documents
curl -X POST https://api.fiscalflowsupport.com/api/v1/documents \
-H "X-API-Key: ffs_live_ejemplodeclavenoreal000000000000000000" \
-H "Content-Type: application/json" \
-d '{
"type": "31",
"encf": "E310000000001",
"external_ref": "venta-2026-000412",
"emission_date": "2026-08-14",
"expiration_date": "31-12-2026",
"buyer": {
"rnc": "101000001",
"name": "Cliente de Prueba SRL"
},
"items": [
{
"description": "Servicio de consultoría",
"quantity": 1,
"unit_price": 5000.00,
"amount": 5000.00,
"itbis_amount": 900.00
}
],
"totals": {
"subtotal": 5000.00,
"exempt_amount": 0.00,
"itbis": 900.00,
"total": 5900.00
},
"payment": {
"type": 1
}
}'
import requests
payload = {
"type": "31",
"encf": "E310000000001",
"external_ref": "venta-2026-000412",
"emission_date": "2026-08-14",
"expiration_date": "31-12-2026",
"buyer": {"rnc": "101000001", "name": "Cliente de Prueba SRL"},
"items": [{
"description": "Servicio de consultoría",
"quantity": 1,
"unit_price": 5000.00,
"amount": 5000.00,
"itbis_amount": 900.00,
}],
"totals": {
"subtotal": 5000.00,
"exempt_amount": 0.00,
"itbis": 900.00,
"total": 5900.00,
},
"payment": {"type": 1},
}
r = requests.post(
"https://api.fiscalflowsupport.com/api/v1/documents",
headers={"X-API-Key": "ffs_live_ejemplodeclavenoreal000000000000000000"},
json=payload,
)
doc = r.json()["data"]
print(doc["id"], doc["status"], doc["track_id"])
const payload = {
type: "31",
encf: "E310000000001",
external_ref: "venta-2026-000412",
emission_date: "2026-08-14",
expiration_date: "31-12-2026",
buyer: { rnc: "101000001", name: "Cliente de Prueba SRL" },
items: [{
description: "Servicio de consultoría",
quantity: 1,
unit_price: 5000.0,
amount: 5000.0,
itbis_amount: 900.0,
}],
totals: { subtotal: 5000.0, exempt_amount: 0.0, itbis: 900.0, total: 5900.0 },
payment: { type: 1 },
};
const r = await fetch("https://api.fiscalflowsupport.com/api/v1/documents", {
method: "POST",
headers: {
"X-API-Key": "ffs_live_ejemplodeclavenoreal000000000000000000",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const { data } = await r.json();
console.log(data.id, data.status, data.track_id);
La respuesta es 202 con status: "processing" y un
track_id: la DGII resolverá después. El cuerpo completo y las tres formas de
enterarte del veredicto están en
Ciclo de vida de un e-CF.
id (un ULID) contra tu factura antes de responderle a tu propio usuario.
Es la única forma de volver a encontrar el documento si el proceso se interrumpe, y el
external_ref por si solo no te da el PDF ni el XML.
El emisor no va en el payload
No mandas RNC de emisor, razón social ni dirección. Todo eso sale de la empresa a la que pertenece tu API key, y su RNC tiene que coincidir con el del certificado digital cargado. Si facturas bajo varios RNC, usa una key por empresa.
Campos
Nivel superior
| Parameter | Type | Required | Description |
|---|---|---|---|
type
|
string | Required | Tipo de comprobante, dos dígitos sin la E: 31, 32, 33, 34, 41, 43, 44, 45, 46 o 47. Ver la tabla de tipos más abajo. |
items
|
array | Required | Al menos una línea. |
totals
|
object | Required | Totales del documento. Se validan contra si mismos antes de emitir. |
encf
|
string | Optional | El e-NCF, formato E + 12 dígitos. Obligatorio en certecf y ecf. Ver Secuencias y fechas. |
external_ref
|
string | Optional | Tu identificador del documento, hasta 100 caracteres. Único por organización; es la clave de idempotencia. Mándalo siempre. |
emission_date
|
string | Optional | Fecha de emisión en yyyy-mm-dd. Por defecto, hoy. |
expiration_date
|
string | Optional | Vencimiento del rango de e-NCF autorizado, en dd-mm-yyyy. Obligatorio salvo para 32 y 34, que lo prohíben. |
buyer
|
object | Optional | Datos del comprador: buyer.rnc (hasta 11 caracteres) y buyer.name (hasta 150). |
payment
|
object | Optional | Condiciones de pago. Si lo omites se asume contado en efectivo por el total. |
income_type
|
string | Optional | Tipo de ingreso de la DGII, dos caracteres. Por defecto 01 (ingresos por operaciones). |
reference
|
object | Optional | Documento que este modifica. Obligatorio en notas de débito (33) y crédito (34). |
notes
|
string | Optional | Comentario libre, hasta 500 caracteres. |
async
|
boolean | Optional | Con true la respuesta vuelve de inmediato con 202 y el envío a la DGII ocurre en segundo plano. Util si tu cliente HTTP tiene un timeout corto. |
items[]
warnings
La respuesta de POST /api/v1/documents (también la de un duplicado y la 202 con
async) trae warnings, una lista que viene vacía cuando no hay nada que
avisar. Hoy un aviso no detiene la emisión; más adelante el mismo caso responderá 422.
{
"success": true,
"data": { "...": "..." },
"warnings": [
{
"code": "BILLING_INDICATOR_MISSING",
"field": "items.1.billing_indicator",
"message": "Línea 2 sin billing_indicator en un comprobante con ITBIS: se declaró ITBIS 18% (indicador 1). Envía el indicador explícito (1=18%, 2=16%, 3=0%, 4=exento)."
}
]
}
totals
| Parameter | Type | Required | Description |
|---|---|---|---|
subtotal
|
number | Required | Suma de los importes gravados y exentos, sin ITBIS. |
exempt_amount
|
number | Required | Parte del subtotal que va exenta. Manda 0 si no hay. |
itbis
|
number | Required | ITBIS total del documento. |
total
|
number | Required | Total a pagar. |
Antes de emitir se comprueba que subtotal + itbis sea igual a total,
con una tolerancia de dos centavos para absorber redondeos. Si no cuadra:
{
"success": false,
"error": {
"code": "INVALID_TOTALS",
"message": "total (5800.00) does not match subtotal + itbis (5900.00).",
"field": "totals.total"
}
}
payment
| Parameter | Type | Required | Description |
|---|---|---|---|
payment.type
|
integer | Optional | TipoPago de la DGII: 1 contado, 2 crédito, 3 gratuito. Por defecto 1. |
payment.due_date
|
string | Optional | FechaLimitePago, en yyyy-mm-dd. Obligatoria cuando payment.type es 2. |
payment.term
|
string | Optional | TerminoPago: condiciones en texto, hasta 100 caracteres. |
payment.method
|
string | Optional | Medio de pago: 01 efectivo, 02 cheque o transferencia, 03 tarjeta, 04 venta a crédito, 05 bonos o certificados de regalo, 06 permuta, 07 otras formas. |
payment.amount
|
number | Optional | Monto pagado por ese medio. |
payment.method y payment.amount solo llegan al XML cuando
payment.type es 2: la tabla de formas de pago es parte del bloque de
crédito. En una venta de contado puedes omitirlos.
reference
| Parameter | Type | Required | Description |
|---|---|---|---|
reference.encf
|
string | Optional | e-NCF del comprobante que se modifica. |
reference.date
|
string | Optional | Fecha del comprobante modificado. Se acepta dd-mm-yyyy o yyyy-mm-dd. |
reference.reason_code
|
string | Optional | Código de modificación de la DGII, dos caracteres. |
reference.reason
|
string | Optional | Motivo en texto, hasta 255 caracteres. |
Tipos de comprobante
| type | Comprobante | expiration_date | Respuesta |
|---|---|---|---|
31 | Factura Crédito Fiscal | Obligatorio | Asincrona |
32 | Factura de Consumo | Prohibido | Sincrona bajo RD$250,000 |
33 | Nota de Débito | Obligatorio | Asincrona |
34 | Nota de Crédito | Prohibido | Asincrona |
41 | Compras | Obligatorio | Asincrona |
43 | Gastos Menores | Obligatorio | Asincrona |
44 | Regimenes Especiales | Obligatorio | Asincrona |
45 | Gubernamental | Obligatorio | Asincrona |
46 | Comprobante para Exportaciones | Obligatorio | Asincrona |
47 | Comprobante para Pagos al Exterior | Obligatorio | Asincrona |
El tipo 42 (Comprobante Gubernamental de uso interno) no se emite por esta API. Los documentos se emiten siempre en pesos dominicanos.
Factura de consumo (32)
No lleva expiration_date y el buyer es opcional. Si el total queda por
debajo de RD$250,000 viaja por el canal de resumen B2C y el veredicto llega en la misma
respuesta, con 200 en vez de 202.
{
"type": "32",
"encf": "E320000000001",
"external_ref": "pos-2026-000891",
"items": [
{ "description": "Almuerzo", "quantity": 2, "unit_price": 350.00, "amount": 700.00, "itbis_amount": 126.00 }
],
"totals": { "subtotal": 700.00, "exempt_amount": 0.00, "itbis": 126.00, "total": 826.00 }
}
Notas de crédito y débito (34 y 33)
Necesitan el bloque reference apuntando al comprobante que modifican. La nota de
crédito (34) prohíbe expiration_date; la nota de débito (33) lo exige.
{
"type": "34",
"encf": "E340000000001",
"external_ref": "nc-2026-000037",
"reference": {
"encf": "E310000000001",
"date": "14-08-2026",
"reason_code": "01",
"reason": "Devolución parcial de mercancía"
},
"buyer": { "rnc": "101000001", "name": "Cliente de Prueba SRL" },
"items": [
{ "description": "Devolución servicio de consultoría", "quantity": 1, "unit_price": 1000.00, "amount": 1000.00, "itbis_amount": 180.00 }
],
"totals": { "subtotal": 1000.00, "exempt_amount": 0.00, "itbis": 180.00, "total": 1180.00 }
}
reference.date es la fecha del comprobante original, no la de hoy. Hasta la versión
1.1.4 viajaba sin normalizar y una fecha en ISO hacía que la DGII rechazara la nota con
"El formato del XML no es valido" — con el e-NCF ya consumido. Hoy se normaliza, pero
sigue siendo el campo que más conviene revisar cuando una nota se rechaza.
PDF, XML y código QR
Cada documento expone sus artefactos en _links. Todos aceptan el ULID del
documento.
| Endpoint | Devuelve |
|---|---|
GET /api/v1/documents/{id}/pdf |
El PDF fiscal, con el código QR de verificación y el código de seguridad. ?format=pos devuelve la versión estrecha para impresora térmica; sin parámetro, la carta. Da 422 mientras el documento siga en processing. |
GET /api/v1/documents/{id}/xml |
El XML firmado que se envío a la DGII. Es el documento con valor legal y el que hay que archivar. Da 404 hasta que exista. |
GET /api/v1/documents/{id}/qr |
El QR como PNG. Público y sin autenticación, porque tiene que poder escanearlo cualquiera. Solo existe para documentos aceptados. |
Enlaces temporales para compartir
Para enviarle el comprobante a un cliente sin darle una API key, genera un enlace firmado. Vale 15 minutos y no requiere autenticación:
curl -X POST \
-H "X-API-Key: ffs_live_ejemplodeclavenoreal000000000000000000" \
https://api.fiscalflowsupport.com/api/v1/documents/01jz8k4qm2n5p7r9t1v3w5x7y9/share-link/pdf
{
"success": true,
"data": {
"type": "pdf",
"url": "https://api.fiscalflowsupport.com/api/v1/documents/01jz8k4qm2n5p7r9t1v3w5x7y9/share/pdf?expires=1786000000&signature=...",
"expires_at": "2026-08-14T15:21:02+00:00"
}
}
Los tipos validos son pdf, xml y qr. Como el enlace caduca
en quince minutos, genéralo cuando el usuario lo pida, no al emitir.
Enviar el comprobante por correo
curl -X POST \
-H "X-API-Key: ffs_live_ejemplodeclavenoreal000000000000000000" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}' \
https://api.fiscalflowsupport.com/api/v1/documents/01jz8k4qm2n5p7r9t1v3w5x7y9/notify
Solo funciona sobre documentos aceptados; sobre uno en processing devuelve 422.
El envío se encola, así que un 200 significa "aceptado para enviar", no "entregado".
Ver el PDF antes de emitir
POST /api/v1/documents/preview devuelve el PDF de un documento que no existe y no se
emite: sirve para que el usuario revise cómo quedará. Toma el mismo payload más
company_rnc, responde con los bytes del PDF (Content-Type: application/pdf)
y no consume secuencia ni cuota.
Consultar y listar
GET /api/v1/documents/{id} devuelve un documento.
GET /api/v1/documents lista los de la empresa de tu key, del más reciente al más
antiguo, con paginación por cursor de 15 en 15. Filtros disponibles:
type, status, encf, external_ref,
environment, date_from y date_to.
curl -H "X-API-Key: ffs_live_ejemplodeclavenoreal000000000000000000" \
"https://api.fiscalflowsupport.com/api/v1/documents?status=rejected&date_from=2026-08-01"
{
"success": true,
"data": [ "..." ],
"meta": {
"next_cursor": "eyJpZCI6NDIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"prev_cursor": null,
"has_more": true,
"total": 137
}
}
Para pedir la página siguiente, pasa ?cursor= con el valor de
meta.next_cursor. Cuando has_more es false, terminaste.
?status=rejected&date_from= de ayer y revisa lo que quedo sin emitir. Es la red
de seguridad si un webhook se perdió.