Paginación y filtros
Los endpoints de listado comparten el mismo contrato — page y limit son obligatorios y los filtros van como JSON en el query string
Todos los endpoints que terminan en /paginated comparten la misma forma de pedir y de responder.
Pedir una página
GET /accounts/paginated?page=1&limit=25
Authorization: Bearer <YOUR_API_KEY>page y limit son obligatorios. No tienen valor por defecto: si los omitís, la request falla con 400. page arranca en 1, no en 0.
La respuesta
{
"rows": [ /* … los items de esta página … */ ],
"next": true,
"previous": false,
"page": 1,
"perPage": 25,
"totalPages": 12,
"totalRows": 287
}| Campo | Descripción |
|---|---|
rows | Los items de esta página. |
next / previous | Si hay página siguiente / anterior. Útil para paginar sin calcular. |
page | La página que estás viendo. |
perPage | El limit que aplicaste. |
totalPages | Cantidad total de páginas. |
totalRows | Cantidad total de items que matchean, ignorando la paginación. |
Para recorrer todo, iterá mientras next sea true en vez de comparar page con totalPages — te ahorra un off-by-one.
let page = 1;
const all = [];
while (true) {
const res = await fetch(`${BASE}/accounts/paginated?page=${page}&limit=100`, { headers });
const body = await res.json();
all.push(...body.rows);
if (!body.next) break;
page++;
}Filtros
Los listados aceptan un parámetro filters que es un objeto JSON serializado como string dentro del query param. No es un set de query params sueltos.
{
"filters": [
{ "field": "email", "value": "user@example.com" }
],
"matchAll": true,
"includeDeleted": false
}Serializado y URL-encodeado:
GET /accounts/paginated?page=1&limit=25&filters=%7B%22filters%22%3A%5B%7B%22field%22%3A%22email%22%2C%22value%22%3A%22user%40example.com%22%7D%5D%7DEn código, dejá que lo arme el cliente HTTP:
const filters = JSON.stringify({
filters: [{ field: 'email', value: 'user@example.com' }],
matchAll: true,
});
const url = `${BASE}/accounts/paginated?${new URLSearchParams({
page: '1',
limit: '25',
filters,
})}`;Campos del objeto
| Campo | Default | Descripción |
|---|---|---|
filters | [] | Array de condiciones { field, value }. |
matchAll | true | true combina las condiciones con AND; false, con OR. |
includeDeleted | false | Si true, incluye registros borrados lógicamente. |
Cómo se compara cada valor
La semántica depende del tipo del campo y del valor que mandes, no es siempre igualdad:
| Valor que mandás | Sobre una columna de texto | Sobre cualquier otra columna |
|---|---|---|
| Un string | Coincidencia parcial, case-insensitive (ilike %valor%) | Igualdad exacta |
| Un número, boolean o fecha | — | Igualdad exacta |
null | IS NULL | IS NULL |
| Un array | Cualquiera de esos valores (IN), exactos | Cualquiera de esos valores (IN) |
Sobre campos de texto, un string busca por subcadena. { "field": "name", "value": "Juan" } también trae "Juana" y "San Juan". Si necesitás una coincidencia exacta sobre texto, mandá el valor dentro de un array: { "field": "name", "value": ["Juan"] }.
No hay operadores de rango (>, <, between) ni negación.
Campos y valores válidos
Cada endpoint declara qué campos son filtrables. Si mandás uno que no lo es, la respuesta es 400 y el mensaje te lista los campos permitidos:
{
"statusCode": 400,
"message": "Invalid filter field \"nickname\". Allowed fields: id, email, name, externalId, createdAt.",
"code": "BAD_REQUEST"
}Lo mismo con los campos de tipo enum, que además validan el valor:
{
"statusCode": 400,
"message": "Invalid value \"ACTIVO\" for filter \"status\". Allowed values: ACTIVE, PENDING, BLOCKED.",
"code": "BAD_REQUEST"
}Es la forma más rápida de descubrir qué acepta un endpoint: mandá un campo inventado y leé la lista que te devuelve.
Filtros en respuestas sin paginación
El feed (GET /v1/feed/account/{accountId}) y las recompensas ganadas (GET /v1/gamification/missions/my-rewards/{accountId}) aceptan el mismo filters, pero devuelven un array completo: page y limit no aplican.
| Endpoint | Campos filtrables |
|---|---|
| Feed | status (ACTIVE, COMPLETED, EXPIRED), type (MISSION, CHALLENGE, STREAK, ROUTINE), campaignId |
| Recompensas ganadas | campaignId, rewardType (PRODUCTS, FRAGMENTS, POINTS, CHEST), status (DELIVERED, PENDING_STOCK) |
Dos diferencias con los listados paginados:
- La comparación es siempre exacta. Son ids y enums, así que un string no busca por subcadena.
includeDeletedno aplica.
Lo que sí se mantiene igual:
matchAllfunciona como en los listados:truecombina las condiciones con AND,falsecon OR.value: nullmatchea ausencia del campo. Es la forma de pedir, por ejemplo, los ítems del feed que no pertenecen a ninguna campaña.
El mensaje del 400 es el mismo que en el resto de la API, pero el code no: acá viene "INTERNAL_SERVER_ERROR" con "statusCode": 400. Si rameás por code como indica Errores, tratá explícitamente este caso por statusCode, para no reintentar con backoff un filtro mal escrito.
Errores
| Status | Causa |
|---|---|
400 | Falta page o limit, o no son enteros (solo en los listados paginados). |
400 | filters no es un JSON válido, o filters.filters no es un array. |
400 | Un field no es filtrable en ese endpoint, o un valor no pertenece al enum de la columna. |
El detalle del formato está en Errores.
