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.
Los estados
Sección titulada «Los estados»| 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 |
Recorrido normal
Sección titulada «Recorrido normal»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──► DISCARDEDREJECTED y CONTINGENCY no son lo mismo
Sección titulada «REJECTED y CONTINGENCY no son lo mismo»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 leedian_errors, se corrige el origen y se emite otro documento. Por esoretryni siquiera lo acepta.CONTINGENCY: la DIAN no respondió. El documento está bien; lo que falló fue el otro extremo. Aquíretryes exactamente lo que hay que hacer, y no cuesta un consecutivo.
Cómo te enteras de los cambios
Sección titulada «Cómo te enteras de los cambios»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:
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.
Idempotencia por reference_code
Sección titulada «Idempotencia por reference_code»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.
Qué pasa exactamente al repetir
Sección titulada «Qué pasa exactamente al repetir»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.
Lo que tienes que saber, sin adornos
Sección titulada «Lo que tienes que saber, sin adornos»- El cuerpo de la repetición se ignora por completo. No se compara
con el original. Si repites
reference_codeconitemsdistintos, recibes201con el documento viejo y los datos nuevos se pierden en silencio. Unreference_code= un documento, para siempre. - Una repetición no consume cupo del plan.
- Una repetición no toma un consecutivo nuevo. El número se toma una vez por documento, no por intento.
- La cabecera
Idempotency-Keyno existe en este API. Mandarla no hace nada. La idempotencia es la dereference_code. reference_codeadmite hasta 100 caracteres, y no puede ir vacío.
Reintentar
Sección titulada «Reintentar»POST/v1/documents/:id/retry
Vuelve a poner en marcha un documento parado. Sin cuerpo.
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.
Errores
Sección titulada «Errores»| 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. |
Descartar
Sección titulada «Descartar»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. |
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.
Errores
Sección titulada «Errores»| 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 |
Buenas prácticas de reintento
Sección titulada «Buenas prácticas de reintento»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.