Inventory
DayZero's inventory module covers products, variants, purchase orders, shipments, warehouse locations, transfers, cycle counts, and production. This feature requires the Commerce add-on; without it the inventory and purchase-order endpoints return 403 BILLING_003.
Most inventory routes live under /api/v1/inventory/…; purchase orders are at /api/v1/purchase-orders and customer order forms at /api/v1/orders.
Products
Create a Product
curl -X POST "https://api.ondayzero.com/api/v1/inventory/products" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "Widget Pro",
"type": "manual",
"category": "Hardware"
}'name and type are required. type is the product's source: manual (created here) or shopify (synced from a connected store — don't create these yourself). Set tracks_expiration: true for perishable goods so cost lots carry expiry dates and are consumed earliest-expiry first (FEFO) instead of oldest first (FIFO). PUT /api/v1/inventory/products/{id} updates a product (including archived), GET fetches one, and DELETE removes it. POST …/products/{id}/convert-to-variant folds a product into another product as a variant.
List Products
curl "https://api.ondayzero.com/api/v1/inventory/products?type=manual&include_variants=true&archived=false" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"| Parameter | Type | Description |
|---|---|---|
type |
string | manual or shopify |
category |
string | Filter by category |
search |
string | Match on product name (minimum length applies) |
include_variants |
boolean | Embed variant data in each product |
archived |
boolean | true only archived, false only live, omit for all |
Plus the standard pagination parameters.
Variants
Products can have multiple variants (sizes, colors, etc.). Variants are what you stock, price and put on invoices:
curl -X POST "https://api.ondayzero.com/api/v1/inventory/products/variants" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"product_id": "PRODUCT_UUID",
"name": "Widget Pro - Large",
"sku": "WP-LG-001",
"barcode": "0123456789012",
"unit_price": 2999,
"unit_cost": 1250,
"uom": "EA",
"initial_inventory_quantity": 100
}'
unit_price(sale price) andunit_costare in cents:2999= $29.99. For sub-cent costs (ingredients priced per gram) useunit_cost_precisein dollars, e.g.0.0478.
Other variant fields: manufacturer_sku, inventory_category (raw_material, packaging or finished_good, which decides the inventory ledger production runs and counts post to), default_vendor_id, and initial_inventory_quantity_date. A sku or barcode already used by another variant returns 409. If the business has Stripe connected, the variant is also mirrored to the Stripe catalog.
GET /api/v1/inventory/products/variants?product_id=…&archived=false lists variants; PUT / DELETE /api/v1/inventory/products/variants/{id} update or remove one; PATCH …/products/variants/{variant_id}/locations/{location_id}/reorder-settings sets reorder_point and reorder_quantity; GET /api/v1/inventory/products/lookup?sku=WP-LG-001 (or ?barcode=…) finds one for scanning workflows. PUT /api/v1/inventory/products/variants/{id}/location-quantities sets opening quantities per warehouse. Unit costs can be bulk-edited with GET …/products/unit-costs/export (CSV), POST …/unit-costs/preview and POST …/unit-costs/apply (rows of sku and new_unit_cost_cents).
Purchase Orders
Purchase orders to vendors — and the shipments, receipts and bills that follow them — live under /api/v1/purchase-orders:
curl "https://api.ondayzero.com/api/v1/purchase-orders?status=submitted&vendor_id=VENDOR_UUID" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"Statuses are draft, outstanding, submitted, in_production, ready, shipped, partially_received, received, completed and canceled; a typical order runs draft → submitted → in_production → ready → shipped → partially_received / received → completed. The list also takes search (PO number, name, description, vendor or status) and cursor pagination. POST /api/v1/purchase-orders creates one:
curl -X POST "https://api.ondayzero.com/api/v1/purchase-orders" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"vendor_id": "VENDOR_UUID",
"status": "draft",
"name": "January restock",
"po_number": "PO-2026-001",
"line_items": [{"variant_id": "VARIANT_UUID", "quantity": 100, "unit_price": 1200}],
"fees": [{"name": "Shipping", "amount": 5000}],
"expected_delivery_date": "2026-02-01"
}'vendor_id, status, name and po_number are required; amounts are cents. Optional: description, shipping_cost, ship_from_location_id, ship_to_location_id, payment_term_id and expected_paid_on. Lifecycle transitions have their own endpoints: POST /api/v1/purchase-orders/{id}/submit, mark-in-production, mark-ready, mark-shipped, mark-partially-received, receive and cancel; POST …/{id}/payments records a vendor payment. Receiving against a PO updates on-hand quantities, and GET /api/v1/inventory/accounts-payable lists the vendor bills generated from POs with their three-way-match status (PO ↔ receipt ↔ bill). Customer-facing order forms are separate: GET / POST /api/v1/orders.
Locations
Warehouse and storage locations (location_type is one of warehouse, store, virtual, drop_ship, office, supplier, manufacturer, packaging, copacker, third_party_logistics, other; default warehouse):
curl "https://api.ondayzero.com/api/v1/inventory/locations" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"The list takes location_type, include_archived and cursor pagination. POST /api/v1/inventory/locations creates one (name required; optional code, address fields, country defaulting to US, is_default, contact fields and notes); PUT / DELETE …/locations/{location_id} update or delete (a location with inventory or the default location cannot be deleted), and POST …/locations/{location_id}/archive archives one (not the default); POST …/locations/{location_id}/set-default chooses where unlocated stock movements land; GET …/locations/{location_id}/inventory shows what is on hand there and GET …/locations/summaries compares all locations at once.
Shipments
Shipments record vendor deliveries against purchase orders (for stock moving between your own locations use transfers):
curl "https://api.ondayzero.com/api/v1/inventory/shipments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"POST /api/v1/inventory/shipments creates one with line_items (variant_id, quantity, optional unit_price), optional fees, status (default draft), tracking_number, shipped_date, expected_arrival_date and the destination location_id. A shipment with status shipped marks its linked purchase order shipped; status received adds the quantities to on-hand stock at location_id and marks the purchase order received. PUT / DELETE /api/v1/inventory/shipments/{id} update or delete one, and moving a shipment out of received reverses the stock it added.
Transfers
Move inventory between locations:
curl -X POST "https://api.ondayzero.com/api/v1/inventory/transfers" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"from_location_id": "LOCATION_UUID_1",
"to_location_id": "LOCATION_UUID_2",
"items": [
{"variant_id": "VARIANT_UUID", "quantity": 10}
]
}'A transfer is created as a draft (statuses are draft, pending, in_transit, received, cancelled). POST …/transfers/{transfer_id}/ship deducts the stock from the source and moves it in transit; it fails with 400 if any item lacks available quantity. POST …/transfers/{transfer_id}/receive takes items of {transfer_item_id, received_quantity, has_damage, damage_notes} and lands the stock at the destination. POST …/cancel abandons it and reverses the deduction if it had shipped. Items can be added or removed while it is open (POST …/{transfer_id}/items, DELETE …/items/{item_id}). Source and destination must differ. Optional fields: carrier, tracking_number, expected_arrival_date, shipping_cost (cents). GET /api/v1/inventory/transfer-suggestions/suggestions proposes site-to-site rebalancing.
Adjustments
Record a manual inventory adjustment (shrinkage, corrections, etc.) — one variant per call:
curl -X POST "https://api.ondayzero.com/api/v1/inventory/adjustments" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID" \
-H "Content-Type: application/json" \
-d '{
"variant_id": "VARIANT_UUID",
"location_id": "LOCATION_UUID",
"quantity_change": -5,
"reason": "Damaged in transit",
"reason_code": "damage"
}'quantity_change is a signed integer (negative removes stock) and cannot be zero. reason is required; reason_code (one of shrinkage, damage, expired, count_variance, theft, transfer, return, receipt_correction, found, other) overrides it for reporting. If location_id is omitted the default location is used; an archived location is refused. The adjustment is valued and posted to the books: a decrease consumes cost lots (oldest first) and debits Inventory Adjustments / credits Inventory, an increase creates a cost lot at the variant's unit_cost and debits Inventory / credits Inventory Adjustments (the category ledgers from your ledger settings are used when configured). For a full stock take, use cycle counts instead: POST /api/v1/inventory/counts starts one (location_id required, optional notes), POST …/counts/{count_id}/lines and PUT …/counts/{count_id}/lines/{line_id} record counted quantities, and POST …/counts/{count_id}/post books the variances. POST …/counts/{count_id}/preview-post shows the per-category book vs counted values, the journal lines and a plan_hash you can pass to post so it is refused if anything moved; POST …/counts/{count_id}/cancel abandons a count. Counts move through draft, in_progress, posted and cancelled; lines can be added by variant_id, barcode or sku. GET /api/v1/inventory/reports/adjustment-reasons summarises adjustments by reason.
Production
Manage recipes (bills of materials) and production runs for manufactured goods:
curl "https://api.ondayzero.com/api/v1/inventory/production/recipes" \
-H "Authorization: Bearer dz_your_token_here" \
-H "x-business-id: YOUR_BUSINESS_ID"POST /api/v1/inventory/production/recipes creates a recipe (name plus materials, output_variants and processing_steps); PUT, DELETE and POST …/recipes/{recipe_id}/duplicate manage them. POST /api/v1/inventory/production/runs creates a run from a recipe (recipe_id, total_units, optional unit_sale_price in cents) and computes its costs immediately; PUT …/runs/{run_id} changes total_units or price and recalculates. POST …/runs/{run_id}/calculate is a what-if that overrides total_units / unit_sale_price without saving, and POST …/runs/{run_id}/complete consumes the material cost lots and creates output lots. When the inventory ledger settings are mapped, completion also posts a production transfer entry (credit raw materials and packaging, credit accrued inventory for processing and tolling cost, debit finished goods).
Ledger settings and costing
Inventory is valued from cost lots: each receipt, finished-goods output and positive adjustment creates a lot, and sales, production and negative adjustments consume lots oldest first (FIFO), or earliest-expiry first for products with tracks_expiration.
GET /api/v1/inventory/ledger-settings returns the ledger map and PUT changes it (omitted fields are kept, null clears one; every ID must be an active ledger of the business): raw_materials_ledger_id, packaging_ledger_id, finished_goods_ledger_id, manufacturing_fees_ledger_id, accrued_inventory_ledger_id and inventory_adjustments_ledger_id. configured becomes true once the finished-goods ledger is set; production runs and counts then post to the category ledgers.
cogs_basis in the same settings is fifo (default: cost of goods sold comes from cost lots or the variant unit cost) or standard (from the standard cost in force on the sale date). Standard costs are managed under /api/v1/inventory/cogs/standard: GET lists them (variant_id, as_of), PUT sets them from rows or csv_text (ranges for one variant may not overlap; dry_run previews), POST …/preview?from_date=…&to_date=… shows what re-rating sales in a window would change and returns a plan_hash, and POST …/apply queues the adjusting journal entry for approval. GET /api/v1/inventory/landed-cost-allocations/orders lists purchase orders that can absorb extra cost, and GET / PUT /api/v1/inventory/landed-cost-allocations/{bill_id} read or replace how a bill's freight and fees are allocated onto received goods.
Reports and planning
GET /api/v1/inventory/reports/valuation?group_by=location|category|vendor— current on-hand value in cents from remaining cost lots;…/reports/aging?detail=truebuckets lot age (0-30, 31-60, 61-90, 91-180, 180+ days) and…/reports/margin-by-sku?lookback_days=…shows margins and sales velocity.GET /api/v1/inventory/cost-history?months=12— landed COGS over time (1-36 months);GET /api/v1/inventory/lots/expiring?within_days=30— cost lots approaching expiry (include_expireddefaults totrue).GET /api/v1/inventory/reorder/suggestions— what to reorder based on reorder points;POST /api/v1/inventory/reorder/draftturns suggestions into draft purchase orders andPOST …/reorder/cash-impactpreviews the cash effect.GET /api/v1/inventory/forecast/skus— per-SKU demand forecasts (POST …/forecast/recomputeto refresh).GET /api/v1/inventory/activity?limit=20— the most recent inventory events (purchase-order status changes, count postings and similar), newest first, up to 100.GET /api/v1/inventory/supply-chainreturns the locations and the open purchase orders and in-transit transfers between them as a map.GET /api/v1/inventory/paymentslists payments made to vendors for purchase orders.
Import
Bulk import products, variants, locations and stock levels from a spreadsheet using a 3-phase AI-assisted flow:
- Analyze —
POST /api/v1/inventory/import/analyze(multipartfile) returns atask_id; pollGET /api/v1/inventory/import/analyze/{task_id}until the AI has mapped the sheets and columns. - Preview —
POST /api/v1/inventory/import/previewwith thefile_id,selected_sheetsand (optionally corrected)sheet_mappingsreturns exactly what would be created or updated. - Apply —
POST /api/v1/inventory/import/applywith thefile_idand the reviewedproducts,locationsandlevelsdecisions.
The same pattern under /api/v1/inventory/po-import/… turns a manufacturer's order spreadsheet into a purchase order with SKU matching.
Errors
| Status | code |
Cause |
|---|---|---|
403 |
BILLING_003 |
The business does not have the Commerce add-on |
409 |
CONFLICT_* |
Creating a product or variant whose sku or barcode is already used |
400 |
VALIDATION_174 |
Shipping a transfer when the source location lacks available quantity |
400 |
GEN_002 |
Body failed validation (for example a transfer item with quantity of 0, or an adjustment with quantity_change of 0) |