Ir al contenido
docs

Paginación y filtros

Cursores, filtros por fecha y cómo hacer una sincronización incremental confiable.

Listas

Todos los endpoints que listan (GET /products, GET /sales, GET /events…) responden igual:

JSON
{
  "object": "list",
  "data": [ { "object": "sale", "id": "…" } ],
  "has_more": true,
  "next_cursor": "eyJjIjoiMjAyNi0wOS0yOFQxNDo1…"
}
  • El orden es siempre created_at descendente (lo más reciente primero), con el id para desempatar.
  • limit va de 1 a 100; por defecto, 25.
  • Si has_more es true, pide la página siguiente pasando next_cursor tal cual en starting_after. Cuando has_more es false, next_cursor es null y terminaste.
  • El cursor es opaco: no lo construyas ni lo modifiques. Uno alterado responde 400 INVALID_CURSOR.
Terminal
curl "https://api.posdata.so/public/v1/sales?limit=100&starting_after=eyJjIjoiMjAyNi0wOS0yOFQxNDo1…" \
  -H "Authorization: Bearer $POSDATA_API_KEY"

Como el orden es por fecha de creación, un registro que se edita no cambia de posición: puedes recorrer todas las páginas mientras el negocio sigue vendiendo sin saltarte ni repetir registros.

Filtros por fecha

ParámetroQué hace
created_sinceRegistros creados en esa fecha o después (incluye).
created_untilRegistros creados antes de esa fecha (excluye).
updated_sinceRegistros modificados en esa fecha o después. Es el filtro para la sincronización incremental.

Las fechas van en ISO 8601 con zona horaria: 2026-09-28T00:00:00Z o 2026-09-28T00:00:00-05:00. Una fecha sin zona horaria se rechaza con 400 VALIDATION_ERROR, porque no hay forma de saber a qué hora te refieres. En la URL, codifica el + de una zona positiva como %2B.

GET /events no tiene updated_since: los eventos no se modifican.

Además, cada lista tiene sus propios filtros (status, customer_id, sku, q, occurred_since…): los encuentras en la referencia. La búsqueda q es literal: % y _ se buscan como esos caracteres, no como comodines. Un filtro que no existe (o mal escrito) se rechaza con 400 VALIDATION_ERROR; nunca se ignora en silencio, porque ignorarlo te devolvería más datos de los que pediste.

Registros eliminados

Productos, categorías y clientes aceptan include_deleted=true en la lista y en la consulta por id. Sin él, los eliminados no aparecen; con él, aparecen con deleted_at distinto de null. Úsalo en tu sincronización incremental para enterarte de lo que se eliminó en las cajas.

Receta: sincronización incremental

Para mantener una copia de, por ejemplo, los productos en tu sistema:

  1. Carga inicial: recorre GET /products?limit=100&include_deleted=true página por página hasta que has_more sea false. Antes de empezar, anota la hora de inicio (T0).
  2. Cada corrida siguiente: pide GET /products?updated_since=<T_anterior>&limit=100&include_deleted=true, recorre todas las páginas y haz upsert por id. Si deleted_at no es null, márcalo como eliminado en tu sistema.
  3. Guarda como nuevo punto de partida la hora en que empezó la corrida, restándole un margen (por ejemplo, 5 minutos). Procesar dos veces un registro es inofensivo si haces upsert; saltarte uno no.
JavaScript
// Node.js 20+ — incremental product sync (sketch)
const BASE = 'https://api.posdata.so/public/v1';
const headers = { Authorization: `Bearer ${process.env.POSDATA_API_KEY}` };

async function syncProducts(since) {
  const startedAt = new Date();
  let cursor = null;
  do {
    const url = new URL(`${BASE}/products`);
    url.searchParams.set('limit', '100');
    url.searchParams.set('include_deleted', 'true');
    if (since) url.searchParams.set('updated_since', since.toISOString());
    if (cursor) url.searchParams.set('starting_after', cursor);

    const res = await fetch(url, { headers });
    if (!res.ok) throw new Error(`Posdata ${res.status} ${res.headers.get('Request-Id')}`);
    const page = await res.json();
    for (const product of page.data) await upsertProduct(product);
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
  // Next run starts 5 minutes before this one began.
  return new Date(startedAt.getTime() - 5 * 60 * 1000);
}

async function upsertProduct(product) {
  // Save it in your system, keyed by product.id.
}
Las ventas llegan cuando la caja sincroniza

En las ventas, created_at es cuando la venta llegó al servidor; cuándo se registró en la caja lo dice occurred_at. Una caja que trabajó sin internet sube sus ventas al reconectarse, con un created_at de ese momento. Por eso, para la sincronización incremental, filtra por created_since/updated_since (sí te traen esas ventas en la siguiente corrida), no por occurred_since. Usa occurred_at para tus reportes. Lee Cómo se sincroniza con las cajas.