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_atdescendente (lo más reciente primero), con elidpara desempatar. limitva de 1 a 100; por defecto, 25.- Si
has_moreestrue, pide la página siguiente pasandonext_cursortal cual enstarting_after. Cuandohas_moreesfalse,next_cursoresnully terminaste. - El cursor es opaco: no lo construyas ni lo modifiques. Uno alterado responde
400 INVALID_CURSOR.
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
Sección titulada «Filtros por fecha»| 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.
Registros eliminados
Sección titulada «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
Sección titulada «Receta: sincronización incremental»Para mantener una copia de, por ejemplo, los productos en tu sistema:
- Carga inicial: recorre
GET /products?limit=100&include_deleted=truepágina por página hasta quehas_moreseafalse. Antes de empezar, anota la hora de inicio (T0). - Cada corrida siguiente: pide
GET /products?updated_since=<T_anterior>&limit=100&include_deleted=true, recorre todas las páginas y haz upsert porid. Sideleted_atno esnull, márcalo como eliminado en tu sistema. - 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.}