> ## 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](https://docs.commercy.com.ar/oauth.md)) |
| 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](https://docs.commercy.com.ar/errores.md).

## 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"`.
* `POST /orders/:id/payment` informa que tu plataforma cobró la venta. Ver [Informar un cobro](#informar-un-cobro).

### Informar un cobro

`POST /orders/:id/payment` le avisa a Commercy que tu plataforma ya cobró una venta. Necesita el scope `write_orders` y el mismo permiso del comerciante que `POST /orders/:id/status`. Responde `200` con la venta, igual que el resto de las escrituras de ventas.

```json theme={null}
{ "amount": 25000, "external_id": "pay_8f3a21", "paid_at": "2026-10-09T12:04:00.000Z" }
```

| Campo | Qué es |
| - | - |
| `amount` | Obligatorio. Lo que cobraste, mayor a 0 y con hasta 2 decimales. Tiene que ser **igual al total** de la venta: no hay cobros parciales ni señas. |
| `external_id` | Obligatorio, hasta 120 caracteres. El id del cobro en tu plataforma: lo elegís vos. Commercy lo guarda con tu app como prefijo y no lo expone en la API. |
| `paid_at` | Opcional, fecha ISO 8601. Si no va, o si es futura, se usa el momento del request. |

Qué hace Commercy:

1. **Registra el cobro** en la venta. En la API pública `payment.status` pasa a `approved`.
2. **Emite `order.paid`** con `paid_via: "app"` (ver [Webhooks](https://docs.commercy.com.ar/webhooks.md)). Tu app no recibe su propio evento.
3. **Confirma la venta** si estaba `pending`: pasa al primer paso del flujo de estados de la tienda, que no es necesariamente `confirmed`. Si la venta estaba en otro estado, no se toca.

Registrar el cobro **no mueve plata ni cambia el medio de pago de la venta**: Commercy sólo deja constancia de que tu app lo cobró. Lo mismo vale al revés: si la venta se cancela, la devolución la hacés vos en tu plataforma (recibís `order.cancelled`). Este endpoint no sirve para informar reembolsos: la API pública no los recibe.

#### Respuestas

| Código | Cuándo | Qué hacer |
| - | - | - |
| `200` | Cobro registrado y venta confirmada. | Nada. |
| `200` con `status: "pending"` y `payment.status: "approved"` | El cobro quedó registrado, pero la venta no se pudo confirmar (por ejemplo, falta stock o el flujo de la tienda no lo permite). Commercy le avisa al comercio y la confirma a mano. | **No reintentes**: el cobro ya está. |
| `200` en un reintento | Mandaste otra vez el mismo `external_id` para una venta ya cobrada por tu app. No se registra ni se emite nada de nuevo. Si la venta seguía `pending`, se vuelve a intentar confirmarla. | Nada. |
| `409` | La venta no admite el cobro. El motivo viene en `details[0].reason` (ver abajo). | Corregí el dato o dejá de insistir. |
| `5xx` | Falló algo transitorio (por ejemplo, otro request tuyo estaba confirmando la misma venta en ese momento). | Reintentá con **el mismo** `external_id`. |

Motivos del `409 conflict`:

| `reason` | Cuándo |
| - | - |
| `already_paid` | La venta ya tiene un cobro registrado con otro id, o el cobro ya se confirmó al entregarla. |
| `online_payment_in_progress` | La venta tiene otro cobro online en curso (por ejemplo, un link de pago todavía vigente). Si el intento anterior venció, sí se acepta. |
| `external_id_in_use` | Ese `external_id` ya se usó para el cobro de otra venta del comercio. |
| `amount_mismatch` | `amount` no coincide con el total de la venta o, en un reintento, con el monto del cobro ya registrado con ese `external_id`. |
| `payment_method_not_supported` | El medio de pago de la venta es efectivo, cobro con lector, cuenta corriente o pago dividido en varios medios. |
| `shared_order` | La venta es compartida con otras tiendas y tu app no puede ver el total completo. |
| `order_closed` | La venta está cancelada, rechazada o devuelta, o tiene una devolución parcial. |

Para no cobrar dos veces por error, mandá siempre el mismo `external_id` para el mismo cobro. El header `Idempotency-Key` también sirve (ver la sección Idempotencia más arriba), pero es opcional y la protección no depende de él.

Algunos detalles:

* **Ventas del POS ya cobradas.** Una venta de mostrador que no es a cuenta corriente ya emitió `order.paid` (con `paid_via: "pos"`) al crearse. Sobre esas ventas el endpoint sólo atribuye el cobro a tu app, para conciliar, y no emite un evento nuevo.
* **`payment.status` en ventas sin pago online.** Vale `pending` en toda venta que no tiene un pago online registrado, aunque se haya cobrado en el local. No lo uses para saber si una venta está cobrada si no fue por un medio online o por tu app.

### 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.