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
429como 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 |
|---|---|---|
429 | Sí | Backoff exponencial con jitter: 1s, 2s, 4s, 8s. Máximo 4 intentos |
5xx | Sí | Igual, máximo 3 intentos |
| Timeout de red | Depende | Seguro en lecturas. En escrituras, leé lo de abajo antes |
Resto de 4xx | No | Es 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
}| Campo | Tipo | Requerido | Restricciones |
|---|---|---|---|
points | number (float) | Sí | Mayor a 0, máximo 1000000000 |
comment | string | No | Puede ser nulo |
purchaseSum | number (float) | No | Puede 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
429es 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ámetro | Tipo | Default | Mínimo | Máximo |
|---|---|---|---|---|
page | integer | 1 | 1 | sin tope declarado |
itemsPerPage | integer | 30 | 1 | 1000 |
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": []
}| Campo | Qué es |
|---|---|
meta.totalItems | Total de registros que matchean la consulta, no los de esta página |
meta.itemsPerPage | Tamaño de página efectivo |
meta.currentPage | Nú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, formatoY-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
idde 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
GETprimero. Recién cuando el mapeo de datos esté validado, pasá a las escrituras. - Probá el manejo de
429a 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.