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
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_reuseditems[].statusapproved(as is),modified(quantity changed) orrejected(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_PRONTOSKU 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 route | What it does | Key |
|---|---|---|
GET /inventory/po-history/page | Paged history (limit, offset, status, q). | read |
GET /inventory/po/{id}/items | An order's lines. | read |
POST /inventory/po/{id}/send | Sends 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}/receive | Records arrival, full or partial. Adds to stock and learns the real lead time. | write |
POST /inventory/po/{id}/unreceive, .../unsend | Undoes a reception or a send mark. | write |
GET /inventory/po/overdue | Orders whose expected arrival has passed with no reception. | read |
GET /inventory/status/export-po | The 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(shapedpo_<number>). Without it you start from the beginning. A cursor this feed did not issue is refused with400 feed_cursor_invalid. limit- Orders per page: 100 by default, 500 at most.
has_moretrueif 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.
stateopen,partially_received,receivedorcancelled. A cancelled order stays in the feed with that state: your ERP must void its own.documents[]- One document per supplier.
document_idandline_idare the same on every export: use them so the purchase is never created twice. supplier.mappedfalseif the supplier has noerp_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- Read a page with the cursor you saved (none the first time).
- Import each order into your ERP, matching on
document_id. - Save
next_cursorafter importing, not before: if your process dies halfway, it re-reads what it did not finish. - If
has_moreistrue, repeat; otherwise wait for the next run. - 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:
format | Format | Extension | Tested against a real system |
|---|---|---|---|
csv | Generic CSV | .csv | Yes |
xlsx | Generic Excel | .xlsx | Yes |
sap | SAP (BAPI_PO_CREATE1 layout) | .txt | No — missing COMP_CODE, PURCH_ORG, PUR_GROUP, PLANT |
dynamics_bc | Dynamics 365 / Business Central | .csv | No |
netsuite | NetSuite CSV import | .csv | No |
odoo | Odoo | .csv | No |
oracle_ebs | Oracle E-Business Suite (open interface) | .csv | No — 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.txtWriting 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; otherwise400 erp_reference_supplier_requiredlists 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: trueanderp_referencefilled in. Your importer must skip documents it already has (bydocument_id). GET /purchase-orders/unacknowledgedlists open, approved orders with documents your ERP has not confirmed yet: your to-do list.