Ir al contenido

Paginación y filtros

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

{
"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.
Ventana de 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.

Parámetro Qué hace
created_since Registros creados en esa fecha o después (incluye).
created_until Registros creados antes de esa fecha (excluye).
updated_since Registros 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.

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.

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.
// 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.
}