Centro de Ayuda de Dardo

Límites, reintentos y paginación

Cómo comportarte con la API sin duplicar operaciones ni saltearte registros al recorrer un listado

Lo que necesitás saber antes de meter tráfico real: cuánto podés pedir, cómo reintentar sin romper nada, y cómo recorrer una colección grande.

Límite de tasa

La API tiene límite de tasa y está activo en producción. Al excederlo recibís 429 Too Many Requests.

Lo que ese 429 no te da:

  • No trae Retry-After. No hay forma de saber cuánto esperar.
  • No trae headers X-RateLimit-*. No podés medir cuánto cupo te queda.
  • No es JSON. Lo emite nginx antes de llegar a la aplicación, con body HTML.

No publicamos un número de requests por segundo porque no lo podemos respaldar hoy. Preferimos no darte una cifra que después no se sostenga: si tu integración necesita un techo garantizado por escrito, escribinos a soporte@dardo.ai con el volumen que esperás y te lo confirmamos.

Mientras tanto, la regla es simple: espaciá los pedidos y autolimitate del lado del cliente. Concretamente:

  • Serializá. Una request atrás de la otra, no en paralelo sin control.
  • Si necesitás concurrencia, usá una cola acotada (por ejemplo, 4 pedidos en vuelo simultáneos) en lugar de disparar todo junto.
  • Tratá el 429 como una excepción, no como tu mecanismo de control de flujo. Si lo estás viendo seguido, tu cliente está pidiendo demasiado rápido.
  • Espaciá las cargas masivas. Una sincronización nocturna que recorre miles de registros conviene que vaya con pausas deliberadas entre páginas.

Reintentos y backoff

Código¿Reintenta?Cómo
429Backoff exponencial con jitter: 1s, 2s, 4s, 8s. Máximo 4 intentos
5xxIgual, máximo 3 intentos
Timeout de redDependeSeguro en lecturas. En escrituras, leé lo de abajo antes
Resto de 4xxNoEs tu payload o tus credenciales

El jitter —una variación aleatoria en la espera— importa más de lo que parece: sin él, varias cajas del mismo comercio reintentan sincronizadas y vuelven a chocar contra el mismo límite todas juntas.

No hay idempotencia

Esto es lo más importante de esta página. Leelo antes de escribir cualquier reintento.

Ninguna operación de la API acepta una clave de idempotencia. No existe Idempotency-Key, no existe un identificador de request en el body, no hay ningún mecanismo de deduplicación en ningún endpoint.

Traducido: el servidor no tiene forma de saber que dos pedidos son la misma operación. Reintentar una acreditación de puntos después de un timeout puede sumar los puntos dos veces.

Mirá el body de una acreditación (POST /api/v2/cards/{id}/add-point):

{
  "points": 10.5,
  "comment": "Ticket #4821",
  "purchaseSum": 2500
}
CampoTipoRequeridoRestricciones
pointsnumber (float)Mayor a 0, máximo 1000000000
commentstringNoPuede ser nulo
purchaseSumnumber (float)NoPuede ser nulo

No hay ningún campo que el servidor pueda usar para detectar un duplicado. comment te sirve para auditar, pero no deduplica.

El caso peligroso es el timeout, no el 429

Son dos situaciones muy distintas:

  • Un 429 es relativamente seguro: el pedido fue rechazado en el borde, casi con certeza sin llegar a aplicarse. Reintentar está bien.
  • Un timeout de red es ambiguo: la operación pudo haberse procesado del lado del servidor y haberse perdido solo la respuesta. Reintentar a ciegas acredita dos veces.

Patrón defensivo: verificá antes de reintentar

Las escrituras sobre tarjetas —add-point, add-stamp, add-scores, add-visit, add-purchase, add-transaction-amount, redeem-coupon, receive-reward y sus contrapartes subtract-*no se reintentan a ciegas ante un timeout.

Escribí un identificador propio en comment

El número de ticket o el ID de transacción de tu sistema. No lo deduplica el servidor, pero te deja auditar después.

Ante un timeout, consultá antes de reintentar

GET /api/v2/operations acepta cardId, customerId, templateId y un rango startDate / endDate. Filtrá por la tarjeta y la ventana de tiempo de la operación dudosa, y fijate si quedó registrada.

Alternativa más barata para un chequeo rápido: GET /api/v2/cards/{id} te devuelve el balance actual.

Reintentá solo si confirmaste que no se aplicó

Si la operación aparece en el listado, no reintentes: ya está.

Si no podés verificar, no adivines

Mandá la operación a una cola de conciliación manual. Es preferible una revisión humana a una doble acreditación en la cuenta de un cliente real.

async function acreditarConVerificacion(cardId, body) {
  try {
    return await addPoint(cardId, body);
  } catch (err) {
    if (!esTimeout(err)) throw err;

    // Ambiguo: puede haberse aplicado. Verificar antes de reintentar.
    const desde = new Date(Date.now() - 5 * 60 * 1000);
    const ops = await get('/api/v2/operations', { cardId, itemsPerPage: 50 });
    const yaEsta = ops.data.some(
      (o) => o.comment === body.comment && new Date(o.createdAt) >= desde,
    );

    if (yaEsta) return ops.data.find((o) => o.comment === body.comment);
    return await addPoint(cardId, body);
  }
}

Paginación

Parámetros

ParámetroTipoDefaultMínimoMáximo
pageinteger11sin tope declarado
itemsPerPageinteger3011000

Los dos van en la query string y son opcionales.

curl "https://api.dardo.ai/api/v2/customers?page=2&itemsPerPage=100" \
  -H "X-API-Key: TU_CLAVE"

El bloque meta

Las respuestas paginadas agregan meta al envelope de respuesta:

{
  "code": 200,
  "meta": {
    "totalItems": 250,
    "itemsPerPage": 30,
    "currentPage": 1
  },
  "data": []
}
CampoQué es
meta.totalItemsTotal de registros que matchean la consulta, no los de esta página
meta.itemsPerPageTamaño de página efectivo
meta.currentPageNúmero de página devuelta

Endpoints paginados

Estos catorce aceptan page e itemsPerPage y devuelven meta:

Endpoint
GET /api/v2/cards
GET /api/v2/companies
GET /api/v2/customers
GET /api/v2/games
GET /api/v2/locations
GET /api/v2/managers
GET /api/v2/operations
GET /api/v2/promotions
GET /api/v2/pushes
GET /api/v2/segments
GET /api/v2/templates
GET /api/v2/templates/{templateId}/utm-links
GET /api/v2/workflows
GET /api/v2/workflows/{workflowId}/logs

Varios tienen además filtros propios que conviene usar antes de paginar: GET /api/v2/customers acepta phone y email; GET /api/v2/cards acepta templateId, customerId, customerEmail y customerPhone. Filtrar en el servidor siempre le gana a traer todo y filtrar de tu lado.

No hay totalPages ni hasMore

El bloque meta no incluye totalPages, hasMore, nextPage ni cursores. La última página la calculás vos:

const totalPages = Math.ceil(meta.totalItems / meta.itemsPerPage);
const hayMas = meta.currentPage < totalPages;

Caso borde: si totalItems es 0, Math.ceil(0 / 30) da 0 mientras que currentPage es 1. La condición corta bien igual, pero manejá el listado vacío de forma explícita en vez de confiar en la aritmética.

Recorrer una colección completa

async function traerTodo(recurso, filtros = {}) {
  const itemsPerPage = 100; // rango válido: 1 a 1000
  const resultados = [];
  let page = 1;
  let totalPages = 1;
  let intentos = 0;

  do {
    const qs = new URLSearchParams({ ...filtros, page, itemsPerPage });
    const res = await fetch(`https://api.dardo.ai/api/v2/${recurso}?${qs}`, {
      headers: { 'X-API-Key': process.env.DARDO_API_KEY },
    });

    if (res.status === 429) {
      // Sin Retry-After: backoff a ciegas, misma pagina.
      await sleep(1000 * 2 ** intentos++ + Math.random() * 250);
      continue;
    }

    if (!res.ok) {
      throw new Error(`${res.status} req=${res.headers.get('x-request-id')}`);
    }

    intentos = 0;
    const body = await res.json();
    resultados.push(...body.data);

    totalPages = Math.ceil(body.meta.totalItems / body.meta.itemsPerPage);
    page++;

    await sleep(200); // espaciar los pedidos
  } while (page <= totalPages);

  return resultados;
}

Para sincronizaciones grandes: subí itemsPerPage para hacer menos pedidos (el máximo es 1000, pero páginas muy grandes aumentan el riesgo de timeout — empezá en 100 o 200 y subí midiendo), y serializá las páginas. Paginar en paralelo multiplica el riesgo de 429 sin ganancia real.

No hay parámetros de orden

Ningún endpoint de listado acepta sort ni order, y el orden por defecto no está declarado.

Esto no es un detalle académico: si el orden no es estable, paginar una colección que está recibiendo registros nuevos produce duplicados y omisiones silenciosas. Ejemplo concreto: estás recorriendo GET /api/v2/customers en diez páginas y, mientras tanto, se dan de alta clientes. Si el orden es por fecha de creación descendente, cada alta empuja un registro de la página N a la N+1, y ese registro no lo vas a ver nunca.

Para un negocio con movimiento real, esto pasa.

Cómo mitigarlo mientras tanto:

  • Sincronizá en ventanas de baja actividad, cuando no entran registros nuevos.
  • Para cargas incrementales, usá los filtros de fecha (startDate / endDate, formato Y-m-d) sobre rangos ya cerrados —ayer, la semana pasada— en lugar de recorrer toda la colección. Un rango cerrado no cambia mientras lo paginás.
  • Deduplicá por id de tu lado al consolidar los resultados. Es barato y te cubre.

Probar sin romper nada

No hay un ambiente de sandbox separado. No existe un host de pruebas ni un modo que se active por header o parámetro: hay un solo entorno.

Lo que sí existe es probar contra una tarjeta inactiva. Una tarjeta que todavía no activaste se puede instalar igual, hasta diez veces, así que te sirve para hacer el ciclo completo de la integración —emitir, acumular, canjear— sin tocar tu programa real.

Recomendaciones para esa etapa:

  • Empezá por GET /api/v2/profile. Es de solo lectura y valida credencial y conectividad de una.
  • Recorré todos los GET primero. Recién cuando el mapeo de datos esté validado, pasá a las escrituras.
  • Probá el manejo de 429 a propósito antes de salir a producción. Es el error que más va a aparecer con volumen real y es fácil de reproducir.
  • Probá el camino del timeout, no solo el feliz. Es el que puede duplicar operaciones.

Cuidado con borrar plantillas por API. Borrar una tarjeta activa se lleva las tarjetas ya emitidas contra ella, y no pide confirmación. No hay vuelta atrás.

Ver también

On this page