Para devs que conectan cuentas de vendedores con OAuth y ven invalid_grant en los logs.
Conectar una cuenta de Mercado Libre por OAuth lleva una tarde. El problema aparece días
después: un vendedor que "se desconectó solo", una sincronización que dejó de andar a las 3 de
la mañana, un invalid_grant que nadie entiende. Muchas veces la causa es la misma: el refresh
token se usó dos veces.
Las dos reglas
Según la documentación de autenticación de Mercado Libre:
- El access token dura 6 horas. Después hay que renovarlo.
- El refresh token es de un solo uso. Cada renovación te devuelve uno nuevo y el anterior deja de servir.
(Para recibir refresh token, tu aplicación del DevCenter tiene que tenerlo activado.)
La renovación es un POST a https://api.mercadolibre.com/oauth/token:
grant_type=refresh_token
client_id=...
client_secret=...
refresh_token=...
La respuesta trae un access_token nuevo y un refresh_token nuevo. Si no guardás el
nuevo, la próxima renovación va a fallar. Ese es el primer error, y el más fácil de ver.
El segundo es más traicionero.
La carrera
Imaginá dos pedidos del mismo vendedor que salen casi juntos (un aviso de orden y un cron, por
ejemplo) justo cuando el token venció:
Pedido A: token vencido → renueva con R1 → recibe R2 → guarda R2
Pedido B: token vencido → renueva con R1 → invalid_grant
R1 ya se usó. Según cómo esté escrito tu código, el pedido B:
- marca la cuenta como desconectada, aunque R2 quedó bien guardado,
- o guarda datos viejos encima de R2 y ahí sí no queda ningún token que sirva,
- o reintenta para siempre con un token que nunca va a andar.
En cualquiera de los tres casos el vendedor queda afuera, aunque nadie hizo nada mal. Y casi
nunca pasa en tus pruebas, porque ahí hay un pedido a la vez. Pasa en producción, con tráfico.
1. Renovar antes de que venza
No esperes al 401. Guardá cuándo vence el token (expires_in viene en segundos) y renová con
margen, antes de cada pedido:
const MARGIN_MS = 60 * 60 * 1000; // 1 hora
if (tokens.expiresAt - Date.now() > MARGIN_MS) return tokens.accessToken;
return (await refresh(userId)).accessToken;
Con una hora de margen, un pedido largo o un reloj un poco desfasado no usan un token vencido.
Si igual llega un 401, renová una sola vez y repetí el pedido. Si vuelve a dar 401, no insistas.
2. Una sola renovación a la vez por vendedor
Dentro de un proceso alcanza con guardar la renovación en curso y que los demás la esperen:
const inFlight = new Map<number, Promise<Tokens>>();
function refresh(userId: number) {
const running = inFlight.get(userId);
if (running) return running; // ya hay una: esperala
const job = doRefresh(userId).finally(() => inFlight.delete(userId));
inFlight.set(userId, job);
return job;
}
Y dentro de doRefresh, releé el token antes de renovar: otra renovación pudo terminar
mientras esperabas. Si el que está guardado ya es nuevo, usalo y no gastes el refresh token.
3. Con varias instancias, el lock va en la base
El Map de arriba vive en la memoria de un proceso. En Vercel, en Lambda o con dos réplicas
detrás de un balanceador, cada instancia tiene el suyo y la carrera vuelve.
Si guardás los tokens en Postgres, un advisory lock por vendedor lo resuelve sin tablas extra:
await db.transaction(async (tx) => {
// Bloquea solo a este vendedor, hasta el final de la transacción.
await tx.query("SELECT pg_advisory_xact_lock($1::bigint)", [userId]);
const current = await getTokens(tx, userId); // releer dentro del lock
if (isFresh(current)) return current; // otra instancia ya renovó
const next = await refreshWithMeli(current.refreshToken);
await saveTokens(tx, next);
return next;
});
La segunda instancia espera el lock, relee, ve el token nuevo y no llama a Mercado Libre. La
transacción queda abierta durante la llamada a /oauth/token, pero es una sola por vendedor
cada varias horas.
4. invalid_grant no se arregla reintentando
Si la renovación devuelve invalid_grant, el refresh token ya no sirve: se usó, venció o el
vendedor le quitó el permiso a tu aplicación. Reintentar no sirve. Lo correcto es:
- Marcar la cuenta como "hay que volver a conectar" y dejar de llamar a la API con ella.
- Avisarle al vendedor (o a tu equipo) con el link para conectar de nuevo.
Si no lo marcás, cada cron y cada aviso vuelven a intentar, llenan los logs y nadie se entera
de que hay una cuenta caída.
Ojo: esto es seguro solo si ya evitaste la carrera (puntos 2 y 3). Si no, el invalid_grant del
pedido B de arriba te haría marcar como caída una cuenta que estaba bien.
Resumen
| Problema | Qué hacer |
|---|---|
| El token vence a las 6 h | Renovar antes, con margen, no después del 401 |
| El refresh token nuevo se pierde | Guardar siempre el que vuelve en cada renovación |
| Dos renovaciones en el mismo proceso | Una sola en curso por vendedor; el resto la espera |
| Dos renovaciones en instancias distintas | Lock por vendedor en la base y releer adentro |
invalid_grant |
Marcar la cuenta para reconectar; no reintentar |
Armé todo esto en VentaKit, un starter de Next.js + TypeScript para la API de Mercado
Libre, con tests para cada caso: https://github.com/javiersivack/ventakit (gratis, MIT).
Además de los tokens, trae los avisos de órdenes a prueba de duplicados y desorden. La versión
Pro suma Postgres con el lock entre instancias, una conciliación por cron que recupera avisos
perdidos y un panel que muestra qué vendedores hay que reconectar:
https://ventakit-site.vercel.app/?utm_source=devto&utm_campaign=art1
Lo probé contra la API real de Mercado Libre con usuarios de prueba: en la demo, el token
venció sin que nadie lo usara y la siguiente corrida del cron lo renovó sola, sin desconectar
a nadie.
VentaKit está hecho con Claude, una IA, como compañero de programación: yo lo dirijo, lo pruebo
contra Mercado Libre real y respondo por él.
¿Preferís no integrarlo vos? Hago integraciones con Mercado Libre, Mercado Pago y otras APIs
para equipos y agencias: https://javiersivack.com/es?utm_source=devto&utm_campaign=ventakit-art1#contact
Top comments (0)