KicketKicket
Mi cuenta
Guía · Paginación y sincronización▾
  • Inicio
  • Primeros pasos
  • Autenticación
  • Paginación y sincronización
  • Errores
  • Límites de uso
  • Webhooks
  • Conciliación
  • Modelo de datos
  • Referencia
  • Descargas
  • Cambios

API de Kicket · v1

  • Inicio
  • Primeros pasos
  • Autenticación
  • Paginación y sincronización
  • Errores
  • Límites de uso
  • Webhooks
  • Conciliación
  • Modelo de datos
  • Referencia
  • Descargas
  • Cambios

Paginación y sincronización

Cómo recorrer un listado entero y cómo traer sólo lo que cambió, sin perder ni duplicar nada.

Cómo es un listado

Todo listado devuelve el mismo sobre:

Un listado
{
  "object": "list",
  "data": [
    "…"
  ],
  "has_more": true,
  "next_cursor": "eyJrIjpb…"
}
  • limit: cuántos objetos por página, de 1 a 500. Sin limit, 200.
  • next_cursor: pasalo tal cual en cursor para la página siguiente, con los mismos filtros. Si cambiás un filtro, el cursor ya no vale (400 invalid_cursor): empezá sin cursor. Es opaco: no lo armes ni lo leas.
  • has_more: false y next_cursor: null quieren decir que no hay más. Una página puede venir con menos de limit, hasta vacía, y seguir con has_more: true (por ejemplo, cuando un filtro descarta mucho o la página llegó a su tamaño máximo): seguí el cursor igual.
  • El orden es siempre (updated_at, id) ascendente, en todos los listados. Un objeto que cambia mientras paginás vuelve a aparecer más adelante, con su updated_at nuevo: no se pierde. En /payments es el de su venta: (updated_at de la venta, venta, lugar del pago).
  • branch_id (una o varias sucursales separadas por coma) acota el listado; sin él, todas las de la cuenta. GET /branches y GET /payment-methods no se paginan: vienen enteros.

Sincronización incremental: updated_since

updated_since trae lo modificado desde un instante (inclusivo: updated_at ≥ updated_since), con su huso: 2026-10-27T09:55:00-03:00 o …Z. Una fecha sin huso es un 400. Incluye:

  • lo nuevo;
  • lo que se editó, también después de cerrado (una venta corregida, un medio de pago cambiado, una devolución parcial);
  • lo que se anuló (la venta sale con status CANCELED o REFUNDED);
  • lo que se borró, como lápida (ver abajo).

La ventana de asentamiento (60 segundos)

Los listados no sirven lo modificado en los últimos 60 segundos. Una escritura se estampa con una hora y se ve un instante después; si el listado la sirviera enseguida, una que se confirma tarde podría quedar detrás de tu marca y perderse. Por eso lo recién cambiado aparece en la corrida siguiente. Un GET /sales/{id} o GET /products/{id}, en cambio, muestra el objeto como está ahora.

Solapá 5 minutos y deduplicá

En cada corrida pedí updated_since = la marca de la corrida anterior menos 5 minutos. Vas a recibir de nuevo algunos objetos que ya tenías: deduplicá por id + updated_at (si el que tenés es igual o más nuevo, ignorá el que llega). El solapamiento cubre relojes corridos y escrituras lentas; la deduplicación hace que no cueste nada.

Compará updated_at como instantes, no como texto: cada sucursal escribe la fecha con su propio huso (-03:00, -05:00…), y como texto 10:00-03:00 parece anterior a 09:00-05:00 aunque sea después.

Lápidas: lo que se borró

Con updated_since, los listados de ventas, productos y categorías mezclan, en el mismo orden, los objetos borrados (sin updated_since no salen). Una lápida tiene deleted: true y su updated_at es el momento del borrado. Al verla, borrá ese id de tu base (si no lo tenías, ignorala). Un combo borrado sale con object: "product", igual que en el listado.

Una lápida
{
  "id": "6720c7d8c9e2b4d7a1f3eb21",
  "object": "sale",
  "deleted": true,
  "branch_id": "65f1c0a2b7e4d91a3c5e7f10",
  "deleted_at": "2026-10-27T11:40:05.208-03:00",
  "updated_at": "2026-10-27T11:40:05.208-03:00"
}

Los pagos no tienen lápida: cuando se borra una venta, sale su lápida en /sales y con ella se van sus pagos. Los clientes y las cajas no se borran.

La receta completa

  1. Guardá en tu base una marca por recurso: el mayor updated_at que viste. La primera vez, una fecha vieja (por ejemplo, el día que empezaste a usar Kicket): la misma función sirve para la carga inicial y para las siguientes.
  2. Pedí el listado con updated_since = marca − 5 minutos y limit=500, y seguí next_cursor hasta que sea null.
  3. Por cada objeto: si es una lápida, borralo; si no, guardalo sólo si su updated_at es más nuevo que el que tenés.
  4. Al terminar, guardá la marca nueva. Si la corrida se corta, no pasa nada: la próxima arranca desde la marca anterior.
  5. Si a mitad de la corrida te llega un 400 invalid_cursor sin que hayas cambiado nada, reiniciá la corrida desde la marca (sin cursor).
  6. En cada corrida compará GET /branches con las sucursales que ya conocés: una sucursal que entra a la cadena trae su historia con su updated_at viejo, así que para una nueva hacé una carga inicial con branch_id=<nueva> y una fecha vieja antes de seguir con la marca común.
  7. Repetí cada pocos minutos. Si usás webhooks, actuá con el data del evento (o con un GET por id): la sincronización es la red de seguridad. Lo recién cambiado entra en la corrida siguiente, por la ventana de asentamiento.
Sincronización incremental
import { llamar } from './kicket.mjs'

const SOLAPAMIENTO_MS = 5 * 60 * 1000

/**
 * Trae lo que cambió en un recurso (sales, products, categories, customers, cash-sessions, refunds) y lo aplica.
 * `marca`: el mayor updated_at que viste la vez anterior (la primera vez, una fecha vieja).
 * Devuelve la marca nueva: guardala en tu base para la próxima corrida.
 */
export async function sincronizar(recurso, marca, aplicar) {
  const desde = new Date(Date.parse(marca) - SOLAPAMIENTO_MS).toISOString()
  let cursor = null
  let nueva = marca
  do {
    const params = new URLSearchParams({ updated_since: desde, limit: '500' })
    if (cursor) params.set('cursor', cursor)
    const pagina = await llamar(`/${recurso}?${params}`)
    for (const obj of pagina.data) {
      await aplicar(obj)
      // Se comparan INSTANTES, no textos: cada sucursal escribe la fecha con su propio huso.
      if (Date.parse(obj.updated_at) > Date.parse(nueva)) nueva = obj.updated_at
    }
    cursor = pagina.next_cursor // una página puede venir corta (hasta vacía) y seguir: mandá el cursor igual
  } while (cursor)
  return nueva
}

// Cómo se aplica una venta en TU base (db es tu capa de datos).
async function aplicarVenta(obj) {
  if (obj.deleted) return db.ventas.borrar(obj.id) // lápida: se borró en Kicket
  const actual = await db.ventas.buscar(obj.id)
  // Deduplicar por id + updated_at: el solapamiento trae de nuevo lo que ya tenías.
  if (actual && Date.parse(actual.updated_at) >= Date.parse(obj.updated_at)) return
  // La venta entera: sus renglones y sus pagos se REEMPLAZAN (un pago puede cambiar de id al corregirse).
  await db.ventas.guardar(obj)
}

const marca = (await db.marcas.leer('sales')) ?? '2020-01-01T00:00:00Z'
await db.marcas.guardar('sales', await sincronizar('sales', marca, aplicarVenta))
Usa el cliente mínimo de Autenticación. Una página de 500 ventas es un solo pedido: una sincronización cada pocos minutos usa una fracción mínima de los 300 pedidos por minuto.

Qué recurso sincronizar

RecursoCon lápidasNotas
/salesSíTrae los renglones y los pagos adentro: con esto alcanza para ventas y cobros.
/productsSíProductos y combos. El stock no mueve updated_at.
/categoriesSí
/paymentsNoPara sincronizar cobros, mejor /sales. Por venta: reemplazá el conjunto entero de esa venta. Una página no parte los pagos de una venta (salvo una con más pagos que limit). Una ronda abierta o una venta que se quedó sin pagos no sale: de eso te enterás por /sales.
/refundsNoLa nota de crédito emitida después no mueve updated_at: para verla, volvé a pedir /refunds?sale_id=.
/customersNoDatos personales (customers:read). last_purchase_at cambia sin mover updated_at: sacala de /sales.
/cash-sessionsNoSólo cajas cerradas. Una que se reabre deja de salir hasta que se vuelve a cerrar.
AnteriorAutenticaciónSiguienteErrores