📋 Descripción General

Este servicio implementa el ciclo completo de facturación electrónica colombiana conforme al Anexo Técnico v1.9 de la DIAN (Resolución 000085/2022). Recibe datos de negocio vía REST, genera el XML UBL 2.1, lo firma digitalmente con XAdES-EPES, lo empaqueta en ZIP y lo transmite al web service SOAP de la DIAN (WcfDianCustomerServices).

El procesamiento es asíncrono: la API responde de inmediato con un id de documento y un estado queued. Un worker BullMQ ejecuta el pipeline en background y actualiza el estado en PostgreSQL. Usa GET /v1/documents/:id para consultar el resultado.

Lectura para IAs / agentes El esquema OpenAPI 3.1 completo está disponible en GET /documentation/openapi.json — úsalo para inspección programática de todos los endpoints, parámetros y tipos. Esta página HTML contiene exactamente la misma información en formato legible.

Tecnología del pipeline

Stack
Fastify (Node.js) → Zod validation → Prisma/PostgreSQL
  ↓ BullMQ worker (Redis)
  ├─ 1. Build UBL 2.1 XML
  ├─ 2. Sign XAdES-EPES (PKCS#12 certificate)
  ├─ 3. ZIP (nombre: NIT+PREFIX+CONSECUTIVE.zip)
  ├─ 4. SendBillAsync / SendTestSetAsync (DIAN SOAP)
  ├─ 5. Poll GetStatusZip ← retries exponenciales
  └─ 6. Store XML+PDF en S3/R2 · Update DB status

🔄 Flujo Completo Paso a Paso

Guía para integrar este servicio desde cero, en orden cronológico:

Configurar credenciales DIAN

En el panel admin (/admin/configuration) configura: NIT emisor, Software ID, Software PIN, Test Set ID, y sube el certificado digital (.p12). El SoftwareSecurityCode se calcula automáticamente como SHA384(softwareId + pin + nit).

Sincronizar resoluciones DIAN

Llama a POST /v1/dian/numbering-range/sync para obtener los rangos de numeración autorizados (GetNumberingRange). Esto guarda en DB la clave técnica (ClTec), prefijo, rangos y fechas de vigencia — necesarios para calcular el CUFE.

Crear API Key

En /admin/integrations crea una API Key. Incluye los headers X-API-Key y X-API-Secret en todas las llamadas de negocio.

Enviar factura de prueba

Con POST /v1/invoices y el prefijo de habilitación (ej: SETT). El servicio usará SendTestSetAsync en ambiente 2 (habilitación).

Consultar resultado

Con GET /v1/documents/:id. Espera estado validated (código DIAN 00). Si hay error, el campo dianMessage describe el rechazo.

Pasar a producción

Cuando DIAN aprueba el set de pruebas, cambia DIAN_AMBIENTE=1 en el panel. El servicio usará SendBillAsync y el WSDL de producción automáticamente.

🔑 Autenticación

Todos los endpoints /v1/* requieren dos headers:

HeaderDescripciónEjemplo
X-API-Key Identificador público de la API Key (generado en /admin/integrations) dk_a1b2c3d4...
X-API-Secret Secreto de la API Key. Solo se muestra una vez al crear. Se almacena como hash SHA256. e5f6a7b8...
Ejemplo con autenticación
curl -X POST https://fe.damaju.com.co/v1/invoices \
  -H "Content-Type: application/json" \
  -H "X-API-Key: dk_tu_api_key" \
  -H "X-API-Secret: tu_api_secret" \
  -d '{ ... }'
⚠️
El Secret solo se muestra una vez Guárdalo de inmediato al crear la API Key. Si lo pierdes, debes revocar la key y crear una nueva.

❤️ Health & Status

GET /health Health check básico Sin autenticación

Verifica que el proceso Node.js esté vivo. Úsalo como liveness probe en Kubernetes/Docker.

Respuesta 200 OK

JSON
{ "status": "ok", "timestamp": "2026-05-22T23:00:00.000Z" }
GET /ready Readiness check — DB + Redis Sin autenticación

Verifica que PostgreSQL y Redis estén accesibles. Úsalo como readiness probe.

Respuesta 200 OK

JSON
{
  "status": "ready",
  "checks": { "database": "ok", "redis": "ok" },
  "timestamp": "2026-05-22T23:00:00.000Z"
}

🧾 Facturas Electrónicas

POST /v1/invoices Crear y enviar factura electrónica a DIAN 🔑 Requiere auth

Valida el payload con Zod (totales matemáticos, DV NIT, DANE codes), construye el XML UBL 2.1, firma con XAdES-EPES, empaqueta en ZIP y encola el envío a DIAN. La respuesta es inmediata; el procesamiento ocurre en background.

ℹ️
CUFE calculado automáticamente El servidor calcula el CUFE como SHA384(NumFac + FecFac + HorFac + ValFac + CodImp1 + ValImp1 + ValImp3 + ValTot + NitOFE + NumAdq + ClTec + ambiente). No es necesario enviarlo en el request.

Parámetros del Body

CampoTipoReq.Descripción
issuerPartyEmisor (Damaju). Ver esquema Party.
customerPartyAdquiriente/cliente. Ver esquema Party.
itemsInvoiceLine[]Líneas de detalle. Mínimo 1, máximo 9999. lineTotal debe ser ≈ quantity × unitPrice.
taxesTaxTotal[]Impuestos agrupados por código (IVA=01, INC=04, ICA=03).
totalsTotalsTotales monetarios. Validación cruzada: taxInclusiveAmount = taxExclusiveAmount + Σtaxes.
prefixstringPrefijo autorizado por resolución DIAN. Ej: SETT
consecutivenumberNúmero específico. Si se omite, se auto-asigna el siguiente disponible.
issueDatestringFecha emisión YYYY-MM-DD. Default: hoy.
issueTimestringHora emisión HH:MM:SS-05:00. Default: ahora Colombia.
currencystringCódigo ISO 4217. Default: COP
invoiceTypeCodeenum01=FV, 02=Exportación, 03=Contingencia, 04=Contingencia DIAN. Default: 01
paymentPaymentMedio de pago y fecha vencimiento.
notesstring[]Notas libres (máx 5000 chars c/u).
orderReferencestringReferencia orden de compra.
resolutionstringNúmero de resolución DIAN. Ej: 18764086918727

Ejemplo completo cURL

cURL — POST /v1/invoices
curl -X POST https://fe.damaju.com.co/v1/invoices \
  -H "Content-Type: application/json" \
  -H "X-API-Key: dk_tu_api_key" \
  -H "X-API-Secret: tu_api_secret" \
  -d '{
  "issuer": {
    "documentType": "31",
    "documentNumber": "900000000",
    "dv": "1",
    "name": "DAMAJU SAS",
    "email": "facturacion@damaju.com.co",
    "address": {
      "street": "Calle 1 # 2-3",
      "city": "Bogotá",
      "cityCode": "11001",
      "department": "Bogotá D.C.",
      "departmentCode": "11",
      "countryCode": "CO"
    },
    "taxScheme": "ZZ",
    "taxLevelCode": "R-99-PN"
  },
  "customer": {
    "documentType": "13",
    "documentNumber": "1234567890",
    "name": "CLIENTE EJEMPLO",
    "email": "cliente@example.com",
    "address": {
      "street": "Carrera 10 # 20-30",
      "city": "Medellín",
      "cityCode": "05001",
      "department": "Antioquia",
      "departmentCode": "05",
      "countryCode": "CO"
    }
  },
  "items": [
    {
      "id": "1",
      "description": "Servicio de consultoría",
      "quantity": 1,
      "unitCode": "EA",
      "unitPrice": 200000,
      "lineTotal": 200000,
      "taxAmount": 38000,
      "taxPercent": 19,
      "taxCode": "01"
    }
  ],
  "taxes": [
    {
      "taxCode": "01",
      "taxAmount": 38000,
      "taxableAmount": 200000,
      "percent": 19
    }
  ],
  "totals": {
    "lineExtensionAmount": 200000,
    "taxExclusiveAmount":  200000,
    "taxInclusiveAmount":  238000,
    "allowanceTotalAmount": 0,
    "chargeTotalAmount":    0,
    "payableAmount":       238000
  },
  "resolution": "18764086918727",
  "prefix": "SETT",
  "payment": {
    "meansCode": "10",
    "dueDate": "2026-06-22"
  }
}'

Respuesta exitosa 201 Created

JSON
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "prefix": "SETT",
  "consecutive": 1,
  "status": "queued",
  "message": "Documento encolado para procesamiento"
}
📌
Procesamiento asíncrono Guarda el id y consulta el estado con GET /v1/documents/:id. El procesamiento típico tarda entre 3 y 30 segundos según la carga de DIAN.

📝 Notas Crédito

POST /v1/credit-notes Crear nota crédito electrónica 🔑 Requiere auth

Mismo payload que POST /v1/invoices más los campos adicionales de referencia a la factura original. El CUDE se calcula como SHA384 con los mismos campos que el CUFE.

Campos adicionales

CampoTipoReq.Descripción
billingReferenceobject Referencia a la factura original:
invoiceId: número completo (ej: SETT1)
invoiceUuid: CUFE de la factura (96 chars hex)
issueDate: fecha de la factura YYYY-MM-DD
creditNoteReasonCodeenum 1=Devolución parcial, 2=Anulación, 3=Rebaja, 4=Descuento, 5=Rescisión, 6=Otros. Default: 2
creditNoteReasonstring Descripción libre de la razón. Default: Anulación de factura electrónica

Ejemplo cURL

cURL — POST /v1/credit-notes
curl -X POST https://fe.damaju.com.co/v1/credit-notes \
  -H "Content-Type: application/json" \
  -H "X-API-Key: dk_tu_api_key" \
  -H "X-API-Secret: tu_api_secret" \
  -d '{
  "billingReference": {
    "invoiceId":   "SETT1",
    "invoiceUuid": "abc123...96hexchars",
    "issueDate":   "2026-05-20"
  },
  "creditNoteReasonCode": "1",
  "creditNoteReason": "Devolución parcial de mercancía",
  "issuer":   { ... },
  "customer": { ... },
  "items":    [ ... ],
  "taxes":    [ ... ],
  "totals":   { ... },
  "resolution": "18764086918727",
  "prefix": "NC"
}'

📝 Notas Débito

POST /v1/debit-notes Crear nota débito electrónica 🔑 Requiere auth

Igual que nota crédito, con campos de razón distintos.

Campos adicionales (distintos a nota crédito)

CampoTipoReq.Descripción
debitNoteReasonCodeenum 1=Intereses, 2=Gastos por cobrar, 3=Cambio de valor. Default: 1
debitNoteReasonstring Descripción libre. Default: Intereses

📄 Consulta de Documentos

GET /v1/documents/:id Estado y detalles de un documento 🔑 Requiere auth

Consulta el estado actual del pipeline para un documento creado previamente.

Respuesta 200 OK

JSON
{
  "id":          "550e8400-e29b-41d4-a716-446655440000",
  "type":        "INVOICE",
  "prefix":      "SETT",
  "consecutive": 1,
  "cufe":        "a3b4c5...96hexchars",
  "trackId":     "uuid-retornado-por-dian",
  "status":      "validated",
  "dianCode":    "00",
  "dianMessage": "Procesado correctamente",
  "xmlPath":     "documents/900000000/invoices/SETT/SETT1.xml",
  "pdfPath":     "documents/900000000/invoices/SETT/SETT1.pdf",
  "createdAt":   "2026-05-22T23:00:00.000Z",
  "updatedAt":   "2026-05-22T23:00:45.000Z"
}
GET /v1/documents/:id/xml Descargar XML firmado UBL 2.1 🔑 Requiere auth

Retorna el XML UBL 2.1 con firma XAdES-EPES tal como fue enviado a DIAN.

cURL
curl -H "X-API-Key: dk_..." -H "X-API-Secret: ..." \
  https://fe.damaju.com.co/v1/documents/ID/xml > factura.xml
GET /v1/documents/:id/pdf Descargar PDF representación gráfica 🔑 Requiere auth

PDF generado con Puppeteer (HTML → PDF). Incluye cabecera, detalle de ítems, totales, CUFE y QR.

cURL
curl -H "X-API-Key: dk_..." -H "X-API-Secret: ..." \
  https://fe.damaju.com.co/v1/documents/ID/pdf > factura.pdf

🏛️ Operaciones DIAN

GET /v1/dian/status/:trackId Consultar estado en DIAN por TrackId 🔑 Requiere auth

Invoca GetStatusZip en el web service DIAN para el trackId (ZipKey) retornado por SendBillAsync o SendTestSetAsync.

Respuesta 200 OK

JSON
{
  "statusCode":        "00",
  "statusDescription": "Procesado correctamente",
  "statusMessage":     "Documento validado por la DIAN",
  "isValid":           true,
  "errors":            [],
  "warnings":          []
}
POST /v1/dian/numbering-range/sync Sincronizar resoluciones (GetNumberingRange) 🔑 Admin session

Llama a GetNumberingRange en DIAN y guarda en la base de datos las resoluciones activas con: número resolución, prefijo, rango de numeración, clave técnica (ClTec), y fechas de vigencia. Ejecutar antes de emitir cualquier documento.

Respuesta 200 OK

JSON
{
  "synced": 2,
  "ranges": [
    {
      "resolutionNumber": "18764086918727",
      "prefix":           "SETT",
      "fromNumber":       1,
      "toNumber":         5000,
      "technicalKey":     "fc8eac422eba16e22ffd8c6f94b3f40a6e38162c...",
      "validFrom":        "2026-01-01",
      "validTo":          "2027-01-01"
    }
  ]
}

🔔 Webhooks de Integración

Endpoints diseñados para ser llamados directamente desde un ERP, e-commerce o POS de Damaju. Cada webhook es un alias semántico de los endpoints principales.

WebhookAcciónEquivalente
POST /v1/webhooks/order-paid Orden pagada → emitir factura electrónica POST /v1/invoices
POST /v1/webhooks/order-cancelled Orden cancelada → emitir nota crédito de anulación POST /v1/credit-notes
POST /v1/webhooks/return-accepted Devolución aceptada → emitir nota crédito de devolución POST /v1/credit-notes
📌
Mismo payload El body de cada webhook es idéntico al endpoint equivalente. Ver sección Facturas o Notas Crédito.

📐 Esquemas de Datos

Party — Emisor / Adquiriente

CampoTipoReq.Descripción
documentTypeenum11=Registro civil, 12=Tarjeta identidad, 13=Cédula ciudadanía, 21=Tarjeta extranjería, 22=Cédula extranjería, 31=NIT, 41=Pasaporte, 42=Documento extranjero, 47=PEP, 50=NIT otro país, 91=NUIP
documentNumberstringNúmero del documento (máx 20 chars)
dvstringDígito de verificación NIT (1 char). Se valida contra el algoritmo DIAN si se provee.
namestringRazón social o nombre completo (máx 450 chars)
emailstringEmail válido (máx 200 chars). Se usa para envío del PDF.
phonestringTeléfono de contacto
addressAddressVer esquema Address
taxSchemestringRégimen tributario DIAN. Default: ZZ
taxLevelCodestringNivel de responsabilidad fiscal. Default: R-99-PN

Address

CampoTipoReq.Descripción
streetstringDirección completa (máx 200 chars)
citystringNombre del municipio
cityCodestringCódigo municipio DANE — exactamente 5 dígitos. Ej: 11001 = Bogotá
departmentstringNombre del departamento
departmentCodestringCódigo departamento DANE — exactamente 2 dígitos. Ej: 11 = Bogotá D.C.
countryCodestringISO 3166-1 alpha-2. Default: CO

InvoiceLine

CampoTipoReq.Descripción
idstringNúmero de línea. Ej: "1"
descriptionstringDescripción del bien o servicio (máx 1000 chars)
quantitynumberCantidad (positivo)
unitCodestringUnidad de medida UN/CEFACT. Default: EA (Each)
unitPricenumberPrecio unitario sin impuesto
lineTotalnumberTotal línea sin impuesto. Debe ser ≈ quantity × unitPrice (tolerancia ±1 COP)
taxAmountnumberValor impuesto de la línea. Debe ser ≈ lineTotal × taxPercent/100 (tolerancia ±1 COP)
taxPercentnumberPorcentaje impuesto. Ej: 19 para IVA 19%
taxCodestringCódigo impuesto DIAN. 01=IVA, 03=ICA, 04=INC. Default: 01

TaxTotal

CampoTipoReq.Descripción
taxCodeenumCódigo tributo DIAN: 01=IVA, 02=INC bebidas azucaradas, 03=ICA, 04=INC, 05=INC bolsas plásticas, 06=Carbono, 07=Combustibles, 08=Tabaco, y más.
taxAmountnumberValor total del impuesto (suma de todas las líneas)
taxableAmountnumberBase gravable total
percentnumberTarifa porcentual. Ej: 19

Totals — Validaciones cruzadas

CampoTipoReq.Regla de validación
lineExtensionAmountnumberDebe ser = Σ(lineTotal de todos los items) ±1 COP
taxExclusiveAmountnumberSubtotal sin impuestos (normalmente = lineExtensionAmount)
taxInclusiveAmountnumberDebe ser = taxExclusiveAmount + Σ(taxAmount de taxes) ±1 COP
allowanceTotalAmountnumberTotal descuentos. Default: 0
chargeTotalAmountnumberTotal cargos adicionales. Default: 0
payableAmountnumberDebe ser = taxInclusiveAmount - allowanceTotalAmount + chargeTotalAmount ±1 COP

🚦 Estados del Pipeline

Un documento transita por estos estados desde la creación hasta la validación DIAN:

pending Creado, esperando ser encolado
queued En cola BullMQ, esperando worker
building Construyendo XML UBL 2.1
signing Firmando con XAdES-EPES
zipping Empaquetando en ZIP para DIAN
sending Enviando a DIAN (SOAP)
track_id_received TrackId recibido, esperando resultado
polling Consultando GetStatusZip en DIAN
validated ✅ Validado correctamente por DIAN
rejected ❌ Rechazado por DIAN (ver dianMessage)
retry En reintento automático (máx 3 intentos)
error Error técnico no recuperable

🏛️ Códigos de Respuesta DIAN

El campo dianCode refleja el código retornado por GetStatusZip:

00 Documento procesado y validado correctamente
01 Alerta — documento con observaciones pero aceptado
02 Documento rechazado — errores en el XML
04 Documento en proceso — consultar nuevamente
10 Error de firma digital — verificar certificado
99 Error técnico DIAN — reintentar más tarde

❌ Manejo de Errores

Todos los errores siguen el mismo formato:

JSON — Error de validación 400
{
  "error": "Validation failed",
  "statusCode": 400,
  "details": [
    {
      "path": ["totals", "payableAmount"],
      "message": "payableAmount (238000) debe ser taxInclusiveAmount - descuentos + cargos (240000.00)"
    }
  ]
}
HTTP StatusCuándo ocurre
200Consulta exitosa (GET)
201Documento creado y encolado (POST)
400Error de validación Zod: campos faltantes, tipos incorrectos, totales matemáticos no cuadran, DV NIT inválido, código DANE incorrecto
401API Key o Secret inválidos / ausentes
404Documento no encontrado
500Error interno — revisar logs en /admin/logs

⚙️ OpenAPI / Integración Programática

El esquema OpenAPI 3.1 completo está disponible como JSON para herramientas, agentes de IA y generadores de clientes:

cURL
curl https://fe.damaju.com.co/documentation/openapi.json
🤖
Para agentes de IA Fetch GET /documentation/openapi.json para obtener el contrato completo de la API en formato estándar. Esta página HTML contiene la misma información con ejemplos más detallados. Los esquemas Zod en src/types/invoice.ts son la fuente de verdad de validación.