The Xagle Inventory API
The Xagle Inventory API lets you programmatically manage items, stock levels, adjustments, physical counts, purchase order receiving, and units of measure. It is designed around stock reconciliation: pull system quantities, compare against a physical count, review variances, and post the true-up in one atomic batch.
A ready-to-import Postman collection covers every endpoint on this page (plus the rest of the Xagle API).
Authentication & Conventions
Authentication works exactly like the rest of the Xagle API: POST /api/v1/login with your email and password returns a token; send it on every request as Authorization: Bearer {token}.
Responses use the standard envelope: {"success": true, "message": "...", "data": ...} on success and {"success": false, "message": "..."} on failure. Validation failures return {"errors": {"field": ["message"]}} with HTTP 422.
| Code | Meaning |
| 200 | Read or update succeeded |
| 201 | Record created / adjustment or receipt posted |
| 404 | Record not found |
| 422 | Validation or business-rule failure (e.g. over-receiving a PO line) |
| 409 | Duplicate request with the same Idempotency-Key still in flight |
| 400 | Other errors |
Pagination: list endpoints paginate by default. Use page and per_page (maximum 200) to page through results; the standard paginator object is returned under data.
Dates: any standard date format is accepted; ISO 8601 (2026-08-04 or 2026-08-04T14:30:00Z) is recommended.
Quantities: every quantity is expressed in the item's unit of measure, which is included as uom wherever quantities appear.
Items
| Operation | Endpoint |
| List (filterable) | GET /api/v1/items |
| Show | GET /api/v1/items/{id} |
| Lookup by SKU | GET /api/v1/items/sku/{sku} |
| Create | POST /api/v1/items |
| Update | PUT /api/v1/items/{id} |
| Delete | DELETE /api/v1/items/{id} |
| Bulk create/update by SKU | POST /api/v1/items/bulk |
| Kit build capacity | GET /api/v1/items/{id}/buildable |
List filters: search (name / SKU / description), sku, category_id, vendor_id, status (active, on_hold, retired), stockable, low_stock=1, updated_since, sort (name, sku, qty_on_hand, updated_at, created_at, price, standard_cost, status) and direction.
Writable fields: name (required), sku (unique, max 50), description, category_id, unit_of_measure_id, price (selling price), standard_cost, stockable, kit_item_type (no / requires_pre_building / explodes_on_invoice), type (product / service), status, source_method (make / purchase), min_stock_qty (reorder point), max_stock_qty, reorder_qty, primary_vendor_id, default_warehouse_id, lead_time, inventory_notes, company_profile_id, tax_rate_id, tax_rate_2_id.
Important: qty_on_hand is accepted on create only, as an opening balance (it writes an opening movement to the ledger). After creation, stock changes only through adjustments, receipts, and counts — a qty_on_hand value in a PUT body is ignored. Updates replace the fields you send and leave omitted fields unchanged.
Deleting an item is refused (400) when it is used as a kit component or has stock movement history — set its status to retired instead.
Show response embeds the category, unit of measure, default vendor, custom fields, kit components, and a stock block:
{
"success": true,
"message": "Record retrieved successfully",
"data": {
"item": { "id": 30, "name": "Slewing Drive", "sku": "SLEW-9", "stockable": 1, ... },
"stock": {
"quantity_on_hand": 10,
"quantity_committed": 0,
"quantity_on_order": 6,
"quantity_available": 10,
"quantity_backordered": 0,
"reorder_point": 2,
"reorder_quantity": 0,
"uom": "Each",
"last_movement_at": "2026-08-04 14:12:13",
"warehouses": []
}
}
}
quantity_committed counts unfulfilled quantities on sent invoices; quotes do not reserve stock. quantity_available = on hand − committed.
Bulk upsert (max 200 rows) is keyed on SKU and returns per-row outcomes instead of failing the whole batch:
POST /api/v1/items/bulk
{ "items": [ { "sku": "WIDGET-1", "name": "Widget", "stockable": 1, "qty_on_hand": 10 } ] }
HTTP 200
{ "success": true, "data": [ { "index": 0, "sku": "WIDGET-1", "action": "created", "id": 31 } ] }
Buildable reports how many finished units of a kit the current component stock supports, and which component is the binding constraint. Pass ?basis=available to use available (on hand − committed) instead of on hand:
GET /api/v1/items/33/buildable
HTTP 200
{
"success": true,
"data": {
"units_buildable": 4,
"basis": "on_hand",
"binding_constraint": { "sku": "HOSE-KIT", "required_per_unit": 1, "quantity_on_hand": 4, "supports_units": 4 },
"components": [
{ "sku": "STEEL-KIT", "required_per_unit": 2, "quantity_on_hand": 10, "quantity_on_order": 0, "supports_units": 5 },
{ "sku": "HOSE-KIT", "required_per_unit": 1, "quantity_on_hand": 4, "quantity_on_order": 0, "supports_units": 4 }
]
}
}
Stock Levels
| Operation | Endpoint |
| Snapshot of all tracked items | GET /api/v1/inventory |
| One item, with warehouse breakdown | GET /api/v1/inventory/{item_id} |
| Full export for reconciliation | GET /api/v1/inventory/export?format=json|csv |
| Open PO quantities by item | GET /api/v1/inventory/on-order |
Snapshot filters: item_id, sku, low_stock=1, nonzero=1, include_inactive=1. Each row contains item_id, sku, name, quantity_on_hand, quantity_committed, quantity_on_order, quantity_available, quantity_backordered, reorder_point, reorder_quantity, uom, and last_movement_at.
/inventory/export returns the same rows unpaginated; format=csv downloads a CSV file. This is the recommended starting point for a reconciliation script.
/inventory/on-order requires the Purchase Orders add-on and returns per-item ordered / received / outstanding totals with a per-line breakdown of every open PO.
Stock Adjustments
| Operation | Endpoint |
| Post an adjustment | POST /api/v1/inventory/adjustments |
| List adjustments | GET /api/v1/inventory/adjustments |
Body: identify the item by item_id or sku, then send exactly one of quantity_delta (relative change, may be negative) or quantity_absolute (set-to value, for physical counts). reason_code is required: physical_count, damage, shrinkage, found, correction, scrap, transfer_in, transfer_out, or other. Optional: notes, reference (free-text link to a count sheet or document), effective_date, location_id.
POST /api/v1/inventory/adjustments
Idempotency-Key: count-2026-08-04-line-17
{ "sku": "SLEW-9", "quantity_delta": -2, "reason_code": "damage", "notes": "cracked in staging" }
HTTP 201
{
"success": true,
"message": "Adjustment successfully posted.",
"data": {
"movement": { "id": 812, "tran_type": "issue", "qty": "2.0000", "reason_code": "damage", ... },
"quantity_on_hand": 8,
"no_change": false
}
}
- Adjustments are immutable ledger entries — there is no update or delete. Corrections are new offsetting adjustments.
- An absolute set equal to the current on-hand returns 200 with
"no_change": trueand writes nothing. - Concurrent adjustments are safe: the delta for an absolute set is computed under a database row lock.
- If negative stock is disabled for the account, a movement that would take on-hand below zero returns 422 with a clear message.
Idempotency: send an Idempotency-Key header (up to 80 characters) on adjustment and PO receipt POSTs. Retrying with the same key and payload replays the original stored response (marked with the Idempotency-Replayed: true header) without posting again. The same key with a different payload is rejected with 422, and a duplicate arriving while the first request is still processing receives 409. Failed attempts release the key so the request can be retried.
List filters: item_id, reason_code, user_id, from_date, to_date.
Movement Ledger
GET /api/v1/inventory/movements returns every stock movement from every source — receipts, invoice shipments, adjustments, kit builds, transfers, web store orders. The ledger is append-only by design: there are no update or delete endpoints.
Filters: item_id or sku, warehouse_id, type (receipt / issue), source_type (adjustment, invoice, inventory_receipt, kit_build, transfer, po, web_store_order), source_id, reason_code, user_id, from_date, to_date.
Each row includes signed_quantity (negative for issues), unit_cost, reference, and a source_type / source_id pair linking it to the document that caused it.
Physical Count Sessions
The endpoint set that makes a real true-up possible: snapshot the system quantities, enter physical counts, review variances, then post once — atomically.
| Operation | Endpoint |
| List sessions | GET /api/v1/inventory/counts |
| Open a session | POST /api/v1/inventory/counts |
| Show a session | GET /api/v1/inventory/counts/{id} |
| Submit counted quantities (batch) | POST /api/v1/inventory/counts/{id}/lines |
| Variance report | GET /api/v1/inventory/counts/{id}/variances |
| Post the session | POST /api/v1/inventory/counts/{id}/post |
Open: {"name": "August count", "item_ids": [30, 31], "variance_threshold_pct": 50}. Scope is either an explicit item_ids list, or all active tracked items (optionally narrowed by category_id / warehouse_id). Each line snapshots the system quantity at open as expected_qty.
Submit counts: lines may reference line_id, item_master_id, or sku; resubmitting a line overwrites its count:
POST /api/v1/inventory/counts/3/lines
{ "lines": [ { "sku": "SLEW-9", "counted_qty": 11 }, { "sku": "HOSE-KIT", "counted_qty": 4 } ] }
Review: the variance report returns uncounted_lines, variance_lines, pending_approval, and each line's expected / counted / variance figures — so a human can review before committing.
Post: commits every variance as a physical_count adjustment and closes the session in a single transaction — partial posting cannot happen. Posting is blocked while any line is uncounted, or while variances above the session threshold are unapproved (send {"approve_all": 1} to approve them in the same call). A completed session cannot be posted twice.
Purchase Orders & Receiving
Available when the Purchase Orders add-on is enabled.
| Operation | Endpoint |
| List | GET /api/v1/purchase-orders |
| Show (with line statuses) | GET /api/v1/purchase-orders/{id} |
| Create | POST /api/v1/purchase-orders |
| Update header | PUT /api/v1/purchase-orders/{id} |
| Delete | DELETE /api/v1/purchase-orders/{id} |
| Add line items | POST /api/v1/purchase-orders/{id}/items |
| Delete a line item | DELETE /api/v1/purchase-orders/{id}/items/{itemId} |
| Receive against the PO | POST /api/v1/purchase-orders/{id}/receipts |
| Receipt history | GET /api/v1/purchase-orders/{id}/receipts |
List filters: status (draft, sent, approved, paid, rejected, canceled), vendor_id, number, from_date, to_date.
Create: send vendor_id or vendor_name (a new vendor is created if the name is unknown), a required purchase_order_date, and optionally expected_delivery, client_id, company_profile_id, terms, footer, summary, and inline items. Each item takes item_master_id and/or name, a quantity, and an optional cost (defaulting to the item's most recent cost). Totals are calculated automatically.
Receiving supports partial receipts and multiple receipts per PO, and accepts the Idempotency-Key header:
POST /api/v1/purchase-orders/12/receipts
Idempotency-Key: receipt-po12-2026-08-04
{
"lines": [ { "po_line_id": 40, "quantity_received": 6 } ],
"received_on": "2026-08-04",
"tracking_number": "1Z999"
}
HTTP 201
{
"success": true,
"message": "Receipt successfully posted.",
"data": {
"receipt": { "id": 7, "received_on": "2026-08-04 00:00:00", ... },
"lines": [
{ "po_line_id": 40, "quantity_ordered": 10, "quantity_received": 6, "quantity_outstanding": 4, "status": "partial" }
],
"is_fully_received": false
}
}
- Receiving more than the ordered quantity on a line is rejected with 422.
- Each receipt posts
receiptmovements to the stock ledger (source_type: "po"), increasing on-hand for tracked items. - Line
statusisopen,partial, orreceived; useis_fully_receivedfor the whole PO. - Deleting a PO (or a PO line) that has posted receipts is refused — reverse the receipt first from the PO screen.
Units of Measure
| Operation | Endpoint |
| List | GET /api/v1/uoms |
| Show | GET /api/v1/uoms/{id} |
| Create | POST /api/v1/uoms |
| Update | PUT /api/v1/uoms/{id} |
| Delete | DELETE /api/v1/uoms/{id} |
Fields: name (unique) and decimal_places (0–4, display precision). A unit of measure assigned to items cannot be deleted. Units are labels with a display precision — there are no unit conversions, so every quantity you read or write is in the item's own unit.
Reconciliation Walkthrough
GET /api/v1/inventory/export— pull current system quantities keyed by SKU.POST /api/v1/inventory/counts— open a count session scoped to the items you are counting; the system quantities are snapshotted.POST /api/v1/inventory/counts/{id}/lines— submit physical counts by SKU, in batches, as they come in.GET /api/v1/inventory/counts/{id}/variances— review every variance before committing anything.POST /api/v1/inventory/counts/{id}/post— commit all variances in one atomic batch. Every change lands on the movement ledger with reasonphysical_count.GET /api/v1/items/{id}/buildable— see how many finished units your reconciled component stock supports, and which component is the constraint.