Ir al contenido
docs

Registrar movimiento de stock

Suma (in) o resta (out) stock de un producto o de una de sus variantes (variant_id).

POST /public/v1/inventory/movements

Permiso inventory:write · Idempotency-Key obligatorio · Inventario

Una salida out es atómica: nunca compromete las últimas unidades que el negocio reserva para sus cajas (el margen de la API, 2 por defecto, se ajusta en cuenta.posdata.so → Inventario → Configuración). Si no alcanza responde 409 INSUFFICIENT_STOCK con available (lo que sí puedes sacar) y no registra nada; con varias peticiones a la vez por la última unidad, solo una gana. Una entrada in no tiene tope y puede declarar su costo (unit_cost), que alimenta el costo promedio de la cuenta. Los productos que se arman con receta y los packs o combos no tienen stock propio (COMPOSITE_STOCK_UNSUPPORTED). Mientras la cuenta no haya pasado al stock en la nube (las cajas aún en una versión anterior), el stock de variantes responde 409 STOCK_MODE_LEGACY. 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

Encabezados

NombreTipoRequeridoDescripción
Idempotency-KeystringSíObligatorio en todo POST. Un valor único por operación (p. ej. un UUID); reutilízalo solo para reintentar la MISMA operación. Válido 24 h. Máximo 255 caracteres. Formato: ^[A-Za-z0-9_\-:.]{1,255}$.

Cuerpo de la petición

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

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

CampoTipoRequeridoDescripción
product_idstring (uuid)Sí—
variant_idstring (uuid)NoObligatorio si el producto tiene variantes: el movimiento es de esa variante.
warehouse_idstring (uuid)NoPor defecto, la bodega predeterminada (o la única bodega activa).
typestringSíin suma, out resta. Valores: in, out.
quantitynumberSíEn la unidad base del producto. Entero, salvo en productos que se venden por peso (sold_by_weight), que aceptan decimales (0.375 kg). Máximo: 1000000000. Mayor que 0.
reasonstringNoMotivo visible en el historial. Por defecto "Movimiento vía API". Máximo 200 caracteres.
referencestringNoTu referencia (orden de compra, pedido…). Máximo 100 caracteres.
unit_costnumberNoSolo en entradas (type: in): lo que costó cada unidad base. Alimenta el costo promedio ponderado de la cuenta. Si no lo conoces, no lo envíes (nunca 0). Una salida sale siempre al costo promedio. Máximo: 1000000000. Mayor que 0.

Ejemplo de petición

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

Terminal
curl -X POST "https://api.posdata.so/public/v1/inventory/movements" \
  -H "Authorization: Bearer $POSDATA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"product_id":"3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d","type":"in","quantity":5}'
JavaScript
const res = await fetch('https://api.posdata.so/public/v1/inventory/movements', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.POSDATA_API_KEY}`,
    'Idempotency-Key': crypto.randomUUID(), // reutilízala solo para reintentar
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "product_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
    "type": "in",
    "quantity": 5
  }),
});
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 uuid

import requests

res = requests.post(
    "https://api.posdata.so/public/v1/inventory/movements",
    headers={
        "Authorization": f"Bearer {os.environ['POSDATA_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),  # reutilízala solo para reintentar
    },
    json={
        "product_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
        "type": "in",
        "quantity": 5,
    },
    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/inventory/movements');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('POSDATA_API_KEY'),
        'Idempotency-Key: ' . bin2hex(random_bytes(16)), // reutilízala solo para reintentar
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'product_id' => '3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d',
        'type' => 'in',
        'quantity' => 5,
    ]),
]);
$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']})");
}
Idempotency-Key
Usa un valor nuevo por operación y repite el mismo solo para reintentar esa misma operación. Mira Idempotencia.

Respuesta

201 Creado: un objeto StockMovement.

CampoTipoRequeridoDescripción
object"stock_movement"Sí—
idstring (uuid)Sí—
product_idstring (uuid)Sí—
variant_idstring (uuid) | nullSí—
warehouse_idstring (uuid) | nullSínull si el movimiento llegó antes que su bodega.
typestringSíTipo de movimiento tal como lo registró la caja.
quantitynumberSíCantidad en la unidad base del producto (puede ser fraccionaria).
reasonstring | nullSí—
referencestring | nullSíReferencia libre del movimiento.
unit_costnumber | nullSíCosto por unidad base al momento del movimiento.
reason_codestring | nullNoMotivo catalogado de un ajuste: shrinkage, damage, expired, theft, internal_use, supplier_return, found, correction, other o transfer_shortage. null si el movimiento no es un ajuste con motivo.
origin_typestring | nullSísale, purchase, purchase_return (goods given back to a supplier), adjustment, recipe_explosion, reversal (a voided purchase or the restock of an annulled sale), transfer (goods moving between two warehouses of the same business: an out at the origin when it is dispatched, an in at the destination when it is received or at the origin when it is returned; it never changes the cost).
sale_idstring (uuid) | nullSíLa venta que causó el movimiento, si aplica.
created_atstring (date-time)Sí—
updated_atstring (date-time)Sí—
Ejemplo de respuesta (valores ilustrativos)
{
  "object": "stock_movement",
  "id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
  "product_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
  "variant_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
  "warehouse_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
  "type": "texto",
  "quantity": 5,
  "reason": "texto",
  "reference": "texto",
  "unit_cost": 1,
  "reason_code": "texto",
  "origin_type": "texto",
  "sale_id": "3f1c2e8d-7a10-4d0b-9a57-0b6f2f0e5c1d",
  "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.

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