Pedidos y tu ERP por API

Registrar y recibir órdenes de compra, y sacar las órdenes aprobadas hacia tu ERP con un cursor, en el formato de tu ERP, devolviendo el número de documento.

En esta página
  1. Registrar la orden que salió
  2. Una orden escrita a mano
  3. Importar órdenes desde un archivo
  4. El ciclo de vida de una orden
  5. El feed de órdenes aprobadas para tu ERP
  6. El archivo en el formato de tu ERP
  7. Devolver el número de documento del ERP

Registrar la orden que salió

POST /inventory/log-po?session_id=... registra una orden basada en las recomendaciones. Sin ella, la orden no existe para StockAI: no se espera su recepción y nunca se aprende el tiempo de entrega real de tus proveedores. Manda lo que decidió el comprador por línea, incluidas las rechazadas:

curl -X POST "https://app.stockai.es/api/v1/inventory/log-po?session_id=$SESSION" \
  -H "Authorization: Bearer $STOCKAI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a0e-1b7d-4c55-9a51-3f0f4e0b8d21" \
  -d '{"items":[{"sku":"SKU-001","recommended_qty":120,"final_qty":100,
                 "status":"modified","unit_cost":2.5,"supplier":"Distribuidora Andina"}]}'

# First call:  201, "replayed": false  -> the order was created
# Same call again (same key, same lines): 200, "replayed": true -> the FIRST order, nothing new
# Same key, different lines: 409 po_idempotency_key_reused
items[].status
approved (tal cual), modified (cambió la cantidad) o rejected (no se pidió). Es la medida de cuánto se sigue a StockAI.
items[].recommended_qty / final_qty
Lo que recomendó StockAI y lo que se pidió.
items[].unit_cost, supplier, supplier_id, warehouse, currency
Opcionales por línea.
destination_warehouse
A qué bodega llega. Si tu llave está limitada a bodegas, debe ser una de las suyas.
cost_center_id
Centro de costo al que se imputa (opcional).
Sin items
Se registran todos los SKUs en PEDIR_YA / PEDIR_PRONTO de la sesión, pero sin decisiones por línea (no cuenta para la adopción).
{
  "id": "fc92e89e-b1c7-4eab-807f-97d173565d19",
  "po_number": 1,
  "session_id": "sess_b70dd9d55a81",
  "sku_count": 1, "total_units": 100.0, "total_value": 250.0,
  "suggested_count": 1, "approved_count": 1, "modified_count": 1, "rejected_count": 0,
  "reception_status": "pending",
  "destination_warehouse": null,
  "source": "forecast",
  "replayed": false
}

Una orden escrita a mano

curl -X POST https://app.stockai.es/api/v1/inventory/po \
  -H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: 0b9a7d4e-2f61-4f0e-9a7b-6c8f1d2e3a45" \
  -d '{"supplier_id":"de7514c0-44b6-41c5-90d6-877801d45fc5",
       "lines":[{"sku":"SKU-004","qty":60,"unit_cost":3.1}],
       "destination_warehouse":"principal"}'

POST /inventory/po crea una orden sin sesión detrás (source: "manual"): supplier_id y lines (sku, qty, unit_cost opcional) obligatorios. Acepta la misma Idempotency-Key. Si el proveedor no existe: 404 supplier_not_found.

Importar órdenes desde un archivo

POST /inventory/po/import/preview y POST /inventory/po/import (multipart file, mapping opcional) crean una orden por cada (referencia, proveedor, bodega) del archivo. Importar el mismo archivo dos veces no duplica. Plantilla: GET /inventory/po/import/template.

El ciclo de vida de una orden

Método y rutaQué haceLlave
GET /inventory/po-history/pageHistorial paginado (limit, offset, status, q).lectura
GET /inventory/po/{id}/itemsLas líneas de una orden.lectura
POST /inventory/po/{id}/sendManda el PDF a cada proveedor por correo y WhatsApp. Los que no tienen contacto se omiten y se informan. Necesita que un administrador de la cuenta tenga el correo verificado.escritura
POST /inventory/po/{id}/receiveRegistra la llegada, total o parcial. Suma el stock y aprende el tiempo de entrega real.escritura
POST /inventory/po/{id}/unreceive, .../unsendDeshace una recepción o un envío marcado.escritura
GET /inventory/po/overdueÓrdenes cuya llegada esperada ya pasó sin recepción.lectura
GET /inventory/status/export-poLa orden sugerida como CSV (session_id, signals, warehouse).lectura
# Partial reception: 60 of the 100 units arrived. received_at is optional (default: now)
# and cannot be earlier than the order date (422 reception_date_before_order).
curl -X POST https://app.stockai.es/api/v1/inventory/po/$PO_ID/receive \
  -H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/json" \
  -d '{"lines":[{"sku":"SKU-001","received_qty":60}]}'

# "data": { "po_log_id": "fc92e89e-...", "reception_status": "partial",
#           "received_at": "2026-10-23T03:00:31.257904+00:00", "lead_time_days": 0.0,
#           "items": [{ "sku": "SKU-001", "final_qty": 100.0, "received_qty": 60.0, ... }] }

El feed de órdenes aprobadas para tu ERP

GET /purchase-orders/export devuelve las órdenes aprobadas que cambiaron desde la última vez que leíste, la más antigua primero, con un cursor para continuar. Es una lectura pura: funciona con una llave de lectura y no marca nada.

curl "https://app.stockai.es/api/v1/purchase-orders/export?limit=100" \
  -H "Authorization: Bearer $STOCKAI_KEY"
{
  "orders": [{
    "id": "fc92e89e-b1c7-4eab-807f-97d173565d19",
    "number": "OC-000001",
    "state": "open",                      // open | partially_received | received | cancelled
    "cancelled_at": null, "cancel_reason": null,
    "created_at": "2026-10-22T20:59:16.750335-06:00",
    "documents": [{                       // one per supplier
      "document_id": "fc92e89e-b1c7-4eab-807f-97d173565d19:-",   // stable on every export
      "number": "OC-000001",
      "supplier": { "id": null, "name": "Distribuidora Andina",
                    "erp_code": null, "mapped": false },          // no erp_code yet: flagged
      "currency": null, "mixed_currency": false,
      "order_date": "2026-10-22",
      "expected_date": "2026-11-06", "expected_date_source": "model",
      "warehouse": "principal",
      "erp_reference": null, "acknowledged": false,
      "lines": [{ "line_id": "d3afc717-a782-4298-9375-b7033d9722be", "sku": "SKU-001",
                  "name": null, "quantity": 100.0, "received_quantity": 0.0,
                  "unit_cost": 2.5, "currency": null, "total": 250.0 }]
    }]
  }],
  "next_cursor": "po_10",
  "has_more": false,
  "held_back": 0
}
since
El next_cursor de la página anterior (tiene la forma po_<número>). Sin él empiezas desde el principio. Un cursor que no emitió este feed se rechaza con 400 feed_cursor_invalid.
limit
Órdenes por página: 100 por defecto, máximo 500.
has_more
true si hay otra página: pídela enseguida con el nuevo cursor.
held_back
Órdenes que cambiaron pero todavía esperan aprobación. No salen hasta aprobarse; cuando se aprueban, vuelven a aparecer solas.
state
open, partially_received, received o cancelled. Una orden cancelada sigue en el feed con ese estado: tu ERP debe anular la suya.
documents[]
Un documento por proveedor. document_id y line_id son los mismos en cada exportación: úsalos para no crear la compra dos veces.
supplier.mapped
false si el proveedor no tiene erp_code: el documento sale igual, marcado, para que no se pierda.
expected_date_source
De dónde sale la fecha esperada: el tiempo de entrega aprendido, o una fecha que el proveedor prometió y una persona aceptó.
CURSOR=$(cat .stockai_cursor 2>/dev/null || true)
while :; do
  PAGE=$(curl -sf "$API/purchase-orders/export?limit=100${CURSOR:+&since=$CURSOR}" -H "$AUTH")
  echo "$PAGE" | jq -c '.data.orders[]' | while read -r ORDER; do
    import_into_erp "$ORDER"            # your code; match on documents[].document_id
  done
  CURSOR=$(echo "$PAGE" | jq -r '.data.next_cursor // empty')
  [ -n "$CURSOR" ] && echo "$CURSOR" > .stockai_cursor     # save AFTER importing
  [ "$(echo "$PAGE" | jq -r '.data.has_more')" = "true" ] || break
done
  1. Lee una página con el cursor que guardaste (ninguno la primera vez).
  2. Importa cada orden en tu ERP, emparejando por document_id.
  3. Guarda next_cursor después de importar, no antes: si tu proceso se cae a la mitad, vuelve a leer lo que no terminó.
  4. Si has_more es true, repite; si no, espera hasta la próxima corrida.
  5. Escribe de vuelta el número que le dio tu ERP a cada documento (abajo).

El archivo en el formato de tu ERP

GET /purchase-orders/export/file?po_ids=<id1>,<id2>&format=<clave> devuelve las órdenes elegidas como el archivo que importa tu ERP, un documento por proveedor. Las canceladas y las que esperan aprobación se rechazan (409 export_order_cancelled, 409 po_approval_required). GET /purchase-orders/export/formats lista los formatos:

formatFormatoExtensiónProbado contra un sistema real
csvCSV genérico.csvSí
xlsxExcel genérico.xlsxSí
sapSAP (estructura BAPI_PO_CREATE1).txtNo — faltan COMP_CODE, PURCH_ORG, PUR_GROUP, PLANT
dynamics_bcDynamics 365 / Business Central.csvNo
netsuiteImportación CSV de NetSuite.csvNo
odooOdoo.csvNo
oracle_ebsOracle E-Business Suite (open interface).csvNo — falta SHIP_TO_ORGANIZATION_CODE
curl "https://app.stockai.es/api/v1/purchase-orders/export/file?po_ids=$PO_ID&format=sap" \
  -H "Authorization: Bearer $STOCKAI_KEY" -o purchase_orders_sap.txt

Devolver el número de documento del ERP

curl -X POST https://app.stockai.es/api/v1/purchase-orders/$PO_ID/erp-reference \
  -H "Authorization: Bearer $STOCKAI_KEY" -H "Content-Type: application/json" \
  -d '{"reference":"4500012345"}'
# Several suppliers on the order? Say which document: {"reference":"...","supplier_id":"de7514c0-..."}

# "data": { "changed": true, "po_log_id": "fc92e89e-...", "reference": "4500012345",
#           "document_id": "fc92e89e-...:-", "number": "OC-000001" }
# The same call again: "changed": false. A different number for the same document: 409.
  • Guarda el número que tu ERP le dio al documento. Es idempotente: repetirlo con el mismo número responde changed: false.
  • Si la orden tiene documentos de varios proveedores, manda supplier_id; si no, 400 erp_reference_supplier_required te lista los proveedores.
  • Un número distinto para un documento que ya tiene uno: 409 erp_reference_conflict. Un número que ya usa otra orden: 409 erp_reference_in_use. Nunca se sobreescribe.
  • Cualquier cambio en una orden la vuelve a poner en el feed, también escribir su referencia: la verás otra vez con acknowledged: true y erp_reference lleno. Tu importador debe saltarse los documentos que ya tiene (por document_id).
  • GET /purchase-orders/unacknowledged lista las órdenes abiertas y aprobadas con documentos que tu ERP todavía no confirmó: tu tarea pendiente.