Skip to content

Inventory

Last updated October 14, 2026

Base URL

All examples assume BASE_URL=https://api.omnishelf.com and OMNI_KEY=omni_sk_YOUR_KEY.

Inventory is the other half of the integration: fulfilment tells Omnishelf what left your shelf, inventory tells Omnishelf what is still on it.


How Omnishelf models your stock

Omnishelf keeps three figures per SKU per warehouse, and every response is built from them.

Figure Who sets it Meaning
reported_quantity You, via POST /v1/inventory The last absolute count you sent for that SKU and warehouse.
allocated_quantity Omnishelf, automatically Units taken by orders received since your last count. Resets to zero on an absolute count.
available_quantity Derived reported minus allocated. Can be negative — that is a signal, not an error.

Stock is a reference figure, never a constraint

Omnishelf never blocks, clamps or rounds a figure you send, and never refuses an order because availability is low or negative. A negative available_quantity means more was ordered than your last count covered — treat it as a prompt to recount, not as a failure.

Absolute counts only

There is one write mode: an absolute count. Every entry carries quantity — the number of units physically on the shelf for that SKU at that warehouse right now. Omnishelf does not accept signed deltas; if you know a change rather than a total, add it to your own figure and send the resulting total.

Absolute counts are safe to repeat — sending 40 twice still means 40, so a retry after a timeout can never double-apply.


The loop most integrations run

  1. Once, at setup: call GET /v1/warehouses and store the warehouse_code values.
  2. On a schedule (nightly is typical): push the full count for every SKU with POST /v1/inventory in batches of up to 200 entries per request.
  3. On each stock event (a receipt, a damage write-off, a manual correction): recalculate the shelf total for that SKU and push it as a single absolute-count entry.
  4. Whenever you want to reconcile: call GET /v1/inventory with updated_since and compare available_quantity against your own system.

GET /v1/warehouses

Your locations, and the only warehouse identifier the API exposes. Use warehouse_code verbatim when reading or writing inventory.

Query parameters

Parameter Type Default Notes
is_active Boolean — Exactly true or false. Anything else is a validation_error. Omit to receive both active and inactive locations.
limit Integer 50 Between 1 and 200. Out-of-range values are a validation_error, not a silent clamp.
cursor Opaque string — next_cursor from the previous page. Results are ordered by warehouse_code ascending.

Request

curl -s "$BASE_URL/v1/warehouses?is_active=true" \
  -H "Authorization: Bearer $OMNI_KEY"

Response — 200

{
  "data": [
    {
      "warehouse_code": "MAIN-LA",
      "name": "Main Warehouse LA",
      "is_active": true,
      "address": {
        "line1": "1200 Alameda St",
        "city": "Los Angeles",
        "state": "CA",
        "postal_code": "90021",
        "country": "US"
      }
    }
  ],
  "pagination": { "next_cursor": null, "has_more": false }
}

Errors

  • 400 validation_error — is_active is not true or false, limit is outside 1–200, or the cursor cannot be decoded.
  • 200 with empty data array — no warehouses matching the filter.

Warning

Inactive locations are rejected on write, so filter with is_active=true before caching codes.


GET /v1/inventory

One entry per SKU per active warehouse, as Omnishelf currently holds it. This is the read side of reconciliation: it tells you what Omnishelf believes, so you can compare it with what you believe.

Query parameters

Parameter Type Default Notes
sku String — Your own seller SKU. Exact match, case-insensitive.
warehouse_code String — Restricts the result to one location. An unknown code returns an empty list, not an error.
updated_since ISO 8601 timestamp — Only entries reported at or after this moment. Entries never counted carry a null reported_at and are excluded whenever this filter is supplied.
limit Integer 50 Between 1 and 200. Out-of-range values are a validation_error.
cursor Opaque string — next_cursor from the previous page. Ordering is stable across pages.

Request

curl -s "$BASE_URL/v1/inventory?warehouse_code=MAIN-LA&limit=100" \
  -H "Authorization: Bearer $OMNI_KEY"

Response — 200

{
  "data": [
    {
      "sku": "JJS021B01V1",
      "product_title": "Velvet Display Stand - Black",
      "warehouse_code": "MAIN-LA",
      "stock_available": 128,
      "reported_at": "2026-08-24T06:12:04Z",
      "last_sold_at": "2026-08-25T14:41:10Z"
    },
    {
      "sku": "LMR12BV1",
      "product_title": "Stackable 6-Tier Metal Shoe Organizer",
      "warehouse_code": "MAIN-LA",
      "stock_available": -3,
      "reported_at": "2026-08-11T05:00:00Z",
      "last_sold_at": "2026-08-25T09:02:55Z"
    }
  ],
  "pagination": { "next_cursor": "eyJzIjoiTE1SMTJCVjEi...", "has_more": true }
}

stock_available is the derived figure — your last report minus what orders have consumed since.

For a fulfilment partner, entries held for another seller also carry that seller's seller_code.

GET returns one figure, POST returns three

GET /v1/inventory currently returns only stock_available, which is the same value as available_quantity. It does not return reported_quantity or allocated_quantity. POST /v1/inventory returns all three in its results. If you need the breakdown, keep the results from your last write. The two missing figures may be added to GET later as new fields. Existing fields will not change. The second entry above is negative: the shelf was last counted on 11 August and has been selling since, so a recount is overdue. reported_at going stale is the earliest warning you get; last_sold_at tells you how fast that stock is moving.

Errors

  • 400 validation_error — limit outside 1–200, an updated_since that is not a valid ISO 8601 timestamp, or an undecodable cursor.
  • 200 with empty data array — no SKU matches the filter, or the seller has no active warehouses.

POST /v1/inventory

Report stock to Omnishelf. Up to 200 entries per request, applied as one transaction: either every entry lands or none does.

Request body

Field Type Required Notes
entries Array Required Non-empty, at most 200 items. Each (sku, warehouse_code) pair may appear only once per request.
entries[].sku String Required Your own seller SKU, matched case-insensitively against the SKUs linked to your account. Must match exactly one of your products.
entries[].warehouse_code String Required A warehouse_code from GET /v1/warehouses. Must be an active location.
entries[].quantity Integer Required Absolute count of the shelf. Resets allocated to zero. Negatives are accepted and never clamped.
entries[].seller_code String Fulfilment partners only Picks the seller when a SKU you count is shared by more than one seller you fulfil for. Sellers don't send it.

Request

curl -s -X POST "$BASE_URL/v1/inventory" \
  -H "Authorization: Bearer $OMNI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nightly-2026-08-26-main-la" \
  -d '{
    "entries": [
      { "sku": "JJS021B01V1", "warehouse_code": "MAIN-LA", "quantity": 128 },
      { "sku": "LMR12BV1", "warehouse_code": "MAIN-LA", "quantity": 22 }
    ]
  }'

Response — 200

{
  "applied": 2,
  "unchanged": 0,
  "results": [
    {
      "sku": "JJS021B01V1",
      "warehouse_code": "MAIN-LA",
      "status": "applied",
      "reported_quantity": 128,
      "allocated_quantity": 0,
      "available_quantity": 128
    },
    {
      "sku": "LMR12BV1",
      "warehouse_code": "MAIN-LA",
      "status": "applied",
      "reported_quantity": 22,
      "allocated_quantity": 0,
      "available_quantity": 22
    }
  ]
}

Read the results array rather than assuming your input was echoed back: it reports the post-write state of each line, including the allocation Omnishelf applied. An entry whose figure did not move comes back with status: "unchanged" and is counted in unchanged, which makes a repeated nightly push cheap to verify.

Idempotency

The Idempotency-Key header is optional. When you do send one, reuse within 24 hours replays the stored response instead of writing again. Reuse with a different body returns 409 idempotency_conflict.

Errors

  • 400 validation_error — with an errors array carrying one entry per problem, each naming the sku, warehouse_code and field. Nothing is written when this is returned.
{
  "error": "validation_error",
  "message": "One or more stock updates are invalid.",
  "errors": [
    { "sku": "UNKNOWN-1", "warehouse_code": "MAIN-LA", "field": "sku", "message": "sku is not linked to this seller" },
    { "sku": "JJS021B01V1", "warehouse_code": "OLD-DC", "field": "warehouse_code", "message": "warehouse_code is not an active location for this seller" }
  ]
}
  • 409 idempotency_conflict — the Idempotency-Key was already used within the last 24 hours with a different request body. Use a new key or resend the original body.
  • 429 rate_limit_exceeded — the standard 100 requests per minute per key. Batch entries rather than sending one request per SKU.

Unknown SKUs and inactive warehouses are reported identically whether or not the value exists elsewhere in Omnishelf, so the endpoint never discloses another seller's data. Resolve the named lines and resend the whole batch — with the same Idempotency-Key only if the body is unchanged.

SKUs that match more than one product or seller

Two validation_error messages need a different fix from the rest:

  • A SKU set on more than one of your products is refused, never guessed: sku matches more than one product; contact Omnishelf support to fix the mapping. Omnishelf has to fix the mapping. One bad line rejects the whole batch, so drop that line and resend the rest.
  • Fulfilment partners counting a SKU that two sellers share, without seller_code, get sku belongs to more than one seller; add seller_code.