- Blog
- Integración
Manejo de errores y reintentos al consumir APIs
Manejo de errores de API: 4xx frente a 5xx, por qué un 404 no es un fallo, backoff exponencial ante 429, timeouts y reintentos en JavaScript y Python.
Tu integración funcionaba perfecto en pruebas. Un martes a las diez de la mañana, en plena emisión de facturas, la red tuvo un hipo de tres segundos y tu sistema se llenó de errores rojos. El vendedor, apurado, escribió la razón social a mano y la escribió mal. Nada de eso tenía que pasar: que una llamada a una API falle de vez en cuando es normal. Lo que no es normal es un cliente que no sabe qué hacer cuando eso ocurre.
En este artículo vas a distinguir errores 4xx de 5xx, entender por qué un 404 es un resultado y no un fallo, qué hacer con un 429, cómo poner timeouts, qué es la idempotencia y cómo registrar los errores para encontrarlos después, con código de reintentos en JavaScript y Python. Si todavía no hiciste tu primera llamada, empieza por cómo integrar una API REST con token.
4xx y 5xx: de quién es el problema
Los códigos de estado HTTP se agrupan por centenas, y esa agrupación es tu primera decisión. Un código 4xx dice "el problema está en la petición": el formato, el token, los créditos, el recurso pedido. Un código 5xx dice "el problema está en el servidor": se cayó, está saturado o un componente intermedio no respondió.
La regla práctica es simple. Un 4xx no se reintenta tal cual, porque la misma petición va a producir el mismo resultado; hay que corregir algo. Un 5xx sí se reintenta, porque en unos segundos el servidor probablemente se recupere. La única excepción dentro de los 4xx es el 429, que es un "espera un poco" y no un "arregla tu petición".
| Código | Qué significa | ¿Reintentar? | Qué hacer |
|---|---|---|---|
| 400 | Petición mal formada | No | Validar el formato antes de enviar |
| 401 | Token inválido o ausente | No | Revisar la configuración del token |
| 402 | Sin créditos | No | Avisar al responsable de la cuenta |
| 404 | El recurso no existe | No | Tratarlo como resultado, no como error |
| 429 | Demasiadas peticiones | Sí, con espera | Backoff exponencial |
| 500, 502, 503, 504 | Falla del servidor | Sí, con espera | Backoff exponencial y límite de intentos |
Hay una tercera categoría que no tiene código porque la respuesta nunca llegó: errores de red. DNS que no resuelve, conexión rechazada, timeout. Trátalos como un 5xx: espera y reintenta.
El 404 no es un fallo
Este punto merece su propia sección porque es el error de diseño más común en integraciones con APIs de datos. En una API de consulta, un 404 significa que el recurso no existe: ese RUC no está inscrito, ese DNI no corresponde a nadie, esa fecha no tiene tipo de cambio publicado. Es una respuesta válida y útil.
Lo que suele pasar es que el cliente HTTP lanza una excepción para cualquier código que no sea 2xx, el desarrollador la captura en un bloque genérico y el usuario ve "Error al consultar RUC, intente nuevamente". Reintenta, ve el mismo mensaje, y concluye que el sistema está roto. Peor aún: el log de errores se llena de 404 y los errores reales quedan enterrados.
El tratamiento correcto tiene tres partes. Primero, el 404 se devuelve como un valor (null, None, un objeto con encontrado: false), nunca como excepción. Segundo, la interfaz muestra un mensaje específico: "No existe un contribuyente con ese RUC. Verifica el número". Tercero, no se reintenta, porque va a seguir siendo 404.
En endpoint.pe, además, las consultas sin resultado no consumen créditos, así que un 404 no te cuesta nada más que el mensaje al usuario. Ten en cuenta también la diferencia con las búsquedas: una consulta por razón social que no encuentra coincidencias no devuelve 404 sino un 200 con total en cero y la lista vacía, porque el recurso (la búsqueda) sí existe, solo que no tiene resultados.
429 y backoff exponencial
El 429 aparece cuando envías demasiadas peticiones en poco tiempo. Es habitual en procesos por lotes: alguien valida 5 000 proveedores de golpe con un bucle sin pausas. Lo que haga tu código en ese momento define si el proceso termina bien o se cae a la mitad.
La respuesta correcta es el backoff exponencial: esperar antes de reintentar, y duplicar la espera en cada intento fallido. Medio segundo, un segundo, dos, cuatro. A eso se le añade un poco de aleatoriedad (jitter) para que, si tienes varios procesos en paralelo, no vuelvan todos a la carga en el mismo milisegundo. Y se pone un tope: cuatro o cinco intentos, y una espera máxima de unos segundos, pasados los cuales el error se propaga.
Si la respuesta trae la cabecera Retry-After, úsala como mínimo de espera: el servidor te está diciendo cuánto tiempo necesita. Y ataca la causa, no solo el síntoma. Un proceso por lotes debería limitar su concurrencia (dos o tres peticiones simultáneas, no cincuenta) y hacer una pausa breve entre llamadas. Casi siempre eso basta para no ver un 429 nunca más. Si la mayoría de tus consultas se repiten (los mismos proveedores todos los meses), una caché de consultas reduce el volumen más que cualquier reintento.
Timeouts e idempotencia
Un timeout es el tiempo máximo que esperas una respuesta. Sin timeout, una petición puede quedarse colgada indefinidamente si la conexión se pierde en un estado ambiguo, y con ella el hilo o el worker que la ejecuta. Un formulario que consulta un RUC mientras el usuario escribe no debería esperar más de unos pocos segundos; un proceso nocturno puede permitirse más. En ambos casos, ponlo explícitamente: timeout=10 en Python, AbortSignal.timeout(10000) en JavaScript.
La idempotencia es la propiedad que hace seguros los reintentos. Una operación es idempotente cuando ejecutarla dos veces produce el mismo efecto que ejecutarla una. Consultar un RUC es idempotente: lo consultes una o cinco veces, el contribuyente sigue siendo el mismo y nada cambió en el mundo. Emitir una factura no lo es: si la petición de emisión se reintenta porque la respuesta se perdió, puedes terminar con dos facturas.
Para las APIs de consulta, que son todas peticiones GET, puedes reintentar con tranquilidad. Para operaciones que crean o modifican algo, necesitas una clave de idempotencia (un identificador único por operación que el servidor usa para no repetirla) o una verificación previa antes de reintentar.
Si el primer intento llegó al servidor y respondió, pero la respuesta se perdió en la red, el reintento es una segunda consulta exitosa y cuesta un segundo crédito. Es un caso raro, y una caché local lo vuelve casi inexistente.
Reintentos en JavaScript y Python
Las dos implementaciones siguen la misma lógica: un bucle con un número máximo de intentos, una espera que crece exponencialmente con jitter, reintentos solo para 429, 5xx y errores de red, y el 404 devuelto como valor.
En JavaScript (Node.js 18 o superior):
const TOKEN = process.env.ENDPOINT_TOKEN;
const REINTENTABLES = new Set([429, 500, 502, 503, 504]);
function esperar(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function consultarRuc(ruc) {
let res;
try {
res = await fetch(`https://api.endpoint.pe/v1/ruc/${ruc}`, {
headers: { Authorization: `Bearer ${TOKEN}` },
signal: AbortSignal.timeout(10_000),
});
} catch (err) {
err.reintentable = true; // red caída, DNS o timeout
throw err;
}
if (res.status === 404) return null;
if (!res.ok) {
const err = new Error(`La API respondió ${res.status}`);
err.status = res.status;
err.reintentable = REINTENTABLES.has(res.status);
err.retryAfter = Number(res.headers.get('retry-after')) || 0;
throw err;
}
return (await res.json()).data;
}
async function conReintentos(fn, intentos = 4, baseMs = 500) {
for (let i = 0; i < intentos; i++) {
try {
return await fn();
} catch (err) {
const ultimo = i === intentos - 1;
if (ultimo || !err.reintentable) throw err;
const espera = Math.max(
baseMs * 2 ** i + Math.random() * 200,
(err.retryAfter || 0) * 1000,
);
await esperar(espera);
}
}
}
const empresa = await conReintentos(() => consultarRuc('20131312955'));
console.log(empresa ? empresa.razon_social : 'RUC no encontrado');
En Python, con requests:
import os
import random
import time
import requests
TOKEN = os.environ["ENDPOINT_TOKEN"]
REINTENTABLES = {429, 500, 502, 503, 504}
def consultar_ruc(ruc: str, intentos: int = 4, base: float = 0.5) -> dict | None:
for i in range(intentos):
ultimo = i == intentos - 1
espera = base * 2**i + random.uniform(0, 0.2)
try:
r = requests.get(
f"https://api.endpoint.pe/v1/ruc/{ruc}",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=10,
)
except requests.RequestException:
if ultimo:
raise
time.sleep(espera)
continue
if r.status_code == 404:
return None
if r.status_code in REINTENTABLES and not ultimo:
retry_after = r.headers.get("Retry-After", "")
if retry_after.isdigit():
espera = max(espera, int(retry_after))
time.sleep(espera)
continue
r.raise_for_status()
return r.json()["data"]
empresa = consultar_ruc("20131312955")
print(empresa["razon_social"] if empresa else "RUC no encontrado")
Observa que un 401 o un 402 salen del bucle en el primer intento con una excepción: reintentarlos sería perder tiempo. Y el 404 vuelve como null o None sin esperar ni reintentar. Este mismo esqueleto sirve para cualquier API de consulta, por ejemplo para la tarea diaria de automatizar el tipo de cambio.
Registrar errores para diagnosticar
Un buen registro de errores responde a tres preguntas cuando algo falla a las tres de la mañana: qué se pidió, qué respondió y cuántas veces se intentó. Para cada llamada fallida guarda la fecha y hora, la ruta consultada, el código de estado (o el tipo de error de red), el número de intento, la latencia y el mensaje de error que devolvió la API. Si la respuesta trae un identificador de petición, guárdalo: es lo primero que pide soporte.
Igual de importante es lo que no debes registrar. El token, nunca: si tu librería de logs vuelca las cabeceras completas, enmascáralo. Los datos personales, lo mínimo: para diagnosticar una consulta de DNI fallida basta con el código de estado y el momento, no hace falta guardar los nombres devueltos. La Ley 29733 de protección de datos personales se aplica también a tus archivos de log.
Asigna niveles con criterio. Un 404 es información, no error. Un 429 es una advertencia que, si se repite, indica que debes bajar la concurrencia. Un 5xx que agotó los reintentos es un error. Un 401 o un 402 son alertas que deben llegar a una persona, porque sin token válido o sin créditos todo el sistema deja de validar. Si tu plan incluye un panel con métricas de uso, revísalo cuando veas 402 o 429: ahí se ve de inmediato si el consumo se disparó.
Por último, define qué pasa cuando se agotan los reintentos. En facturación, lo razonable es degradar con gracia: el usuario continúa con los datos a mano, el registro queda "pendiente de validación" y se revalida cuando el servicio responda, como recomendamos en la checklist para validar un cliente antes de facturar.
Que los errores lleguen como códigos HTTP estándar y con mensajes claros es, además, un criterio para elegir proveedor. Lo desarrollamos en cómo elegir una API de consulta RUC y DNI en Perú.
En resumen
- Un 4xx se corrige, un 5xx se reintenta. La excepción es el 429, que se reintenta con espera.
- El 404 es un resultado ("no existe"), no un fallo: devuélvelo como valor, muestra un mensaje claro y no lo reintentes.
- Ante un 429 o un 5xx, usa backoff exponencial con jitter, un tope de intentos y respeta
Retry-Aftersi llega. - Pon siempre un timeout explícito y reintenta solo operaciones idempotentes; las consultas
GETlo son. - Registra ruta, código, intento y latencia; nunca el token ni más datos personales de los necesarios.
- Decide de antemano qué hace tu sistema cuando la API no responde: degradar con gracia es mejor que bloquear la venta.
Preguntas frecuentes
¿Cuántos reintentos debo configurar?
Entre tres y cinco suele ser suficiente. Con base de medio segundo y duplicación, cuatro intentos suman unos siete segundos de espera total, que es lo máximo razonable para una llamada que bloquea a un usuario. Un proceso por lotes puede permitirse más intentos y esperas más largas.
¿Debo reintentar un 401 o un 402?
No. Un 401 significa que el token está mal configurado o fue revocado, y un 402 que la cuenta no tiene créditos. Reintentar no cambia nada; lo que hace falta es una alerta para que alguien corrija la configuración o recargue créditos.
¿Un reintento consume créditos?
Solo la consulta que devuelve datos consume un crédito. Un 404, un 429 o un error del servidor no te cuestan nada. El único caso en que pagas dos veces es cuando la primera respuesta se perdió en la red después de haber llegado al servidor, y es poco frecuente.
¿Qué hago si la API no responde durante varios minutos?
Deja que el flujo continúe en modo degradado: el usuario ingresa los datos a mano, el registro queda marcado como no validado y una tarea posterior lo valida cuando el servicio vuelve. Bloquear la emisión de una factura por una validación que puede hacerse después es un mal negocio.






