Skip to content

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

bash
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

bash
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:

bash
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) and unit_cost are in cents: 2999 = $29.99. For sub-cent costs (ingredients priced per gram) use unit_cost_precise in 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:

bash
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:

bash
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):

bash
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):

bash
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:

bash
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:

bash
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:

bash
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=true buckets 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_expired defaults to true).
  • GET /api/v1/inventory/reorder/suggestions — what to reorder based on reorder points; POST /api/v1/inventory/reorder/draft turns suggestions into draft purchase orders and POST …/reorder/cash-impact previews the cash effect.
  • GET /api/v1/inventory/forecast/skus — per-SKU demand forecasts (POST …/forecast/recompute to 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-chain returns the locations and the open purchase orders and in-transit transfers between them as a map.
  • GET /api/v1/inventory/payments lists 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:

  1. Analyze — POST /api/v1/inventory/import/analyze (multipart file) returns a task_id; poll GET /api/v1/inventory/import/analyze/{task_id} until the AI has mapped the sheets and columns.
  2. Preview — POST /api/v1/inventory/import/preview with the file_id, selected_sheets and (optionally corrected) sheet_mappings returns exactly what would be created or updated.
  3. Apply — POST /api/v1/inventory/import/apply with the file_id and the reviewed products, locations and levels decisions.

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)