Ir al contenido

Ciclo de vida de un documento

Emitir es asíncrono. El 201 de un POST /v1/invoices dice que el documento se aceptó y se encoló, no que la DIAN lo validó: llega con status: "QUEUED", full_number: null y cufe: null.

A partir de ahí el documento pasa solo por tres etapas —construir, enviar, entregar— y va cambiando de estado. Esta página es lo que hay que saber para escribir una integración que aguante un timeout, un despliegue a media emisión y una caída de la DIAN.

Estado Qué significa ¿retry? ¿discard?
DRAFT Defecto de la columna en base de datos. Un documento emitido por el API nunca pasa por aquí: se crea directamente en QUEUED. No Sí
QUEUED Aceptado y encolado para construir. Todavía no tiene número, ni CUFE, ni XML. Sí Sí
SIGNED Numerado, con el UBL armado, el CUFE/CUDE calculado, firmado XAdES, validado contra el XSD y guardado. Encolado para transmitir. Sí Sí
SENT En vuelo hacia la DIAN. Un intento falló por red y la cola va a reintentar sola. No No
VALIDATED La DIAN respondió IsValid. Es el estado final bueno: dispara el PDF, el AttachedDocument y el correo. No No
REJECTED La DIAN contestó y dijo que no. Los motivos quedan en dian_errors. No Sí
CONTINGENCY La DIAN no estuvo disponible tras agotar los 5 intentos: caída, timeout o un fault SOAP. El documento sigue vivo, con una ventana de 48 horas. Sí Sí
FAILED Falló la construcción antes de transmitir: sin certificado, sin clave técnica en el rango, XML que no cumple el XSD, error de firma. El motivo queda en dian_errors[0].message. Sí Sí
DISCARDED Se dio por perdido a propósito. Deja de contar como pendiente, pero la fila se queda. No No
QUEUED ──build──► SIGNED ──send──► VALIDATED ──deliver──► PDF + AttachedDocument + correo
│ │ │
│ ├──► REJECTED (la DIAN dijo que no)
│ └──► SENT ──► CONTINGENCY (la DIAN no respondió tras 5 intentos)
│
└──► FAILED (no se pudo construir ni firmar)
{DRAFT, QUEUED, SIGNED, FAILED, CONTINGENCY, REJECTED} ──discard──► DISCARDED

Se confunden porque los dos dejan el documento sin validar, y se tratan al revés:

  • REJECTED: la DIAN respondió. Hay un problema en el documento, y reintentar tal cual produce el mismo rechazo. Se lee dian_errors, se corrige el origen y se emite otro documento. Por eso retry ni siquiera lo acepta.
  • CONTINGENCY: la DIAN no respondió. El documento está bien; lo que falló fue el otro extremo. Aquí retry es exactamente lo que hay que hacer, y no cuesta un consecutivo.

Lo correcto es un webhook. El ciclo dispara document.validated, document.rejected, document.contingency, document.failed y document.email_failed.

Si prefieres barrer pendientes cada tanto, filtra por estado:

Ventana de terminal
curl "https://sandbox.facturacion.misofk.com/v1/documents?status=FAILED&limit=100" \
-H "Authorization: Bearer $TOKEN"

Filtra por status, no por fecha. from y to comparan contra issue_date, y un documento que se quedó en QUEUED no tiene fecha de emisión todavía: se cae del filtro justo el que estabas buscando.

reference_code es tu identificador del documento, y es la clave de idempotencia de este API. El ámbito es la pareja emisor + reference_code: dos empresas distintas pueden usar el mismo valor; una misma empresa, no.

Al recibir un POST de creación, el servidor busca reference_code en tu emisor. Hay tres desenlaces:

a) No existe — se crea. Se descuenta cupo del plan, se guarda en QUEUED y se encola. Respuesta 201 con el documento nuevo.

b) Existe con OTRO tipo de documento — 409 IDEMPOTENCY_CONFLICT:

{
"status": 409,
"code": "IDEMPOTENCY_CONFLICT",
"message": "reference_code VE-2026-000841 ya existe con otro tipo de documento",
"errors": []
}

El espacio de nombres es uno solo para facturas, notas, documento soporte y POS. El tropiezo clásico: reutilizar VE-2026-000841 —el de la venta— como reference_code de la nota crédito de esa misma venta. Ponle otro, por ejemplo NC-2026-000129.

c) Existe con el MISMO tipo — te devuelve el documento que ya había, con su estado actual y 201. Antes de devolvértelo, si estaba parado, le vuelve a dar cuerda:

Estado del que ya existía Qué hace el servidor
QUEUED sin XML firmado Reencola la construcción
QUEUED o SIGNED con XML firmado Reencola el envío a la DIAN
SENT, VALIDATED, REJECTED, FAILED, CONTINGENCY, DISCARDED Nada; lo devuelve tal cual

Ese reencolado no es un detalle. Quien repite un reference_code casi siempre es una caja que no recibió respuesta. Devolverle el documento sin más era contestarle «ya está» sobre algo que se había quedado parado en un despliegue y no iba a avanzar nunca: la venta quedaba cobrada y sin llegar a la DIAN.

  1. El cuerpo de la repetición se ignora por completo. No se compara con el original. Si repites reference_code con items distintos, recibes 201 con el documento viejo y los datos nuevos se pierden en silencio. Un reference_code = un documento, para siempre.
  2. Una repetición no consume cupo del plan.
  3. Una repetición no toma un consecutivo nuevo. El número se toma una vez por documento, no por intento.
  4. La cabecera Idempotency-Key no existe en este API. Mandarla no hace nada. La idempotencia es la de reference_code.
  5. reference_code admite hasta 100 caracteres, y no puede ir vacío.

POST/v1/documents/:id/retry

Vuelve a poner en marcha un documento parado. Sin cuerpo.

Ventana de terminal
curl -X POST https://sandbox.facturacion.misofk.com/v1/documents/b1f3a7d0-9c42-4e18-8a5b-6d0e2f4c9a11/retry \
-H "Authorization: Bearer $TOKEN"

Se reintentan cuatro estados: FAILED, CONTINGENCY, QUEUED y SIGNED. Los dos primeros son fallos que el sistema vio. Los otros dos están en la lista por una razón muy concreta: si el proceso se cae —y se cae en cada despliegue— un documento se queda en QUEUED con su trabajo perdido, o en SIGNED con el XML hecho y sin enviar. Sin retry ahí acababa la historia.

Qué reintenta depende de lo que ya esté hecho:

Situación Qué se reencola Respuesta
Ya tiene XML firmado Solo el envío a la DIAN { "id": "...", "status": "SENT" }
No tiene XML firmado La construcción completa { "id": "...", "status": "QUEUED" }

Dos peticiones simultáneas de retry sobre el mismo documento tampoco lo construyen dos veces: el trabajo lleva el id del documento como identificador en la cola, así que el segundo no entra.

HTTP code Cuándo
400 VALIDATION El :id no es un UUID
401 / 403 UNAUTHORIZED / FORBIDDEN Token ausente, vencido o de una credencial que no abre esta puerta
404 NOT_FOUND Documento no encontrado — no existe, o no es de tu emisor
422 VALIDATION No se puede reintentar un documento SENT. Se reintentan: FAILED, CONTINGENCY, QUEUED, SIGNED.

POST/v1/documents/:id/discard

Darlo por perdido, a propósito. Es la salida cuando el documento ya no puede llegar: al emisor le falta el certificado, la prueba se hizo con un NIT que no existe, la venta se anuló antes de transmitirse. Reintentar eso es pedirle a la DIAN que rechace lo mismo una y otra vez; no hacer nada es dejarlo contando como pendiente y disparando el aviso cada mañana.

Campo Tipo Obligatorio Descripción
reason string No Por qué se da por perdido. Se guarda recortado de espacios. La columna admite 300 caracteres: sé breve.
Ventana de terminal
curl -X POST https://sandbox.facturacion.misofk.com/v1/documents/b1f3a7d0-9c42-4e18-8a5b-6d0e2f4c9a11/discard \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "Prueba de integración con NIT inexistente" }'
{ "id": "b1f3a7d0-9c42-4e18-8a5b-6d0e2f4c9a11", "status": "DISCARDED" }

Se puede descartar DRAFT, QUEUED, SIGNED, FAILED, CONTINGENCY y REJECTED. VALIDATED y SENT no: uno ya está en la DIAN y el otro podría llegar todavía.

HTTP code Cuándo
400 VALIDATION El :id no es un UUID
401 / 403 UNAUTHORIZED / FORBIDDEN Credencial ausente o que no corresponde
404 NOT_FOUND Documento no encontrado
422 VALIDATION No se puede descartar un documento VALIDATED: ya salió hacia la DIAN. Si hay que anularlo, se emite una nota de crédito.
422 VALIDATION Ese documento ya estaba descartado

Espera creciente, siempre. Ante un fallo de red o un 5xx, reintenta a 1 s, 2 s, 4 s, 8 s, 16 s, y para. Reintentar en bucle cerrado solo te acerca al límite de 300 peticiones por minuto por IP, y llegar ahí te gana un 429 que también hay que reintentar.

El mismo reference_code, sin excepción. Es lo único que impide el duplicado. Genera el reference_code antes de la primera llamada y guárdalo junto a la venta en tu base de datos; si lo generas dentro del reintento, cada intento crea un documento nuevo y la idempotencia no te sirve de nada.

Qué reintentar y qué no:

Situación ¿Reintentar? Cómo
Timeout, conexión cortada, no llegó respuesta Sí Repite el POST idéntico. Te devuelve el documento si se había creado
429 Sí Espera y baja el ritmo
INTERNAL con status 500 Sí Espera creciente. Si se repite, escríbenos con el reference_code y la hora
INTERNAL con status 501 o 404 No INTERNAL también es el código de relleno: mira status. Ver Errores
La DIAN no respondió Sí No llega como error de la petición: el POST ya devolvió 201 con status: "QUEUED". El documento pasa solo a CONTINGENCY y se dispara el webhook document.contingency; el motivo queda en dian_errors[0].message. Se recupera con POST /documents/:id/retry y el consecutivo no se pierde
Documento en CONTINGENCY o FAILED Sí POST /documents/:id/retry, no un POST de creación nuevo
VALIDATION (400/422) No Arregla el campo que dice errors[]. El mismo cuerpo da el mismo error
IDEMPOTENCY_CONFLICT (409) No Cambia el reference_code: ese ya es de otro tipo de documento
NOT_FOUND, FORBIDDEN No Revisa el id y la credencial
CERT_MISSING, CERT_EXPIRED, CERT_NIT_MISMATCH, SOFTWARE_NOT_CONFIGURED No Se arregla en la configuración del emisor, no en tu código
PLAN_REQUIRED, PLAN_EXHAUSTED, PLAN_EXPIRED No Hay que contratar o renovar
RANGE_EXHAUSTED, RANGE_EXPIRED No Hay que pedirle numeración a la DIAN. Avisa antes de llegar al final del rango
Documento en REJECTED No Lee dian_errors, corrige y emite un documento nuevo con otro reference_code
Documento en SENT No Consulta hasta que se resuelva

Los rangos de notas crédito y débito son la excepción de la fila de numeración: no llevan vigencia, así que RANGE_EXPIRED no les puede pasar, y cuando se agotan no hay nada que pedirle a la DIAN — se registra el tramo siguiente con el mismo prefijo. Ver rangos de numeración.

Dale tiempo entre retry y retry. El envío a la DIAN ya reintenta solo 5 veces con espera exponencial que arranca en 30 segundos. Volver a llamar a retry a los 5 segundos no acelera nada: solo pisa un trabajo que estaba en marcha. Minutos, no segundos.

Ten un final. Un documento que lleva días en FAILED por algo que no se va a arreglar tiene que acabar en discard, con su motivo escrito. La alternativa —dejarlo ahí— es una lista de pendientes que nadie mira.

  • Errores — la forma de la respuesta y qué significa cada code
  • Webhooks — enterarte del cambio de estado sin consultar en bucle
  • Facturas — todos los campos de la emisión