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.
El cliente de ejemplo (integration.ts) lanza ApiFailure con estado, problema y Retry-After, y nunca reintenta por su cuenta.
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. |
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.
prepareInvoice, prepareCredit y prepareDebit del cliente de ejemplo fijan los bytes para un proceso, pero no los guardan: tu integración debe persistirlos antes del primer envío. submit hace un solo intento.
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.
| Ámbito | Solicitudes/s | Ráfaga | Simultáneas |
|---|---|---|---|
| Servicio API | 100 | 200 | 64 |
| IP (IPv6 agrupada por /64) | 50 | 100 | 16 |
| Empresa y ambiente | 30 | 60 | 8 |
| Clave verificada | 20 | 40 | 4 |
| Escrituras por empresa y ambiente | 10 | 50 | 4 |
| Cuenta del sandbox | 1 | 60 | 4 |
| Salud (presupuesto separado) | 5 | 10 | 2 |
Son valores iniciales por proceso, no capacidad fiscal garantizada. Varias claves comparten el límite de su empresa; varios clientes detrás de una IP comparten el de la red.
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.
Los operadores ajustan los límites con HTTP_<SCOPE>_RATE, HTTP_<SCOPE>_BURST y HTTP_<SCOPE>_CONCURRENCY (SCOPE: GLOBAL, IP, TENANT, KEY, WRITE, SANDBOX, HEALTH) HTTP_MAX_ENTRIES (10.000 por defecto) y SANDBOX_DAILY_DOCUMENTS (50). Las entradas inactivas vencen a los cinco minutos, y los valores inválidos impiden arrancar. Los eventos agregados http.protection publican cada minuto los rechazos por ámbito y motivo, sin claves, IP, empresa ni cuerpo. El servidor solo confía en direcciones reenviadas por el proxy local de loopback (Caddy); no expongas un proxy que acepte cabeceras de origen sin validar, y revisa esta confianza si cambia la topología de red. Antes de ejecutar varios procesos API hay que coordinar los presupuestos, porque los contadores son locales a cada proceso.