> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnicommerce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Inventory and marketplace listing gaps

> Find active products with available stock and no recorded listing across any store of selected marketplaces.

Use `GET /api/v1/inventory` to answer questions such as “Which products have
stock but no Lazada listing in any of my stores?” The endpoint and the inventory
assistant use the same stock calculations and listing checks.

Requests require `catalog:read` and the caller's inventory read permission.
The bearer credential determines the organization and permitted inventory.
Do not send `organizationId`.

## Find products with stock and no listing

Combine `status=in_stock`, `unlistedMarketplaces=lazada`, and `groupBy=product`:

```bash theme={null}
curl --get "https://omnicommerce.sg/api/v1/inventory" \
  --header "Authorization: Bearer $OMNI_API_KEY" \
  --data-urlencode "status=in_stock" \
  --data-urlencode "unlistedMarketplaces=lazada" \
  --data-urlencode "groupBy=product" \
  --data-urlencode "limit=30" \
  --data-urlencode "offset=0"
```

This returns one page in `groups`. Each product group includes its identifier,
name in `label`, SKU, and available units in `totalQuantity`. A parent's
quantity rolls up its active variants. `in_stock` requires positive available
stock, calculated from canonical on-hand stock minus locked units.

Use `unlistedMarketplaces` for the marketplace where listings must be absent.
The separate `marketplace`, `country`, and `storeIds` filters narrow the source
inventory. For example, `marketplace=shopee` together with
`unlistedMarketplaces=lazada` finds Shopee-associated inventory without a
recorded Lazada listing. To search the whole inventory, omit the source filters.

## What counts as listing presence

The absence check covers every organization store for each selected
marketplace, independently of source inventory filters. It checks listing
identities on the product and its variants, plus exact catalog SKU matches on
other product families. SKU matching trims whitespace and ignores case.

Disabled and historical listing identities count as recorded presence.
Marketplace configuration or a seller SKU alone does not establish that a
listing exists. Families without a usable catalog SKU for duplicate matching
are excluded from confirmed results and counted in
`listingPresence.unknownProductCount`.

`listingPresence.source` is `saved_catalog`: the result reflects stored catalog
evidence. It does not perform a fresh live check of marketplace shops.

The filter supports every marketplace listed in the
[API reference](https://docs.omnicommerce.ai/api-reference.md). Repeat the query parameter to require absence
from multiple marketplaces:

```text theme={null}
unlistedMarketplaces=lazada&unlistedMarketplaces=shopee
```

A product must have no recorded listing on either selected marketplace to
match this example. SDKs accept an array for the same filter.

## Retrieve the complete list

Each request returns at most 30 groups. Continue with the returned `nextOffset`
and identical filters until `hasMore` is `false`.

| Field | Meaning |
| - | - |
| `groups` | The current page of matching groups. |
| `totalGroups` | The full matching group count, independent of the current page. |
| `hasMore` | Whether another page remains. |
| `nextOffset` | The offset for the next request, or `null` at the end. |
| `listingPresence.complete` | Whether all scoped families could be checked for recorded listing presence. This is separate from pagination completion. |
| `listingPresence.unknownProductCount` | Families excluded because duplicate-SKU checks could not be completed. |

Reconcile collected rows against `totalGroups`, rather than `stats.totalSKUs`,
which counts stock-bearing SKUs rather than product groups. The following
examples bound requests using the first matching count and reject incomplete
or changing results before reporting a complete list.

```ts TypeScript theme={null}
import { OmniClient } from "@omni-commerce/sdk";

const client = new OmniClient(); // Reads OMNI_API_KEY.
const filters: Parameters<typeof client.inventory.list>[0] = {
  status: "in_stock",
  unlistedMarketplaces: ["lazada"],
  groupBy: "product",
  limit: 30,
};

let offset = 0;
let page = await client.inventory.list({ ...filters, offset });
const expectedTotal = page.totalGroups;
const maxPages = Math.max(1, Math.ceil(expectedTotal / 30));
const records: typeof page.groups = [];

for (let pagesRead = 0; pagesRead < maxPages; pagesRead++) {
  if (page.totalGroups !== expectedTotal || !page.listingPresence?.complete) {
    throw new Error("Inventory changed or listing checks are incomplete.");
  }
  records.push(...page.groups);
  if (!page.hasMore) break;

  const next = page.nextOffset;
  if (
    next === null ||
    !Number.isSafeInteger(next) ||
    next <= offset ||
    next >= expectedTotal ||
    pagesRead + 1 === maxPages
  ) {
    throw new Error("Incomplete inventory pagination.");
  }
  offset = next;
  page = await client.inventory.list({ ...filters, offset });
}

if (
  records.length !== expectedTotal ||
  new Set(records.map((record) => record.key)).size !== expectedTotal
) {
  throw new Error("Inventory rows are missing or duplicated.");
}
console.log(`${records.length} matching product records`, records);
```

```python Python theme={null}
from omnicommerce import Omni

filters = {
    "status": "in_stock",
    "unlisted_marketplaces": ["lazada"],
    "group_by": "product",
    "limit": 30,
}

with Omni() as client:  # Reads OMNI_API_KEY.
    offset = 0
    page = client.inventory.list(**filters, offset=offset)
    expected_total = page["totalGroups"]
    max_pages = max(1, (expected_total + 29) // 30)
    records = []

    for pages_read in range(max_pages):
        if (page["totalGroups"] != expected_total or
                not page.get("listingPresence", {}).get("complete")):
            raise RuntimeError("Inventory changed or listing checks are incomplete.")
        records.extend(page["groups"])
        if not page["hasMore"]:
            break

        next_offset = page["nextOffset"]
        if (type(next_offset) is not int or next_offset <= offset or
                next_offset >= expected_total or pages_read + 1 == max_pages):
            raise RuntimeError("Incomplete inventory pagination.")
        offset = next_offset
        page = client.inventory.list(**filters, offset=offset)

    if (len(records) != expected_total or
            len({record["key"] for record in records}) != expected_total):
        raise RuntimeError("Inventory rows are missing or duplicated.")
    print(f"{len(records)} matching product records", records)
```

Python arguments use snake\_case, while response keys remain camelCase.
`AsyncOmni` exposes the same method with `await`.

For larger exports, save each successful page and its continuation together.
Offset pages are not immutable snapshots; rerun the report if the catalog
changes during collection. A failed request or unresolved listing check must
remain an incomplete report.

## Interpret product records and stock

Separate catalog records can share a SKU. They remain separate report rows;
adding their stock together can double-count the same physical inventory.
Report distinct SKU counts separately from product-record counts when needed.

For general catalog browsing, `products.list().items[].stockQuantity` is the
Products page's catalog-stock metric. Use inventory `totalQuantity` when the
question is about available units after locks.

See the [TypeScript SDK](https://docs.omnicommerce.ai/sdk/typescript.md#inventory-with-no-marketplace-listing),
[Python SDK](https://docs.omnicommerce.ai/sdk/python.md#inventory-with-no-marketplace-listing), and the generated
[API reference](https://docs.omnicommerce.ai/api-reference.md) for the complete endpoint contract. For stock
writes and marketplace sync, see [catalog inventory updates](https://docs.omnicommerce.ai/guides/catalog-updates.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.