# Cómo se sincroniza con las cajas

Qué ve la API, cuándo llegan tus cambios a las cajas y los límites de esta versión. Léelo antes de integrar.

Posdata funciona **sin internet**: cada caja (el computador, la tablet o el celular donde se vende) guarda sus datos localmente y los sube a la nube cuando tiene conexión. La API pública trabaja sobre esa nube. Entender esto te evita reportes equivocados.

## La API ve lo que las cajas ya subieron

Cuando lees ventas, turnos de caja o movimientos, ves lo que las cajas **ya subieron**. Si una caja lleva horas sin internet, sus ventas de esas horas todavía no están en la API; aparecerán cuando se reconecte.

Para saber qué tan al día están los datos, consulta `GET /me` → `data_freshness`:

```json
{
  "data_freshness": {
    "last_device_sync_at": "2026-09-28T14:58:12.000Z",
    "devices": [
      { "name": "Caja 01", "kind": "primary", "platform": "windows", "last_sync_at": "2026-09-28T14:58:12.000Z" },
      { "name": "Caja 02", "kind": "primary", "platform": "android", "last_sync_at": "2026-09-28T09:12:40.000Z" },
      { "name": "Tablet bodega", "kind": "primary", "platform": "android", "last_sync_at": null }
    ]
  }
}
```

- `last_sync_at` es el **último cambio que esa caja subió de verdad** a la nube, no la última vez que se conectó. Es `null` si la caja nunca ha subido nada.
- `last_device_sync_at` es el más reciente de todos.

En el ejemplo, la Caja 02 no sube nada desde las 9:12 (UTC). Puede que no haya vendido, o puede que esté sin internet con ventas pendientes: la API no puede distinguirlo. Antes de cerrar un reporte del día, revisa que las cajas que vendieron hayan subido cambios después de su cierre.

## Las ventas traen dos fechas

Cada venta trae dos momentos distintos:

| Campo         | Qué es                                                                                                                                 |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `occurred_at` | Cuándo se **registró la venta en la caja**, con el reloj de esa caja. Es la fecha del negocio: úsala para reportes.                    |
| `created_at`  | Cuándo la venta **llegó a la nube**. Una caja que vendió sin internet sube sus ventas al reconectarse, y quedan con la hora de subida. |

En una caja con internet las dos fechas casi coinciden. Si la caja vendió el sábado sin conexión y subió el lunes, `occurred_at` dice sábado y `created_at` dice lunes.

> **Ventas sincronizadas antes del 28-sep-2026**
>
> En las ventas que llegaron a la nube antes del 28 de septiembre de 2026, `occurred_at` es igual a la hora de llegada (`created_at`): la hora de la caja no se guardaba.

Qué usa cada cosa:

- **`occurred_since` y `occurred_until`** de `GET /sales` filtran por `occurred_at` (la fecha del negocio).
- **`created_since`, `created_until` y `updated_since`** filtran por la llegada a la nube (y la última modificación). Son los filtros para la **sincronización incremental**: una venta que llega tarde tiene un `created_at` reciente, así que nunca te la saltas. Si sincronizas por `occurred_since`, una venta del sábado que sube el lunes cae en un rango que quizás ya procesaste.
- **El reporte `GET /reports/daily-sales`** suma las ventas **completadas** por día calendario de Bogotá (`America/Bogota`) según `occurred_at`, con un rango de hasta 92 días. La venta del sábado cuenta el **sábado**, pero solo aparece en el reporte cuando la caja la sube: un día puede cambiar después de cerrado.

## Tus cambios llegan a las cajas por su sincronización normal

Cuando creas algo por la API (un producto, una categoría, un cliente, un movimiento o un conteo de inventario) o editas un producto, una variante, una categoría o un cliente, el cambio queda en la nube de inmediato y las cajas lo reciben por su **sincronización normal**:

- **Cajas en línea**: en segundos. Posdata les avisa que hay cambios nuevos.
- **Cajas sin conexión**: cuando se reconecten.

La API responde en cuanto el cambio está guardado en la nube; no espera a que las cajas lo reciban. Para las cajas, tu integración es un dispositivo más del negocio. En las ediciones gana el cambio más reciente: si alguien edita el mismo cliente desde una caja, se queda el que llegue último.

## Productos: se crean y se editan

- **Crear**: `POST /products` crea productos sin variantes, con `initial_stock` opcional.
- **Editar un producto**: `PATCH /products/{id}` cambia el nombre, las descripciones, el SKU, la categoría, los precios (`base_price`, `final_price`, `percent_discount`), la clase fiscal y el estado (`active`, `sellable`).
- **Editar una variante**: `PATCH /products/{id}/variants/{variantId}` cambia su etiqueta, SKU, precios y estado (`active`). Responde el producto completo, con todas sus variantes.

Una edición **nunca toca el inventario**: el cambio viaja a las cajas **sin stock**, y cada caja conserva el suyo. Por eso tampoco se pueden editar el stock, la unidad (`base_unit`), la venta por peso (`sold_by_weight`) ni el tipo de producto (`kind`): cambiarían el significado del stock que ya tiene cada caja. Para cambiar existencias usa [movimientos y conteos](https://developers.posdata.so/stock/).

Si cambias `base_price` sin enviar `final_price`, `final_price` toma el mismo valor.

### Las cajas deben estar actualizadas

Una caja con una versión vieja de Posdata interpretaría una edición sin stock como «stock en cero». Para que eso nunca pase, **editar productos y variantes solo funciona cuando todos los dispositivos del negocio tienen la versión de Posdata que lo soporta**. Si alguno no la tiene, la respuesta es `409 DEVICES_OUTDATED`, **no se modifica nada** y el error trae la lista de dispositivos que faltan:

```json
{
  "error": {
    "code": "DEVICES_OUTDATED",
    "message": "No se modificó nada: estas cajas todavía no tienen la versión de Posdata que acepta ediciones de productos desde la API: Caja 02. Ábrelas con internet para que se actualicen solas (tarda un minuto en reportarse) o, si ya no las usas, revócalas en cuenta.posdata.so → Dispositivos. Crear productos y mover stock sí funciona mientras tanto.",
    "devices": [
      {
        "name": "Caja 02",
        "kind": "primary",
        "platform": "android",
        "app_version": "260901-a1b2c3d",
        "last_seen_at": "2026-09-20T17:42:10.000Z"
      }
    ],
    "request_id": "req_…"
  }
}
```

Cómo resolverlo:

1. **Abre esos dispositivos con internet.** Posdata se actualiza solo y, un minuto después, el dispositivo reporta la versión nueva. Luego reintenta la edición.
2. **Si ya no usas alguno**, revócalo en [cuenta.posdata.so](https://cuenta.posdata.so) → **Dispositivos**. Un dispositivo olvidado en un cajón también cuenta.

`last_seen_at` es la última vez que ese dispositivo se reportó (`null` si nunca lo hizo) y te ayuda a reconocer los que ya no se usan. Mientras tanto, **crear productos y mover stock sí funciona**.

## Stock e inventario

El stock se lleva por **nivel** (bodega, producto, variante): el último conteo físico más los movimientos ocurridos después. Todo el detalle está en [Cómo funciona el stock](https://developers.posdata.so/stock/). Lo esencial para sincronizar:

- **La nube lleva el stock oficial cuando todas las cajas principales del negocio tienen la versión de Posdata que lo soporta** (el paso es automático). Antes de eso, `stock` es `null` en productos y variantes, y `GET /inventory/levels`, `POST /inventory/counts` y los movimientos de variantes responden `409 STOCK_MODE_LEGACY`. Mira [los dos modos](https://developers.posdata.so/stock/#dos-modos-las-cajas-o-la-nube).
- **`POST /inventory/movements`** suma (`in`) o resta (`out`) stock; nunca fija un valor absoluto. Con el stock en la nube, una salida es atómica y nunca compromete las últimas unidades que el negocio reserva para sus cajas (el margen de la API): si no alcanza, responde `409 INSUFFICIENT_STOCK` y no registra nada.
- **`POST /inventory/counts`** registra un conteo físico en el instante en que se hizo (`counted_at`): las ventas posteriores se suman encima, aunque lleguen más tarde.
- **Una entrada puede declarar su costo** (`unit_cost` en `POST /inventory/movements`) y el negocio lleva un costo promedio por producto. `GET /inventory/kardex` te da el libro de un producto con su saldo corrido y `GET /inventory/valuation`, el valor del inventario hoy o a una fecha de corte; los costos y valores exigen el permiso `inventory:costs` y un costo desconocido es siempre `null`, nunca `0` ([detalle](https://developers.posdata.so/stock/#kardex-y-costo-promedio)).
- **Las ventas en las cajas nunca se rechazan.** Si una caja sin internet vende lo que ya se comprometió por otro canal, el nivel puede quedar negativo: Posdata avisa al negocio para que cuente y envía el webhook `inventory.stock.negative`.
- **El stock que ves incluye solo lo que las cajas ya subieron**, igual que las ventas: las ventas de una caja sin internet se aplican cuando se reconecta.

## Dinero, impuestos y cantidades

- **Dinero**: pesos colombianos (COP) en **enteros**, sin decimales ni centavos. `25900` son $25.900.
- **Precios de productos**: tal como los digita el negocio. Lo normal es que incluyan impuestos; si el negocio configuró precios antes de impuestos, vienen así.
- **Tarifas de impuesto**: **porcentaje entero**. `19` es 19 %, nunca `0.19`. Los impuestos por unidad (como el de bolsas) traen `per_unit` y `rate: null`.
- **Impuestos de una venta**: cada línea trae `taxes` congelados al momento de vender, y la venta trae `tax_breakdown` agrupado por tributo y tarifa, con el código DIAN (`01` IVA, `04` INC…). La propina (`tip_amount`) nunca es base gravable. Ingreso neto = `total - tax - tip_amount`.
- **Cantidades**: pueden tener decimales en productos que se venden por peso; se expresan en la unidad base del producto.
- **Fechas**: ISO 8601 en UTC.

## Lo que la API no hace en la versión 1

Para que sepas con qué contar hoy:

- **Publica el stock solo cuando la cuenta lo lleva en la nube**: mientras alguna caja principal no tenga la versión de Posdata que lo soporta, `stock` es `null` y los niveles, los conteos y los movimientos de variantes responden `409 STOCK_MODE_LEGACY` ([detalle](https://developers.posdata.so/stock/#dos-modos-las-cajas-o-la-nube)).
- No edita el stock, la unidad, la venta por peso ni el tipo de un producto con `PATCH` (el stock cambia solo con movimientos y conteos), y solo edita productos y variantes cuando todos los dispositivos del negocio están actualizados (`409 DEVICES_OUTDATED`).
- No mueve ni cuenta stock de productos con receta: muévelo en sus insumos.
- No aparta stock: no hay reservas con vencimiento. Una salida descuenta de inmediato (o, con el stock en la nube, responde `409 INSUFFICIENT_STOCK` si no alcanza), y no hay una operación que descuente varias líneas a la vez o ninguna.
- El margen de la API es uno por cuenta, no por producto ni por bodega.
- No calcula costos por su cuenta: el costo promedio sale de las entradas con `unit_cost` y de lo que el negocio registra en la app. El valor del inventario no incluye los productos sin costo ni los niveles negativos (los lista aparte).
- No crea ventas, facturas ni pedidos externos, y no emite facturas electrónicas de ventas hechas fuera del POS.
- No sube imágenes, no elimina registros y no crea variantes.
- No publica el PDF ni el XML de los documentos electrónicos.
- No ofrece OAuth para aplicaciones de terceros: cada negocio crea sus propias keys.
- No restringe el acceso por dirección IP.

Si alguna de estas limitaciones te frena, cuéntanos tu caso en [Soporte](https://developers.posdata.so/soporte/).

---

Fuente: https://developers.posdata.so/sincronizacion/ · Índice para agentes: https://developers.posdata.so/llms.txt
