Emitir un e-CF

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

POST /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.

Guarda el 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.

202 Response
{
  "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:

422 Response
{
  "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
31Factura Crédito FiscalObligatorioAsincrona
32Factura de ConsumoProhibidoSincrona bajo RD$250,000
33Nota de DébitoObligatorioAsincrona
34Nota de CréditoProhibidoAsincrona
41ComprasObligatorioAsincrona
43Gastos MenoresObligatorioAsincrona
44Regimenes EspecialesObligatorioAsincrona
45GubernamentalObligatorioAsincrona
46Comprobante para ExportacionesObligatorioAsincrona
47Comprobante para Pagos al ExteriorObligatorioAsincrona

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
200 Response
{
  "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"
200 Response
{
  "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.

Un buen uso de este listado es la conciliación diaria: pide ?status=rejected&date_from= de ayer y revisa lo que quedo sin emitir. Es la red de seguridad si un webhook se perdió.