> ## Documentation Index
> Fetch the complete documentation index at: https://docs.commercy.com.ar/llms.txt
> Use this file to discover all available pages before exploring further.

# Referencia de la API

> Endpoints, convenciones, paginación e idempotencia de la API pública v1.

La API pública de Commercy es una API REST en JSON. Todos los ejemplos de esta página usan la versión `2026-10`.

| Qué | Valor |
| - | - |
| URL base | `https://api.commercy.com.ar/api/public/2026-10` |
| Autenticación | `Authorization: Bearer <access_token>` (ver [Autenticación OAuth](/oauth)) |
| Formato | JSON, `Content-Type: application/json` en las escrituras |
| Tamaño máximo del body | 1 MiB |
| CORS | No tiene: llamá a la API desde tu servidor |

## 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 scope `read_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:

```json theme={null}
{ "data": { "id": "6710b3c2e4a1f20012ab34cf", "name": "Sucursal Centro" } }
```

Una colección:

```json theme={null}
{ "data": [ { "id": "..." } ], "next_cursor": "eyJrIjoiNjcxMGIz..." }
```

Los errores tienen su propio formato, descripto en [Errores](/errores).

## Paginación

Las colecciones se paginan con cursor. Parámetros:

| Parámetro | Detalle |
| - | - |
| `limit` | Entero de 1 a 100. Por defecto 50. |
| `cursor` | El `next_cursor` de la página anterior. |

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 header `Idempotency-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/adjustments` y opcional en el resto de los `POST`, `PUT` y `PATCH`. `DELETE` no 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 `5xx` no se guardan, así que podés reintentar con la misma clave.

## Tienda

### `GET /store`

Sin scope. Datos del comercio donde está instalada la app.

```json theme={null}
{
  "data": {
    "id": "6710b3c2e4a1f20012ab34ce",
    "name": "Mi Tienda",
    "currency": "ARS",
    "plan_tier": "professional",
    "shops": [{ "id": "6710b3c2e4a1f20012ab34cf", "name": "Sucursal Centro" }]
  }
}
```

`plan_tier` es `professional`, `enterprise`, `demo` o `developer`.

## Sucursales

Scope `read_shops`.

### `GET /shops` y `GET /shops/:id`

```json theme={null}
{
  "data": {
    "id": "6710b3c2e4a1f20012ab34cf",
    "name": "Sucursal Centro",
    "description": null,
    "image": null,
    "phone": "+5491155550000",
    "email": "centro@mitienda.com",
    "address": {
      "street": "Av. Corrientes",
      "street_number": 1234,
      "cross_streets": null,
      "city": "Buenos Aires",
      "postal_code": "C1043",
      "full": "Av. Corrientes 1234, Buenos Aires"
    },
    "schedule": [{ "day": "monday", "open": "09:00", "close": "18:00" }],
    "is_active": true,
    "created_at": "2026-01-10T15:00:00.000Z",
    "updated_at": "2026-09-30T18:20:00.000Z"
  }
}
```

La sucursal que no pertenece al comercio responde `404`.

## Categorías y marcas

Scope `read_products`. `GET /categories` y `GET /brands` aceptan `limit` y `cursor`.

```json theme={null}
{ "id": "6710...", "name": "Remeras", "slug": "remeras", "parent_id": null, "depth": 0, "is_active": true, "image": null, "updated_at": "2026-09-01T10:00:00.000Z" }
```

Las marcas traen `id`, `name`, `slug`, `is_active`, `image` y `updated_at`.

## Productos

Scope `read_products`.

### `GET /products`

| Filtro | Detalle |
| - | - |
| `updated_since` | Sólo productos modificados desde esa fecha. |
| `sku` | Productos cuyo SKU, o el de alguna variante, coincide. **El SKU no es único**: devuelve una lista. |
| `category` | Id de una categoría del comercio. |
| `brand` | Id de una marca del comercio. |
| `limit`, `cursor` | Paginación. |

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.

```json theme={null}
{
  "data": {
    "id": "6710c0d1e4a1f20012ab3500",
    "name": "Remera básica",
    "slug": "remera-basica",
    "description": "Algodón peinado",
    "short_description": null,
    "sku": "REM-001",
    "barcode": null,
    "type": "simple",
    "images": ["https://cdn.commercy.com.ar/..."],
    "category_ids": ["6710..."],
    "brand_ids": [],
    "tags": ["verano"],
    "shop_ids": ["6710b3c2e4a1f20012ab34cf"],
    "visibility": "PUBLIC",
    "is_active": true,
    "price": 12500,
    "currency": "ARS",
    "has_variants": true,
    "variant_axes": ["talle"],
    "variants": [
      {
        "id": "6710c0d1e4a1f20012ab3501",
        "sku": "REM-001-M",
        "barcode": null,
        "name": "M",
        "attributes": { "talle": "M" },
        "images": [],
        "price": 12500,
        "compare_at_price": null,
        "is_active": true,
        "position": 0
      }
    ],
    "iva_rate": 21,
    "weight": { "value": 200, "unit": "g" },
    "created_at": "2026-01-10T15:00:00.000Z",
    "updated_at": "2026-09-30T18:20:00.000Z"
  }
}
```

`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

Scope `read_prices`.

### `GET /prices`

Precio **resuelto** de cada producto y variante en una sucursal, con el mismo motor que usa el punto de venta.

| Parámetro | Detalle |
| - | - |
| `shop` | **Obligatorio.** Id de una sucursal del comercio. |
| `price_list` | Id de una lista de precios. Si no la mandás se usa la lista por defecto de la sucursal. |
| `product` | Limita el resultado a un producto. |
| `limit`, `cursor` | En este endpoint `limit` va de 1 a **25** (por defecto 25). Una página puede traer menos productos si tienen muchas variantes. |

```json theme={null}
{
  "data": [
    {
      "product_id": "6710c0d1e4a1f20012ab3500",
      "shop_id": "6710b3c2e4a1f20012ab34cf",
      "price_list_id": null,
      "price": 12500,
      "original_price": 12500,
      "source": "base",
      "currency": "ARS",
      "variants": [
        { "variant_id": "6710c0d1e4a1f20012ab3501", "price": 12500, "original_price": 12500, "source": "base" }
      ]
    }
  ],
  "next_cursor": null
}
```

`price_list_id` solo viene informado si el comercio usa listas de precios. `source` indica de dónde sale el precio resuelto.

## Stock

Scope `read_stock`.

### `GET /stock`

Una fila por producto o, si tiene variantes, por variante.

| Parámetro | Detalle |
| - | - |
| `shop` | **Obligatorio.** Id de una sucursal del comercio. |
| `product` | Limita el resultado a un producto. |
| `limit`, `cursor` | La paginación es por producto: una página puede traer más filas que `limit` si hay variantes. |

```json theme={null}
{
  "data": [
    {
      "shop_id": "6710b3c2e4a1f20012ab34cf",
      "product_id": "6710c0d1e4a1f20012ab3500",
      "variant_id": "6710c0d1e4a1f20012ab3501",
      "sku": "REM-001-M",
      "on_hand": 14,
      "available": 12
    }
  ],
  "next_cursor": null
}
```

`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

Scope `read_orders`. Sólo se listan las **ventas** del comercio; las compras a proveedores no aparecen.

### `GET /orders`

| Filtro | Detalle |
| - | - |
| `status` | Estado de la venta (`pending`, `confirmed`, `delivered`, etc.; depende del flujo de estados de la tienda). |
| `updated_since`, `created_since` | Fechas ISO 8601. |
| `shop` | Id de una sucursal del comercio. |
| `limit`, `cursor` | Paginación. |

### `GET /orders/:id`

El `:id` acepta el `id` de la venta o su `order_number`.

```json theme={null}
{
  "data": {
    "id": "6710d1e0e4a1f20012ab3600",
    "order_number": "A-000123",
    "status": "confirmed",
    "source": "storefront",
    "shop_id": "6710b3c2e4a1f20012ab34cf",
    "seller_shop_ids": ["6710b3c2e4a1f20012ab34cf"],
    "customer": { "id": "6710...", "name": "Ana Pérez", "email": "ana@example.com", "phone": "+5491155551111" },
    "line_items": [
      {
        "product_id": "6710c0d1e4a1f20012ab3500",
        "variant_id": "6710c0d1e4a1f20012ab3501",
        "sku": "REM-001-M",
        "name": "Remera básica",
        "quantity": 2,
        "unit_price": 12500,
        "total": 25000,
        "is_custom": false
      }
    ],
    "total": 25000,
    "discount": 0,
    "currency": "ARS",
    "payment": { "gateway": "mercadopago", "status": "approved" },
    "shipping": {
      "method_type": "delivery",
      "method_name": "Envío a domicilio",
      "price": 2500,
      "address": { "street": "Calle Falsa", "street_number": "123", "apartment": null, "city": "CABA", "state": "Buenos Aires", "postal_code": "C1000", "notes": null },
      "recipient_name": "Ana Pérez",
      "recipient_phone": "+5491155551111",
      "recipient_document": "30111222",
      "carrier": null,
      "tracking_number": null,
      "tracking_url": null
    },
    "created_at": "2026-10-09T12:00:00.000Z",
    "updated_at": "2026-10-09T12:05:00.000Z"
  }
}
```

Con el scope `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

Scope `read_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`

| Filtro | Detalle |
| - | - |
| `updated_since` | Fecha ISO 8601. |
| `email` | Coincidencia exacta (sin distinguir mayúsculas). |
| `limit`, `cursor` | Paginación. |

```json theme={null}
{
  "data": {
    "id": "6710e2f1e4a1f20012ab3700",
    "name": "Ana Pérez",
    "email": "ana@example.com",
    "phone": "+5491155551111",
    "document": { "type": "DNI", "number": "30111222" },
    "customer_type": "individual",
    "tags": ["mayorista"],
    "shipping_addresses": [
      { "street": "Calle Falsa", "street_number": "123", "apartment": null, "city": "CABA", "state": "Buenos Aires", "postal_code": "C1000", "notes": null }
    ],
    "created_at": "2026-03-01T10:00:00.000Z",
    "updated_at": "2026-09-01T10:00:00.000Z"
  }
}
```

`document` solo viene informado para un DNI. Los datos fiscales (CUIT, CUIL) nunca se exponen.

## Escrituras

Todas requieren el scope `write_*` 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 /products` crea un producto. **No acepta precio ni costo** (ni en el producto ni en sus variantes): la API responde `400` si los mandás. El producto nace con precio 0 y visibilidad interna; asignale el precio con `PUT /prices/:productId` y publicalo después con `PATCH`.
* `PATCH /products/:id` edita los datos del producto. En las variantes sólo se editan las existentes (por `id`); las variantes nuevas se crean únicamente al crear el producto.
* `DELETE /products/:id` da de baja el producto. Si tiene reservas vigentes responde `409 conflict` con `details[0].reason = "has_reservations"`.

### Precios (`write_prices`)

`PUT /prices/:productId` es el único camino para cambiar precios. Body:

```json theme={null}
{
  "price": 13000,
  "variants": [{ "id": "6710c0d1e4a1f20012ab3501", "price": 13000 }],
  "expected": { "price": 12500 }
}
```

Cambia el precio base del producto y/o de sus variantes (mayores a 0). Con `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):

```json theme={null}
{ "shop": "6710b3c2e4a1f20012ab34cf", "product": "6710c0d1e4a1f20012ab3500", "variant": "6710c0d1e4a1f20012ab3501", "delta": -2, "reason": "Sincronización ERP" }
```

Usá `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/status` con `{ "status": "...", "notes": "..." }` cambia el estado. Los estados permitidos son `confirmed`, `preparing`, `shipped` y `delivered`, 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), responde `409 conflict` con `details[0].reason = "payment_confirmation_required"`: esa confirmación se hace desde Commercy.
* `POST /orders/:id/fulfillment` con `{ "tracking_number": "...", "tracking_url": "..." }` carga los datos de seguimiento. Si la venta no tiene datos de envío responde `409 conflict` con `details[0].reason = "no_shipping"`.

### Clientes (`write_customers`)

* `POST /customers` con `name` (obligatorio), `email`, `phone`, `document` (`{ "type": "DNI", "number": "..." }`) y `shipping_addresses`.
* `PATCH /customers/:id` con los mismos campos, todos opcionales. Solo se admite `DNI` como 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.