# Inventory

Read product stock owned by your Meykt organization, including your goods at an active fulfillment partner. This endpoint requires `inventory:read`. It does not book stock, announce deliveries or write to your ERP or Shopify.

## Read and synchronize inventory

`GET /v1/inventory` — requires `inventory:read`.

```bash
curl 'https://api.meykt.com/v1/inventory?cursor=0&limit=100' \
  -H "Authorization: Bearer $MEYKT_API_KEY"
```

Start with `cursor=0` and an empty local inventory projection. Apply each returned change in order, keyed by `inventory_id`. Keep the returned `next_cursor` only after storing that page successfully. When `has_more` is true, request the next page immediately; otherwise save the cursor for your next poll. Use `next_cursor`, not the separate `revision` field, as your continuation value. Applying a change means setting that inventory entry to the returned state, not adding its `physical` amount to the previous amount. Commit your local page updates and cursor together; otherwise a crash can skip or repeat data incorrectly.

For a full rebuild, construct a separate empty projection and follow every page from `0` before replacing your established local view. An early page or an intermediate historical entry is not the complete current warehouse. Keep independent cursors for each API connection; changing only the key on the same connection does not create a new inventory stream.

```json
{
  "data": {
    "environment": "live",
    "observed_at": "2026-10-18T20:00:00Z",
    "revision": "12",
    "next_cursor": "15f5fbb9-336c-4ddf-b80c-b0b66cb2c46f:12",
    "has_more": false,
    "changes": [{
      "revision": "12",
      "inventory_id": "7c794b49-3342-41cb-90fd-292845e58269",
      "removed": false,
      "inventory": {
        "id": "7c794b49-3342-41cb-90fd-292845e58269",
        "article_id": "0d937679-9930-4b9d-8b59-2f4a06d3b770",
        "external_article_ids": ["ERP-CUP-7"],
        "sku": "CUP-7",
        "name": "Example cup",
        "warehouse": { "id": "15b6810a-d674-4d45-83b4-1ddcf98437fa", "name": "North warehouse", "origin": "partner" },
        "unit": "Stück",
        "physical": "20",
        "reserved": "4",
        "available": "13"
      }
    }]
  }
}
```

| Field | Meaning |
|---|---|
| `inventory_id`, `inventory.id` | Stable opaque identity for one article/warehouse inventory scope. |
| `article_id` | Your Meykt article reference. |
| `external_article_ids` | Article references explicitly linked through this API connection. An empty list means no such reference is linked. Meykt's `sku` may differ from the ERP reference. |
| `warehouse.id`, `warehouse.origin` | Opaque warehouse reference and `own` or `partner`. They do not grant access to a partner organization. |
| `physical` | Physically recorded quantity, including goods not yet eligible for allocation. |
| `reserved` | Quantity already reserved. |
| `available` | Currently eligible free quantity, after reservations and other holds; receiving/clarification stock and blocked or inaccessible stock are excluded. |
| `revision` on a change | Monotonic revision in this connection's inventory projection. |
| Top-level `revision` | Latest observed projection revision; a paginated read may still be finishing an earlier range. |
| `observed_at` | When this page was read and its access checked, not when a particular physical movement happened. |

Quantities and revisions are **decimal strings**. Quantities preserve up to six decimal places. Use decimal arithmetic; never round them through a binary floating-point or 32-bit integer type. The `unit` is the recorded base unit; the API performs no implicit unit conversion.

## Removal and lost access

A removal is a tombstone:

```json
{ "revision": "13", "inventory_id": "7c794b49-3342-41cb-90fd-292845e58269", "removed": true, "inventory": null }
```

Remove this inventory entry from your local authorized projection. A tombstone can follow a withdrawn product release or ended partner relationship; it does not claim that the physical goods disappeared. Old pages also withhold payloads that are no longer authorized.

A completely empty successful delta page normally means no new authorized changes in that range; retain entries you already have. An absent article is not confirmed zero stock: it may not have a stock setup or may be outside the current sharing scope. Do not turn a read failure into zero stock or an unchanged-success response. On failure, keep the previous cursor and retry.

## Consistency and limits

This stream reconciles observed **states**, including current permissions. It is not a journal of every receipt, pick or movement between polls. Intermediate states can be combined into the next observed state. Apply changes idempotently; retrying a page must not create another stock movement in your ERP.

The first page of a new poll reconciles stock and pins the upper revision for its continuation pages. Those pages finish that range without reconciling stock again. Quantity changes made meanwhile appear in a subsequent poll. Current access is checked on every page, so a withdrawn grant can redact even an older change in the pinned range. Starting again at `cursor=0` replays the retained projection history; it is not a constant-size current-stock snapshot.

A cursor belongs to its API connection. Keep it opaque, do not edit it and do not reuse it for another connection. The page limit is 1–250, default 100. The current read supports up to 5,000 article/warehouse scopes for the connected owner; an oversized projection fails visibly instead of silently truncating stock.

Only the owner's product stock is exposed. A partner's own goods, other customers' goods and material purchase costs are not included. Inventory responses do not promise separate quality grades, serial-number allocation, expiry selection, virtual-bundle availability or future incoming stock.

## Test keys

A test key returns `environment: "test"`, revision/cursor `"0"` and an empty `changes` array. It neither reads live inventory nor creates pretend warehouse quantities. Use this response to check authentication and response handling; it is not evidence that your live warehouse is empty.

For current order progress and actual shipment proof, see [Shipping and progress](/en/developers/shipping).


## Frozen initial snapshot and bounded changes

`POST /v1/inventory/snapshots` with `{"request_id":"11111111-1111-4111-8111-111111111111"}` opens a frozen view using `inventory:read`. Keep that UUID when retrying after a lost response. The receipt contains `snapshot_id`, `revision`, `cursor`, `delta_cursor`, `item_count` and `expires_at`. A snapshot lasts one hour; up to ten can be active per connection. Expired request identities remain recoverable as expired for seven further days; use a new UUID to start a new snapshot.

`GET /v1/inventory/snapshots/{snapshotId}?cursor=0&limit=100` reads its first page. Continue with `next_cursor` while `has_more` is true. Apply each entry by `inventory_id`, replacing its state or removing it when `removed` is true. Current access is checked on every page: the snapshot never restores withdrawn access to old stock. Build into a separate empty local view, then replace the previous view only after the last page.

`GET /v1/inventory/changes?cursor=<delta_cursor>` continues after the snapshot revision. This feed retains changes for seven days and returns `retention_floor`. A cursor that is too old returns `409 snapshot_required`; start a new snapshot instead of assuming an empty warehouse. Changes remain replacements, not quantities to add.

`GET /v1/inventory/sync` reports the current synchronization revision, retention floor, pending changes and last reconciliation error for this connection. A lagging or failed reconciliation is not a confirmed zero quantity. All four operations use `inventory:read` and never book physical stock.

## Exact positions by condition

`GET /v1/inventory/positions` requires both `inventory:read` and `conditions:read`. Start with `request_id=<UUID>` and retain that UUID after a lost response. Continue with the returned `snapshot_id` and `next_cursor`, omitting request_id. `cursor=0` starts a page sequence and `limit` accepts 1–250 (default 100). Provide exactly one of request_id and snapshot_id. Snapshots last one hour and contain at most 5000 positions; a limit or permission error is never an empty warehouse.

Capability discovery publishes these as two alternatives: opening requires `request_id` and permits only the initial cursor `0`; reading requires `snapshot_id`. Both reject the other identity. For AI calls, use `open_api_inventory_positions` and `read_api_inventory_positions`: each publishes its own required arguments. The general reader also accepts `inventory_positions`; its field descriptions explain the identity choice and the same service validates every request.

Each position includes `physical`, `reserved`, `exclusive_held`, `available` and `certified_available` as exact decimal strings in unit `piece`. Availability also respects accessible storage and exclusive physical claims. `condition` distinguishes immutable code/grade/disposition/version from explicit unclassified legacy goods; legacy never claims certified quality. The original aggregate inventory contract keeps its published meaning and unit label.

Physical goods exclusively held as cover for a Shopify sales quota are included in `exclusive_held` and already deducted from `available`. Do not subtract them again in your ERP. On adoption by an actual order, cover becomes that order's ordinary reservation without releasing the same goods for a second sale. Expected-delivery cover remains future stock and never increases `physical` or `available` in this API. A Shopify sales quota is a separate sales offer; it is not this physical inventory snapshot.

`source_scope_ref` is stable across future lots of the same owner's physical article, operator and warehouse. `condition_ref` binds one exact immutable condition version. Do not automatically include a newly defined condition version in an external sales offer. Operator, warehouse and lot references are opaque and owner-bound. Every stored page checks current partner releases again.

This endpoint supplies complete position snapshots, not a movement journal or a condition-delta stream. Replace a local position view only after collecting all pages successfully. Refresh it after relevant notifications and periodic reconciliation; never interpret a missing failed page as zero stock. A frozen quantity is the observation at `observed_at`, not a promise that no later movement occurred.


## Close a completed position snapshot

`POST /v1/inventory/positions/snapshots/{snapshotId}/close` requires `inventory:read` and `conditions:read`. Send an empty JSON object `{}`. Close only after applying every page successfully, or when intentionally discarding that entire read attempt. It changes no stock, reservation or shipment. It immediately releases the active-snapshot slot and deletes only the temporary position rows.

The snapshot ID identifies the idempotent operation. The response is:

```json
{
  "data": {
    "snapshot_id": "11111111-1111-4111-8111-111111111111",
    "status": "closed",
    "closed_at": "2026-10-19T12:10:00.000Z",
    "replay_until": "2026-10-26T13:00:00.000Z"
  }
}
```

Retry the same close after a lost response. Its original `closed_at` remains unchanged. Further page reads and retries of the original snapshot-opening `request_id` return `409 inventory_snapshot_closed`; they cannot recreate the closed capture during its replay window. Open a fresh capture with a **new** request UUID. There is no undo. Unknown snapshots and snapshots belonging to another connection receive the same unavailable response.

The original request identity and close receipt remain recoverable until `replay_until`, which is the original snapshot expiry plus seven days. Cleanup does not reset that deadline. After this bounded retention window, do not reuse the old opening UUID: create a new one. A close request itself never creates a snapshot, even after its receipt has expired.

For ongoing condition synchronization, coalesce `inventory.positions_changed` notifications and read one new complete position snapshot, apply every page, then close it. If changes arrive while reading, perform a subsequent refresh. Retain the previous complete local view after any failed or expired page. Keep normal page and rate limits; closing a snapshot frees its slot but does not raise those limits. The `/v1/inventory/changes` feed contains aggregate inventory changes and **is not a condition-position delta feed**.

The integration-profile operation is `inventory.positions.close`, with canonical input `{ "snapshot_id": "..." }`. MCP and Meyki use `close_api_inventory_positions` under the same API-management and data-reading permissions as opening and reading positions. The tool defaults to a preview; `dry_run:false` closes the snapshot.
