DEV Community

Javier Sivack
Javier Sivack

Posted on

El refresh token de Mercado Libre es de un solo uso: cómo no desconectar a tus vendedores

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:

  1. El access token dura 6 horas. Después hay que renovarlo.
  2. 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=...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Marcar la cuenta como "hay que volver a conectar" y dejar de llamar a la API con ella.
  2. 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)