Errores, idempotencia y reintentos
Formato de error, qué hacer con cada código, cómo reintentar sin duplicar documentos y los límites de tasa de la API.
Formato Problem
Todos los errores usan el mismo cuerpo:
{
"code": "FISCAL_VALIDATION_FAILED",
"message": "…",
"request_id": "…",
"retryable": false,
"action": "fix_request",
"errors": [{ "…": "errores por campo" }]
}action:fix_request,check_credentials,check_permissions,consult_operation,retry_same_key,top_up,contact_supportonone.- Puede incluir
operation_id,resource_idyresource_type. - Guarda el cuerpo de forma privada junto con
request_id. No registres la clave ni material de firma.
Qué hacer con cada respuesta
| Respuesta | Qué hacer |
|---|---|
401 INVALID_API_KEY | Revisa la credencial del ambiente. No envíes otra factura. |
403 PERMISSION_DENIED / ENVIRONMENT_DISABLED | Revisa permisos y capacidades. Producción sigue bloqueada. |
404 | El recurso no existe para tu empresa y ambiente (ver aislamiento). |
400 IDEMPOTENCY_KEY_REQUIRED | Prepara una clave válida antes de enviar. |
422 FISCAL_VALIDATION_FAILED | Corrige los errores publicados. Confirma que no hubo admisión antes de preparar otra identidad. |
422 UNSUPPORTED_OPERATION | La ruta está pendiente; no está en la referencia. |
409 IDEMPOTENCY_CONFLICT / EXTERNAL_REFERENCE_CONFLICT | No cambies la identidad para eludir el conflicto. Compara con la solicitud original. |
409 VERSION_CONFLICT / 428 PRECONDITION_REQUIRED | Vuelve a consultar el recurso y decide con su ETag actual. |
409 SECRET_ALREADY_DELIVERED | El secreto ya se entregó. Usa resource_id para consultar el recurso; rota o sustituye si lo perdiste. |
429 SANDBOX_DAILY_LIMIT | Tu cuenta del sandbox agotó sus facturas del día. Retry-After dice cuántos segundos faltan para la medianoche de Colombia. |
403 SANDBOX_RECIPIENT_NOT_ALLOWED | El sandbox solo envía correo al de la persona dueña de la cuenta. Usa esa dirección o prueba la entrega con webhooks. |
429 / 503, timeout o pérdida de conexión | No supongas que no hubo admisión. Sigue la sección siguiente. |
Lo que responde la DIAN
Que Cufy acepte tu factura (202) no significa que la DIAN la haya validado. Eso pasa después, y llega en dian.status cuando consultas el documento o en el webhook document.validated.
dian.status | Qué significa |
|---|---|
validated | La DIAN la aceptó. Ya tiene valor fiscal. Puede traer avisos. |
rejected | La DIAN la rechazó. Corrige y emite otra; esta no sirve. |
unknown | No sabemos qué pasó. No la vuelvas a emitir: concilia consultando el mismo documento. |
Cuando hay algo que leer, viene en dian.errors y dian.warnings, con mensajes que empiezan por Regla: CÓDIGO,. Un error rechaza el documento; un aviso no: la factura vale igual.
Reglas que vas a ver en el sandbox
Estas son las que aparecen a diario. Varias no son culpa tuya: el sandbox emite con un emisor de pruebas compartido, y la DIAN comenta ese emisor en cada documento.
| Regla | Qué dice la DIAN | Qué hacer |
|---|---|---|
FAJ43b | El nombre del emisor no coincide con el registrado en el RUT. | Nada. Es un aviso sobre el emisor compartido del sandbox, no sobre tu factura. |
FAJ44b | El NIT del emisor no corresponde al nombre registrado en el RUT. | Nada, por lo mismo. |
RUT01 | La DIAN todavía no valida el estado del RUT en habilitación y lo avisa en cada documento. | Nada. |
CAK55 / DAK55 | La nota crédito o débito no trae el correo del cliente. | Envía buyer.email y el aviso desaparece. |
CBF02 | La nota no repite el número de la factura en un campo opcional de la referencia. | Nada. La referencia al original sí va completa. |
90 | La DIAN ya había recibido y procesado este documento. | No lo envíes otra vez. Consulta el documento original. |
Cualquier otra regla llega con las palabras de la DIAN tal cual. Son mensajes escritos para contadores, no para desarrolladores: si uno no se entiende, escríbenos con el request_id y el código de la regla.
Cuando el documento se queda a medio camino
Si el documento no llegó a la DIAN, su estado trae un blocked_reason:
blocked_reason | Qué pasó | Qué hacer |
|---|---|---|
TRANSPORT_ATTEMPTS_EXHAUSTED | Lo intentamos varias veces y la DIAN no respondió. | Reanuda con POST /v1/documents/{id}/resumption cuando la DIAN vuelva a responder. |
TRANSPORT_SIGNING_BLOCKED | El emisor de pruebas del sandbox no está disponible. | No es tu factura. Inténtalo más tarde o escríbenos. |
Reanudar no emite un documento nuevo: conserva su número y su identidad. Ver Corregir y reanudar.
Idempotencia
Toda mutación exige Idempotency-Key: de 8 a 128 caracteres, A-Z a-z 0-9 . _ : -, empezando por letra o dígito.
- La clave vale dentro de empresa, ambiente, operación y recurso destino. Se retiene al menos 90 días y mientras haya trabajo pendiente.
- Misma clave y mismo contenido → devuelve el resultado original (
202con el mismodocument_id). - Misma clave y contenido distinto →
409 IDEMPOTENCY_CONFLICT. - Misma referencia externa y contenido distinto →
409 EXTERNAL_REFERENCE_CONFLICT. - Los decimales se comparan como texto:
"1.0"y"1.00"son contenidos distintos. - No dependas del encabezado
Idempotency-Replayed(el servicio no lo emite). Compara el ID devuelto y consulta el recurso.
Reintentar sin duplicar
Antes del primer envío, guarda de forma durable los bytes exactos del cuerpo, la referencia externa y la clave de idempotencia.
Si la respuesta no llega (timeout, conexión perdida, 429, 503), no generes una factura nueva. Si ya tienes un document_id, consúltalo.
Si no lo tienes, decide explícitamente repetir el envío con los mismos bytes y la misma clave. Si la primera solicitud se había admitido, recibirás esa misma admisión.
Nunca cambies issued_at, decimales, referencia ni clave de una solicitud incierta. Si agotas el presupuesto de consultas, continúa más tarde con el mismo ID.
Serializa el cuerpo una sola vez y guarda esos bytes, no el objeto que los produjo: volver a serializar puede cambiar el orden de las claves o los decimales, y entonces la misma clave de idempotencia devuelve 409 en lugar de tu documento.
Un documento con dian.status: unknown exige conciliación. No es un rechazo y no autoriza otra emisión.
Recursos versionados
Los recursos que lo implementan devuelven ETag. Las acciones que exigen If-Match rechazan versiones antiguas con 409 VERSION_CONFLICT. Vuelve a consultar y evalúa la acción con la versión actual.
Límites de tasa
La API aplica límites antes de leer el cuerpo o autenticar, y otros después de identificar empresa y clave. También cuentan las consultas, las credenciales inválidas y las rutas inexistentes. Son independientes del saldo.
Los límites se aplican en varios niveles a la vez: por dirección IP, por empresa y ambiente, por clave y, aparte, a las escrituras. Una cuenta del sandbox tope con el suyo mucho antes que con los demás; los valores que te afectan están en Límites del sandbox.
Dos cosas que sorprenden: varias claves de la misma empresa comparten su límite, y varios clientes detrás de la misma IP también. Que una clave esté ociosa no te da margen extra.
Un rechazo devuelve HTTP_RATE_LIMIT o HTTP_CONCURRENCY_LIMIT (429 para límites de cliente, 503 por saturación) o HTTP_STATE_CAPACITY (503), con retryable: true, action: retry_same_key y Retry-After en segundos.
- Espera al menos
Retry-Aftery añade variación aleatoria. - Repite con la misma clave y el mismo cuerpo.
- Una solicitud rechazada por estos límites no reserva numeración ni consume crédito.
Límites del sandbox
Una cuenta del sandbox público es gratuita y emite con el emisor compartido de habilitación, así que además de lo anterior tiene tres límites propios:
| Límite | Valor | Qué pasa al superarlo |
|---|---|---|
| Facturas por día | 50 | 429 SANDBOX_DAILY_LIMIT, con la espera hasta la medianoche de Colombia |
| Peticiones por minuto | 60 | 429 HTTP_RATE_LIMIT, con su Retry-After |
| Destinos de webhook | 3 | 429 WEBHOOK_ENDPOINT_LIMIT al registrar el cuarto |
Ninguna factura rechazada por un límite consume numeración: reintenta con la misma Idempotency-Key cuando pase la espera. El correo del sandbox solo llega a la dirección de la persona dueña de la cuenta; para probar notificaciones a tu sistema, usa webhooks. Una empresa real no tiene estos límites.