2026-10.
Versionado
La versión va en la URL y es una fecha (2026-10). Una versión nueva se publica como otro prefijo, sin romper la anterior, y cada versión se mantiene al menos 12 meses después de que sale la siguiente. Una versión inexistente responde 404 not_found.
Convenciones
- Los nombres de campo son
snake_case. - Los ids son strings.
- Los montos son números en pesos argentinos y los recursos con importes traen
"currency": "ARS". - Las fechas son ISO 8601 en UTC, por ejemplo
2026-10-09T12:00:00.000Z. - Los campos sin valor vienen como
null. Los campos de costo sólo aparecen si tu app tiene el scoperead_costs. - Los parámetros y campos desconocidos se rechazan con
400 invalid_request(no se ignoran en silencio), para que no creas que aplicó un filtro que no existe. - Todas las respuestas traen el header
X-Request-Id; mandalo a soporte si necesitás ayuda con un request puntual. - Un recurso de otro comercio responde
404 not_found, igual que uno que no existe. Nunca confirmamos que existe.
Respuestas
Un recurso:Paginación
Las colecciones se paginan con cursor. Parámetros:
Mientras
next_cursor no sea null, hay más páginas. El cursor es opaco y está atado a los filtros con los que se pidió: si lo usás con otros filtros recibís 400 invalid_request. Las listas de productos, ventas y clientes se ordenan por fecha de modificación ascendente, así que podés sincronizar de forma incremental guardando la última fecha vista y pidiendo updated_since.
updated_since y created_since son fechas ISO 8601 e incluyen el instante indicado (>=).
Idempotencia
Mandá un headerIdempotency-Key (1 a 255 caracteres ASCII imprimibles, por ejemplo un UUID) para que un reintento no repita la operación.
- Es obligatorio en
POST /stock/adjustmentsy opcional en el resto de losPOST,PUTyPATCH.DELETEno usa idempotencia. - La clave vale 24 horas y se guarda por instalación.
- Repetir la misma clave con el mismo body devuelve la respuesta original, con el header
Idempotent-Replayed: true. - La misma clave con otro body responde
422 idempotency_conflict. - Si el primer request todavía está en curso (hasta 60 segundos) responde
409 conflict: esperá y reintentá. - Las respuestas
5xxno se guardan, así que podés reintentar con la misma clave.
Tienda
GET /store
Sin scope. Datos del comercio donde está instalada la app.
plan_tier es professional, enterprise, demo o developer.
Sucursales
Scoperead_shops.
GET /shops y GET /shops/:id
404.
Categorías y marcas
Scoperead_products. GET /categories y GET /brands aceptan limit y cursor.
id, name, slug, is_active, image y updated_at.
Productos
Scoperead_products.
GET /products
No se listan los productos en borrador. Un
category o brand que no es del comercio responde 404.
GET /products/:id
Devuelve un producto con sus variantes.
price es el precio base. Para el precio que realmente paga un cliente en una sucursal y lista de precios usá GET /prices. Con el scope read_costs el producto trae además cost, y cada variante su propio cost.
Precios
Scoperead_prices.
GET /prices
Precio resuelto de cada producto y variante en una sucursal, con el mismo motor que usa el punto de venta.
price_list_id solo viene informado si el comercio usa listas de precios. source indica de dónde sale el precio resuelto.
Stock
Scoperead_stock.
GET /stock
Una fila por producto o, si tiene variantes, por variante.
on_hand es el stock físico y available lo que se puede vender (descontadas las reservas). Nunca se exponen lotes, costos ni proveedores.
Ventas
Scoperead_orders. Sólo se listan las ventas del comercio; las compras a proveedores no aparecen.
GET /orders
GET /orders/:id
El :id acepta el id de la venta o su order_number.
read_costs cada línea trae además cogs (costo de la mercadería vendida). customer es null si la venta no tiene un comprador identificable.
Clientes
Scoperead_customers. Sólo los clientes del comercio: no incluye al equipo ni a los contactos que todavía no son clientes.
GET /customers y GET /customers/:id
document solo viene informado para un DNI. Los datos fiscales (CUIT, CUIL) nunca se exponen.
Escrituras
Todas requieren el scopewrite_* correspondiente y devuelven el recurso actualizado en el mismo formato que la lectura. Los POST que crean un recurso responden 201.
Productos (write_products)
POST /productscrea un producto. No acepta precio ni costo (ni en el producto ni en sus variantes): la API responde400si los mandás. El producto nace con precio 0 y visibilidad interna; asignale el precio conPUT /prices/:productIdy publicalo después conPATCH.PATCH /products/:idedita los datos del producto. En las variantes sólo se editan las existentes (porid); las variantes nuevas se crean únicamente al crear el producto.DELETE /products/:idda de baja el producto. Si tiene reservas vigentes responde409 conflictcondetails[0].reason = "has_reservations".
Precios (write_prices)
PUT /prices/:productId es el único camino para cambiar precios. Body:
expected hacés una escritura condicional: si el precio actual no es el esperado responde 409 conflict. En productos con variantes no se acepta price a nivel producto: el precio base se deriva de las variantes.
Stock (write_stock)
POST /stock/adjustments (con Idempotency-Key obligatorio):
delta (entero distinto de 0, suma o resta) o set (cantidad absoluta), nunca los dos. variant es obligatorio si el producto tiene variantes. Si el ajuste dejaría el stock en negativo responde 409 conflict con details[0].reason = "insufficient_stock". Devuelve la fila de stock actualizada.
Ventas (write_orders)
POST /orders/:id/statuscon{ "status": "...", "notes": "..." }cambia el estado. Los estados permitidos sonconfirmed,preparing,shippedydelivered, y tienen que existir en el flujo de estados de la tienda. Nunca se puede cancelar, rechazar ni devolver una venta desde la API. Si la entrega exige confirmar el cobro (por ejemplo un retiro pagado en efectivo), responde409 conflictcondetails[0].reason = "payment_confirmation_required": esa confirmación se hace desde Commercy.POST /orders/:id/fulfillmentcon{ "tracking_number": "...", "tracking_url": "..." }carga los datos de seguimiento. Si la venta no tiene datos de envío responde409 conflictcondetails[0].reason = "no_shipping".
Clientes (write_customers)
POST /customersconname(obligatorio),email,phone,document({ "type": "DNI", "number": "..." }) yshipping_addresses.PATCH /customers/:idcon los mismos campos, todos opcionales. Solo se admiteDNIcomo documento: los datos fiscales no se gestionan por la API.
DELETE /installation
Sin scope. La app se desinstala a sí misma. Responde { "data": { "uninstalled": true } } y el access token deja de valer en el siguiente request.