Orders and your ERP over the API

Logging and receiving purchase orders, and taking approved orders out to your ERP with a cursor, in your ERP's format, writing the document number back.

On this page
  1. Logging the order that went out
  2. A hand-written order
  3. Importing orders from a file
  4. An order's lifecycle
  5. The approved-order feed for your ERP
  6. The file in your ERP's format
  7. Writing the ERP's document number back

Logging the order that went out

POST /inventory/log-po?session_id=... logs an order built from the recommendations. Without it the order does not exist for StockAI: its reception is not expected and your suppliers' real lead time is never learned. Send what the buyer decided per line, rejected ones included:

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 (as is), modified (quantity changed) or rejected (not ordered). It is the measure of how much StockAI is followed.
items[].recommended_qty / final_qty
What StockAI recommended and what was ordered.
items[].unit_cost, supplier, supplier_id, warehouse, currency
Optional per line.
destination_warehouse
Which warehouse it arrives at. If your key is limited to warehouses, it must be one of them.
cost_center_id
Cost centre it is charged to (optional).
No items
Every PEDIR_YA / PEDIR_PRONTO SKU of the session is logged, but with no per-line decisions (it does not count for adoption).
{
  "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
}

A hand-written order

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 creates an order with no session behind it (source: "manual"): supplier_id and lines (sku, qty, optional unit_cost) are required. It accepts the same Idempotency-Key. Unknown supplier: 404 supplier_not_found.

Importing orders from a file

POST /inventory/po/import/preview and POST /inventory/po/import (multipart file, optional mapping) create one order per (reference, supplier, warehouse) in the file. Importing the same file twice does not duplicate. Template: GET /inventory/po/import/template.

An order's lifecycle

Method and routeWhat it doesKey
GET /inventory/po-history/pagePaged history (limit, offset, status, q).read
GET /inventory/po/{id}/itemsAn order's lines.read
POST /inventory/po/{id}/sendSends the PDF to each supplier by email and WhatsApp. Those without contact details are skipped and reported. Needs an account administrator with a verified email.write
POST /inventory/po/{id}/receiveRecords arrival, full or partial. Adds to stock and learns the real lead time.write
POST /inventory/po/{id}/unreceive, .../unsendUndoes a reception or a send mark.write
GET /inventory/po/overdueOrders whose expected arrival has passed with no reception.read
GET /inventory/status/export-poThe suggested order as CSV (session_id, signals, warehouse).read
# 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, ... }] }

The approved-order feed for your ERP

GET /purchase-orders/export returns the approved orders that changed since you last read, oldest first, with a cursor to resume from. It is a pure read: it works with a read key and marks nothing.

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
The previous page's next_cursor (shaped po_<number>). Without it you start from the beginning. A cursor this feed did not issue is refused with 400 feed_cursor_invalid.
limit
Orders per page: 100 by default, 500 at most.
has_more
true if there is another page: fetch it right away with the new cursor.
held_back
Orders that changed but are still awaiting approval. They are not served until approved; once approved they come back on their own.
state
open, partially_received, received or cancelled. A cancelled order stays in the feed with that state: your ERP must void its own.
documents[]
One document per supplier. document_id and line_id are the same on every export: use them so the purchase is never created twice.
supplier.mapped
false if the supplier has no erp_code: the document is served anyway, flagged, so it is not lost.
expected_date_source
Where the expected date comes from: the learned lead time, or a date the supplier promised and a person accepted.
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. Read a page with the cursor you saved (none the first time).
  2. Import each order into your ERP, matching on document_id.
  3. Save next_cursor after importing, not before: if your process dies halfway, it re-reads what it did not finish.
  4. If has_more is true, repeat; otherwise wait for the next run.
  5. Write back the number your ERP gave each document (below).

The file in your ERP's format

GET /purchase-orders/export/file?po_ids=<id1>,<id2>&format=<key> returns the chosen orders as the file your ERP imports, one document per supplier. Cancelled orders and orders awaiting approval are refused (409 export_order_cancelled, 409 po_approval_required). GET /purchase-orders/export/formats lists the formats:

formatFormatExtensionTested against a real system
csvGeneric CSV.csvYes
xlsxGeneric Excel.xlsxYes
sapSAP (BAPI_PO_CREATE1 layout).txtNo — missing COMP_CODE, PURCH_ORG, PUR_GROUP, PLANT
dynamics_bcDynamics 365 / Business Central.csvNo
netsuiteNetSuite CSV import.csvNo
odooOdoo.csvNo
oracle_ebsOracle E-Business Suite (open interface).csvNo — missing 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

Writing the ERP's document number back

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.
  • Stores the number your ERP gave the document. It is idempotent: repeating it with the same number answers changed: false.
  • If the order has documents for several suppliers, send supplier_id; otherwise 400 erp_reference_supplier_required lists the suppliers.
  • A different number for a document that already has one: 409 erp_reference_conflict. A number another order already uses: 409 erp_reference_in_use. It is never overwritten.
  • Any change to an order puts it back in the feed, writing its reference included: you will see it again with acknowledged: true and erp_reference filled in. Your importer must skip documents it already has (by document_id).
  • GET /purchase-orders/unacknowledged lists open, approved orders with documents your ERP has not confirmed yet: your to-do list.