# Storefront · Inventory

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /storefront/inventory/sku/{sku}

**Get inventory for a SKU across locations**

`operationId: InventoryController_getInventoryBySku`

Every stock level held for one SKU, one entry per location. This is the read for "where can we ship this from" — sum `availableQuantity` across the entries for total sellable stock.

#### Signature

```http
GET /storefront/inventory/sku/{sku} (sku: string) -> Stock levels per location
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A location where the SKU has never been stocked has no record at all, so it is absent rather than reported as zero.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/inventory/sku/{sku}/location/{locationId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `sku` | path | string | yes | Product SKU. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Stock levels per location |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/inventory/sku/{sku}/location/{locationId}

**Get inventory for a SKU at one location**

`operationId: InventoryController_getInventory`

The stock level for one SKU at one location, with its on-hand, reserved and available quantities.

#### Signature

```http
GET /storefront/inventory/sku/{sku}/location/{locationId} (sku: string, locationId: string) -> The stock level, or null when the SKU has never been stocked there
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Returns `null` with a `200` when there is no record — the write endpoints raise a `400` for the same condition.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `sku` | path | string | yes | Product SKU. |
| `locationId` | path | string | yes | Location id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The stock level, or null when the SKU has never been stocked there |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/inventory/location/{locationId}

**List inventory at a location**

`operationId: InventoryController_getInventoryByLocation`

Every stock level held at one location, paged. Set `lowStock=true` to narrow it to items at or below their reorder point — the picking list for a restock.

#### Signature

```http
GET /storefront/inventory/location/{locationId} (locationId: string, lowStock?: boolean, page?: integer, pageSize?: integer) -> A page of stock levels
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/inventory/alerts/low-stock`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `locationId` | path | string | yes | Location id. |
| `lowStock` | query | boolean | — | Only items below their reorder point. Only the exact string `true` enables it. |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of stock levels |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/inventory/alerts/low-stock

**Get low stock alerts**

`operationId: InventoryController_getLowStockAlerts`

Every SKU at or below its reorder point, across the org or at one location. The reorder worklist.

#### Signature

```http
GET /storefront/inventory/alerts/low-stock (locationId?: string) -> Stock levels below their reorder point
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A SKU with no `reorderPoint` set cannot trigger an alert.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/inventory/location/{locationId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `locationId` | query | string | — | Restrict to one location. Omit for the whole org. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Stock levels below their reorder point |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/inventory/stats

**Get inventory statistics**

`operationId: InventoryController_getStats`

Aggregate stock figures for the org or one location — totals, value and how many SKUs are below their reorder point.

#### Signature

```http
GET /storefront/inventory/stats (locationId?: string) -> Aggregate inventory statistics
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/inventory/alerts/low-stock`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `locationId` | query | string | — | Restrict to one location. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Aggregate inventory statistics |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/inventory/sku/{sku}/transactions

**Get transaction history for a SKU**

`operationId: InventoryController_getTransactionHistory`

The movement ledger for a SKU — every adjustment, reservation, sale, return, count and transfer, with the quantity before and after and the reference that caused it.

This is the audit trail: when stock does not match expectations, this is where the discrepancy is found.

#### Signature

```http
GET /storefront/inventory/sku/{sku}/transactions (sku: string, locationId?: string, page?: integer, pageSize?: integer) -> A page of movements
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A `reserve` movement records `previousQuantity` and `newQuantity` as the same value — reserving moves stock between the reserved and available buckets without changing what is on hand.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory/adjust`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `sku` | path | string | yes | Product SKU. |
| `locationId` | query | string | — | Restrict to one location. |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of movements |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory

**Set inventory for a SKU at a location**

`operationId: InventoryController_setInventory`

Creates or replaces the stock record for one SKU at one location. This is the endpoint that establishes a level in the first place — every other write requires the record to exist already.

It **sets** rather than adjusts, so it overwrites the current quantity outright. Use `POST /storefront/inventory/adjust` for a relative change with a recorded reason, and `POST /storefront/inventory/count` to reconcile against a physical count.

#### Signature

```http
POST /storefront/inventory (body) -> The stock level
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Overwrites rather than accumulates. Calling it twice with the same quantity leaves that quantity, not double.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory/adjust`
- `POST /storefront/inventory/count`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The stock level to set. `sku` and `locationId` identify it.

```json
{
  "sku": "DRK-COLA-330",
  "locationId": "loc_downtown",
  "quantity": 120,
  "reorderPoint": 25,
  "reorderQuantity": 200
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The stock level |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory/adjust

**Adjust inventory**

`operationId: InventoryController_adjustInventory`

Applies a relative change to on-hand stock and records why. Send a positive `adjustment` to add stock and a negative one to remove it.

A `reason` is required — an unexplained stock change is the thing that makes a ledger useless. Attach `referenceType` and `referenceId` to link the movement to whatever caused it.

The adjustment is refused if it would take stock below zero.

#### Signature

```http
POST /storefront/inventory/adjust (body) -> The updated stock level
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Not idempotent: each call applies the change again.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVENTORY_NOT_FOUND | Inventory not found for <sku> at <locationId> | No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. | Create the level first with `POST /storefront/inventory`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory/count`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The change, and why.

```json
{
  "sku": "DRK-COLA-330",
  "locationId": "loc_downtown",
  "adjustment": -3,
  "reason": "Damaged in the stockroom"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated stock level |
| `400` | Inventory not found for <sku> at <locationId> — No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory/reserve

**Reserve inventory for an order**

`operationId: InventoryController_reserveInventory`

Commits stock to an order without shipping it: `reservedQuantity` goes up and `availableQuantity` comes down by the same amount. **On-hand `quantity` does not change** — the goods are still on the shelf, just no longer sellable to anyone else.

Refused when there is not enough available, and the error reports exactly how much there is.

Every reservation must eventually be released or fulfilled, or the stock stays committed forever.

#### Signature

```http
POST /storefront/inventory/reserve (body) -> The updated stock level
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Not idempotent: a retry reserves the quantity a second time. Nothing links a reservation to its order beyond the ledger entry, so a duplicate cannot be detected automatically.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVENTORY_NOT_FOUND | Inventory not found for <sku> at <locationId> | No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. | Create the level first with `POST /storefront/inventory`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory/release`
- `POST /storefront/inventory/fulfill`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What to reserve, and for which order.

```json
{
  "sku": "DRK-COLA-330",
  "locationId": "loc_downtown",
  "quantity": 2,
  "orderId": "66f1a2b3c4d5e6f708192a3b"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated stock level |
| `400` | Inventory not found for <sku> at <locationId> — No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory/release

**Release a reservation**

`operationId: InventoryController_releaseReservation`

Undoes a reservation when an order is cancelled: `reservedQuantity` comes down and `availableQuantity` goes back up. On-hand quantity is untouched.

Call this whenever a reserved order does not ship, or the stock stays committed and invisible to other customers.

#### Signature

```http
POST /storefront/inventory/release (body) -> The updated stock level
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Releasing more than was reserved is not rejected — reserved stock is floored at zero, which silently inflates available stock. Release exactly what you reserved.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVENTORY_NOT_FOUND | Inventory not found for <sku> at <locationId> | No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. | Create the level first with `POST /storefront/inventory`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory/reserve`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What to release, and from which order.

```json
{
  "sku": "DRK-COLA-330",
  "locationId": "loc_downtown",
  "quantity": 2,
  "orderId": "66f1a2b3c4d5e6f708192a3b"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated stock level |
| `400` | Inventory not found for <sku> at <locationId> — No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory/fulfill

**Fulfill inventory after shipment**

`operationId: InventoryController_fulfillInventory`

Takes stock off the shelf once an order ships. On-hand `quantity` comes down by the shipped amount, the matching reservation is cleared, and `availableQuantity` is recomputed.

This is the step that makes a sale permanent — the movement is recorded as type `sale`.

Refused if it would take on-hand stock below zero. The check runs before anything is written, so a rejected fulfilment leaves the record untouched.

#### Signature

```http
POST /storefront/inventory/fulfill (body) -> The updated stock level
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Fulfilling clears the reservation as part of the same call — do not also release it, or available stock will be overstated.
- Reserved quantity is floored at zero, so fulfilling stock that was never reserved still works and simply reduces on-hand.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVENTORY_NOT_FOUND | Inventory not found for <sku> at <locationId> | No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. | Create the level first with `POST /storefront/inventory`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory/reserve`
- `POST /storefront/inventory/return`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What shipped, and for which order.

```json
{
  "sku": "DRK-COLA-330",
  "locationId": "loc_downtown",
  "quantity": 2,
  "orderId": "66f1a2b3c4d5e6f708192a3b"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated stock level |
| `400` | Inventory not found for <sku> at <locationId> — No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory/return

**Return stock from a return**

`operationId: InventoryController_returnToStock`

Books returned goods back in. **`condition` decides whether stock actually moves:**

- `restockable` — on-hand and available both go up by `quantity`.
- `damaged` — **no stock change at all.** The movement is still recorded, so the return is auditable, but the goods are written off rather than made sellable.

That asymmetry is easy to miss: a `200` on a `damaged` return does not mean anything was added.

#### Signature

```http
POST /storefront/inventory/return (body) -> The stock level — unchanged when the condition was `damaged`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVENTORY_NOT_FOUND | Inventory not found for <sku> at <locationId> | No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. | Create the level first with `POST /storefront/inventory`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /storefront/returns/{rmaNumber}/complete`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What came back, from which return, and in what state.

```json
{
  "sku": "DRK-COLA-330",
  "locationId": "loc_downtown",
  "quantity": 2,
  "returnId": "RMA-4821",
  "condition": "restockable"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The stock level — unchanged when the condition was `damaged` |
| `400` | Inventory not found for <sku> at <locationId> — No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory/count

**Record a physical count**

`operationId: InventoryController_performCount`

Reconciles the system against a physical stocktake. On-hand quantity is **set** to `countedQuantity`, `availableQuantity` is recomputed from it, and `lastCountDate` is stamped.

The response includes the `variance` — counted minus previous — which is the number a stocktake report actually cares about. A negative variance is shrinkage.

Reserved quantity is left alone: a count measures what is on the shelf, not what is promised to orders.

#### Signature

```http
POST /storefront/inventory/count (body) -> The reconciled level and the variance
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A count can take available stock negative if more is reserved than was counted — the count is trusted as the truth and is not clamped.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVENTORY_NOT_FOUND | Inventory not found for <sku> at <locationId> | No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. | Create the level first with `POST /storefront/inventory`. Note this is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /storefront/inventory/adjust`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

What was counted.

```json
{
  "sku": "DRK-COLA-330",
  "locationId": "loc_downtown",
  "countedQuantity": 117,
  "notes": "Quarterly stocktake"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The reconciled level and the variance |
| `400` | Inventory not found for <sku> at <locationId> — No inventory record exists for that SKU at that location. A SKU that has never been stocked at a location has no record — it is not treated as zero. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "inventory": {
    "data": {
      "sku": "DRK-COLA-330",
      "quantity": 117,
      "reservedQuantity": 20,
      "availableQuantity": 97
    }
  },
  "variance": -3
}
```

## GET /storefront/inventory/transfers

**List inventory transfers**

`operationId: InventoryController_listTransfers`

Lists transfers with optional filters and paging. Filter by `status=in_transit` for stock currently on the road.

#### Signature

```http
GET /storefront/inventory/transfers (status?: string, fromLocationId?: string, toLocationId?: string, page?: integer, pageSize?: integer) -> A page of transfers
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/inventory/transfers/{transferId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `status` | query | "pending" \| "in_transit" \| "completed" \| "cancelled" | — |  |
| `fromLocationId` | query | string | — |  |
| `toLocationId` | query | string | — |  |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | A page of transfers |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /storefront/inventory/transfers

**Create an inventory transfer**

`operationId: InventoryController_createTransfer`

Opens a transfer of stock between two locations. The transfer starts `pending`.

Source stock is checked up front: if any line exceeds what the source location holds, the whole transfer is refused rather than created partially.

#### Signature

```http
POST /storefront/inventory/transfers (body) -> The created transfer
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INSUFFICIENT_SOURCE_INVENTORY | Insufficient inventory for <sku> at source location | A line asks for more than the source location holds. | Check source levels first. The whole transfer is refused — no partial transfer is created. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /storefront/inventory/transfers/{transferId}/ship`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Where the stock is going, and what.

```json
{
  "fromLocationId": "loc_warehouse",
  "toLocationId": "loc_downtown",
  "items": [
    {
      "sku": "DRK-COLA-330",
      "productName": "Cola 330ml",
      "quantity": 48
    }
  ],
  "notes": "Weekly replenishment"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created transfer |
| `400` | Insufficient inventory for <sku> at source location — A line asks for more than the source location holds. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /storefront/inventory/transfers/{transferId}

**Get an inventory transfer**

`operationId: InventoryController_getTransfer`

Fetches one transfer with its lines, including the received quantities once it has been receipted.

#### Signature

```http
GET /storefront/inventory/transfers/{transferId} (transferId: string) -> The transfer
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | TRANSFER_NOT_FOUND | Transfer <transferId> not found | No transfer in the org has that id. | Check the id with `GET /storefront/inventory/transfers`. This is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /storefront/inventory/transfers`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `transferId` | path | string | yes | Transfer id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The transfer |
| `400` | Transfer <transferId> not found — No transfer in the org has that id. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /storefront/inventory/transfers/{transferId}/ship

**Ship an inventory transfer**

`operationId: InventoryController_shipTransfer`

Marks a pending transfer as dispatched and records the carrier and tracking. The transfer moves to `in_transit` and the stock leaves the source location.

Only a `pending` transfer can be shipped.

#### Signature

```http
PUT /storefront/inventory/transfers/{transferId}/ship (transferId: string, body) -> The shipped transfer
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | TRANSFER_NOT_FOUND | Transfer <transferId> not found | No transfer in the org has that id. | Check the id with `GET /storefront/inventory/transfers`. This is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /storefront/inventory/transfers/{transferId}/receive`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `transferId` | path | string | yes | Transfer id. |

### Request body

How the stock was dispatched.

```json
{
  "trackingNumber": "1Z999AA10123456784",
  "carrier": "ups"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The shipped transfer |
| `400` | Transfer <transferId> not found — No transfer in the org has that id. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /storefront/inventory/transfers/{transferId}/receive

**Receive an inventory transfer**

`operationId: InventoryController_receiveTransfer`

Books a transfer in at the destination. The received quantities are recorded per line and added to the destination's stock, and the transfer moves to `completed`.

**Receive what actually arrived, not what was sent.** `receivedQuantity` can be lower than the shipped quantity, and the difference is the shrinkage in transit — which the transfer record then preserves as evidence.

Only an `in_transit` transfer can be received.

#### Signature

```http
PUT /storefront/inventory/transfers/{transferId}/receive (transferId: string, body) -> The completed transfer
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A line omitted from `receivedItems` is not booked in — send an entry for every line, using `0` for one that did not arrive.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | TRANSFER_NOT_FOUND | Transfer <transferId> not found | No transfer in the org has that id. | Check the id with `GET /storefront/inventory/transfers`. This is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /storefront/inventory/transfers/{transferId}/ship`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `transferId` | path | string | yes | Transfer id. |

### Request body

What actually arrived, per line.

```json
{
  "receivedItems": [
    {
      "sku": "DRK-COLA-330",
      "receivedQuantity": 48
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The completed transfer |
| `400` | Transfer <transferId> not found — No transfer in the org has that id. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /storefront/inventory/transfers/{transferId}/cancel

**Cancel an inventory transfer**

`operationId: InventoryController_cancelTransfer`

Cancels a transfer that has not yet shipped, returning the committed stock to the source location.

**Only a `pending` transfer can be cancelled.** Once stock is in transit it physically exists somewhere between two locations, so it must be received — short if necessary — rather than cancelled.

#### Signature

```http
PUT /storefront/inventory/transfers/{transferId}/cancel (transferId: string, body) -> The cancelled transfer
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | TRANSFER_NOT_FOUND | Transfer <transferId> not found | No transfer in the org has that id. | Check the id with `GET /storefront/inventory/transfers`. This is a `400`, not a `404`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /storefront/inventory/transfers/{transferId}/receive`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `transferId` | path | string | yes | Transfer id. |

### Request body

Why the transfer is being cancelled.

```json
{
  "reason": "Store no longer needs the stock"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The cancelled transfer |
| `400` | Transfer <transferId> not found — No transfer in the org has that id. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

