Ir al contenido
docs

Editar producto

Cambia nombre, precios, categoría, SKU, clase fiscal o estado.

PATCH /public/v1/products/{id}

Permiso products:write · Productos

El cambio viaja a las cajas SIN stock, unidad ni tipo: cada caja conserva los suyos, así que nunca toca el inventario. Requiere que todas las cajas del negocio tengan la versión de Posdata que lo soporta; si alguna no la tiene responde 409 DEVICES_OUTDATED con la lista y no modifica nada. El cambio llega a las cajas del negocio por su sincronización normal (en segundos si están en línea; si están sin conexión, al reconectarse).

Parámetros

Parámetros de ruta

NombreTipoRequeridoDescripción
idstring (uuid)Sí—

Cuerpo de la petición

JSON (Content-Type: application/json): un objeto ProductUpdate.

Envía al menos 1 campo.

Un campo que no esté en la lista se rechaza con VALIDATION_ERROR.

CampoTipoRequeridoDescripción
namestringNoMáximo 200 caracteres.
descriptionstringNoMáximo 5000 caracteres.
description_shortstringNoMáximo 300 caracteres.
skustringNoMáximo 60 caracteres.
category_idstring (uuid) | nullNo—
base_priceintegerNoSi cambias base_price sin enviar final_price, final_price toma el mismo valor. Mínimo: 0. Máximo: 10000000000.
final_priceintegerNoPrecio al que vende la caja. Mínimo: 0. Máximo: 10000000000.
percent_discountnumberNoMínimo: 0. Máximo: 100.
tax_classstringNoValores: standard, reduced_5, excluded, exempt, service_19, event_package, bag, passthrough.
activebooleanNo—
sellablebooleanNo—

Ejemplo de petición

El cuerpo del ejemplo lleva solo los campos obligatorios, con valores ilustrativos: ajústalos a tus datos.

Terminal
curl -X PATCH "https://api.posdata.so/public/v1/products/3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d" \
  -H "Authorization: Bearer $POSDATA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Café molido 500 g"}'
JavaScript
const res = await fetch('https://api.posdata.so/public/v1/products/3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.POSDATA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Café molido 500 g"
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (${data.error.request_id})`);
Python
import os

import requests

res = requests.patch(
    "https://api.posdata.so/public/v1/products/3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
    headers={
        "Authorization": f"Bearer {os.environ['POSDATA_API_KEY']}",
    },
    json={
        "name": "Café molido 500 g",
    },
    timeout=30,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{data['error']['code']}: {data['error']['message']} ({data['error']['request_id']})")
PHP
<?php
$ch = curl_init('https://api.posdata.so/public/v1/products/3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('POSDATA_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'name' => 'Café molido 500 g',
    ]),
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
    throw new RuntimeException("{$data['error']['code']}: {$data['error']['message']} ({$data['error']['request_id']})");
}

Respuesta

200 Actualizado: un objeto Product.

CampoTipoRequeridoDescripción
object"product"Sí—
idstring (uuid)Sí—
namestringSí—
descriptionstringSí—
description_shortstringSí—
skustring | nullSíSKU del producto (null si no tiene).
category_idstring (uuid) | nullSí—
kindstringSícomposite = se arma desde una receta y no descuenta su propio stock. Valores: simple, composite.
sellablebooleanSífalse = insumo que no se vende por sí solo.
activebooleanSí—
tax_classstringSíClase fiscal del ítem. standard hereda del perfil fiscal del negocio. Valores: standard, reduced_5, excluded, exempt, service_19, event_package, bag, passthrough.
base_priceintegerSíPrecio de lista, antes del descuento propio del producto.
final_priceintegerSíPrecio al que el POS vende. Si el producto no tiene descuento es igual a base_price.
percent_discountnumberSíDescuento del producto sobre base_price, en porcentaje (0-100, puede tener decimales).
base_unitstringSíUnidad base (und, g, kg, lb, oz, ml, l, doc). Stock y costo se expresan en ella.
sold_by_weightbooleanSíSe vende por peso: la cantidad de la línea es un peso en base_unit.
stocknumber | nullSíStock oficial en base_unit (suma de todas sus bodegas y variantes; puede tener decimales en productos por peso y ser negativo si una venta ganó la carrera). Es null mientras la cuenta no lleve el stock en la nube (sus cajas aún en una versión anterior), en productos sin control de stock y en los que se arman con receta. Por bodega: GET /inventory/levels. Para moverlo: POST /inventory/movements o POST /inventory/counts.
unit_costnumberSíCosto promedio ponderado por unidad base (informativo).
image_urlstring (uri) | nullSí—
imagesarreglo de string (uri)Sí—
has_variantsbooleanSí—
variantsarreglo de ProductVariantSíVariantes vigentes (no eliminadas).
deleted_atstring (date-time) | nullSí—
created_atstring (date-time)Sí—
updated_atstring (date-time)Sí—
Ejemplo de respuesta (valores ilustrativos)
{
  "object": "product",
  "id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
  "name": "Café molido 500 g",
  "description": "texto",
  "description_short": "texto",
  "sku": "CAF-500",
  "category_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
  "kind": "simple",
  "sellable": true,
  "active": true,
  "tax_class": "standard",
  "base_price": 25900,
  "final_price": 25900,
  "percent_discount": 19,
  "base_unit": "texto",
  "sold_by_weight": true,
  "stock": 5,
  "unit_cost": 1,
  "image_url": "https://ejemplo.com",
  "images": [
    "https://ejemplo.com"
  ],
  "has_variants": true,
  "variants": [
    {
      "object": "product_variant",
      "id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
      "product_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
      "label": "texto",
      "sku": "CAF-500",
      "base_price": 25900,
      "final_price": 25900,
      "percent_discount": 19,
      "images": [
        "https://ejemplo.com"
      ],
      "is_default": true,
      "active": true,
      "stock": 5,
      "created_at": "2026-09-28T15:04:05.000Z",
      "updated_at": "2026-09-28T15:04:05.000Z"
    }
  ],
  "deleted_at": "2026-09-28T15:04:05.000Z",
  "created_at": "2026-09-28T15:04:05.000Z",
  "updated_at": "2026-09-28T15:04:05.000Z"
}

Errores

Todo error responde con un objeto Error y un request_id. Ramifica por error.code, nunca por el mensaje.

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

VALIDATION_ERRORINVALID_JSONINVALID_CURSOR

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

UNAUTHENTICATEDINVALID_API_KEYAPI_KEY_REVOKEDAPI_KEY_EXPIREDAPI_KEY_WRONG_ENVIRONMENTACCOUNT_INACTIVE

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

PLAN_REQUIREDINSUFFICIENT_SCOPEBROWSER_REQUESTS_NOT_ALLOWED

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

NOT_FOUND

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

CONFLICTSKU_ALREADY_EXISTSIDEMPOTENCY_IN_PROGRESSNO_WAREHOUSEDEVICES_OUTDATEDINSUFFICIENT_STOCKSTOCK_MODE_LEGACY

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

IDEMPOTENCY_KEY_REUSEDCOMPOSITE_STOCK_UNSUPPORTED

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

RATE_LIMITED

Cuerpo: un objeto Error. Qué significa cada código y qué hacer: Errores.

INTERNAL_ERROR