Cufy API

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_support o none.
  • Puede incluir operation_id, resource_id y resource_type.
  • Guarda el cuerpo de forma privada junto con request_id. No registres la clave ni material de firma.

Qué hacer con cada respuesta

RespuestaQué hacer
401 INVALID_API_KEYRevisa la credencial del ambiente. No envíes otra factura.
403 PERMISSION_DENIED / ENVIRONMENT_DISABLEDRevisa permisos y capacidades. Producción sigue bloqueada.
404El recurso no existe para tu empresa y ambiente (ver aislamiento).
400 IDEMPOTENCY_KEY_REQUIREDPrepara una clave válida antes de enviar.
422 FISCAL_VALIDATION_FAILEDCorrige los errores publicados. Confirma que no hubo admisión antes de preparar otra identidad.
422 UNSUPPORTED_OPERATIONLa ruta está pendiente; no está en la referencia.
409 IDEMPOTENCY_CONFLICT / EXTERNAL_REFERENCE_CONFLICTNo cambies la identidad para eludir el conflicto. Compara con la solicitud original.
409 VERSION_CONFLICT / 428 PRECONDITION_REQUIREDVuelve a consultar el recurso y decide con su ETag actual.
409 SECRET_ALREADY_DELIVEREDEl secreto ya se entregó. Usa resource_id para consultar el recurso; rota o sustituye si lo perdiste.
429 SANDBOX_DAILY_LIMITTu 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_ALLOWEDEl 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ónNo 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.statusQué significa
validatedLa DIAN la aceptó. Ya tiene valor fiscal. Puede traer avisos.
rejectedLa DIAN la rechazó. Corrige y emite otra; esta no sirve.
unknownNo 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.

ReglaQué dice la DIANQué hacer
FAJ43bEl 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.
FAJ44bEl NIT del emisor no corresponde al nombre registrado en el RUT.Nada, por lo mismo.
RUT01La DIAN todavía no valida el estado del RUT en habilitación y lo avisa en cada documento.Nada.
CAK55 / DAK55La nota crédito o débito no trae el correo del cliente.Envía buyer.email y el aviso desaparece.
CBF02La nota no repite el número de la factura en un campo opcional de la referencia.Nada. La referencia al original sí va completa.
90La 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_reasonQué pasóQué hacer
TRANSPORT_ATTEMPTS_EXHAUSTEDLo intentamos varias veces y la DIAN no respondió.Reanuda con POST /v1/documents/{id}/resumption cuando la DIAN vuelva a responder.
TRANSPORT_SIGNING_BLOCKEDEl 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 (202 con el mismo document_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-After y 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ímiteValorQué pasa al superarlo
Facturas por día50429 SANDBOX_DAILY_LIMIT, con la espera hasta la medianoche de Colombia
Peticiones por minuto60429 HTTP_RATE_LIMIT, con su Retry-After
Destinos de webhook3429 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.

En esta página