uindocs
API

Idempotencia y reintentos

Cómo reintentar un evento sin duplicar recompensas, usando operationId

Mandar un evento (POST /gamification/triggers) no es una operación segura de reintentar a ciegas: si la primera llamada llegó y completó una misión, y tu sistema la reintenta por un timeout de red, el usuario avanza dos veces.

Para eso existe operationId.

Cómo se usa

Agregá un operationId único por evento de negocio en el body:

{
  "programmaticCode": "PAGO_SERVICIO",
  "accountId": "user-4821",
  "idType": "EXTERNAL",
  "metadata": { "amount": 1500 },
  "operationId": "7f3d9a4c-2e5b-4a6f-9c1d-8e2b3a4d5e6f"
}
FormatoString de 1 a 64 caracteres. Recomendado: un UUID.
AlcancePor cliente. Dos clientes distintos pueden usar el mismo operationId sin pisarse.
OpcionalSi no lo mandás, no hay protección: cada request se procesa como un evento nuevo.

El operationId tiene que venir de tu operación de negocio (el id de la transacción, del pago, del movimiento), no generarse en el momento de la request. Si lo generás por request, cada reintento lleva uno distinto y no protege de nada.

Qué pasa con un duplicado

Estado del originalRespuesta al duplicado
Ya procesado200 con el mismo body que devolvió la primera llamada. No se vuelve a procesar: no se otorgan recompensas de nuevo ni avanza el progreso.
Todavía en curso409 — "A trigger notification with this operationId is already being processed". Reintentá más tarde con el mismo operationId.
Falló200 con { "rewards": [], "progressedMissions": [] } — ver la advertencia de abajo.

Un operationId se quema aunque el procesamiento falle. Si la primera llamada entró pero explotó de nuestro lado, el reintento con el mismo operationId no reprocesa el evento: devuelve 200 con listas vacías. Un 200 con rewards: [] y progressedMissions: [] es ambiguo — puede ser un evento que legítimamente no movió nada, o un original fallido.

Si el evento es crítico y recibiste un 5xx en la primera llamada, reintentá con un operationId nuevo en lugar de repetir el original.

Qué reintentar y qué no

Respuesta¿Reintentar?
2xxNo. Ya se procesó.
400No. El body es inválido; reintentar da lo mismo.
401 / 403No. Revisá la API Key o los permisos. Ver Errores.
404No. El programmaticCode no existe para tu cliente, o la cuenta no existe con idType=INTERNAL.
409 de idempotenciaSí, con el mismo operationId y backoff.
5xx o timeoutSí, con backoff exponencial. Si mandaste operationId, leé la advertencia de arriba.
async function sendEvent(body, attempt = 0) {
  const res = await fetch(`${BASE}/gamification/triggers`, {
    method: 'POST',
    headers,
    body: JSON.stringify(body),
  });

  if (res.ok) return res.json();

  const err = await res.json();

  // Original todavía en vuelo: mismo operationId, más tarde.
  const retryable = res.status >= 500 || res.status === 409;
  if (retryable && attempt < 4) {
    await sleep(2 ** attempt * 500);
    return sendEvent(body, attempt + 1);
  }

  throw new ApiError(err.code, err.traceId);
}

El resto de la API

operationId aplica hoy a los dos endpoints de envío de eventos:

  • POST /gamification/triggers (sincrónico)
  • POST /gamification/triggers/async

Los demás endpoints no tienen clave de idempotencia. Para las escrituras que importan:

  • POST /accounts — si la cuenta ya existe para ese externalId, usá POST /accounts/resolve, que devuelve la existente o la crea. Es seguro de repetir.
  • Canjes (/marketplace/publications/redeem/:id, /loyalty/products/:id/redeem) — no son idempotentes. Un reintento tras un timeout puede generar un segundo canje. Antes de reintentar, consultá GET /redemptions/:accountId para ver si el primero entró.

On this page