# PUT /v1/inventory/{productId}

**Service:** Inventory  
**Operation:** `InventoryService_SetStockLevels`

Replaces the on-hand quantity for each row+colour combination you include. Entries you omit are left unchanged — this is a partial update, not a full replace. The response echoes the product's complete stock position (all entries, including unchanged ones) with recomputed promised, on-the-way, and available. The product must have inventory tracking enabled; a non-tracked product returns HTTP 404.

## Parameters

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `productId` | path | string | yes | The product to update. Required. |

## Request body

`SetStockLevelsBody` (application/json)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `entries` | StockLevelInput[] | yes | The stock levels to set. At least one entry is required. Each identifies a product row and optional colour, with the new on-hand quantity. Entries you omit are left unchanged. |
| `entries.productRowId` | string | yes | The product variant row to set stock for. Must belong to the product identified in the request. |
| `entries.colour` | string | no | The colour to set stock for. Required for colour-tracked products (one of the row's `colourOptions`); leave empty for non-colour-tracked products. |
| `entries.onHand` | string | yes | The on-hand quantity to record, as a decimal string. Replaces the current value. |

## Responses

| Status | Schema | Description |
| --- | --- | --- |
| 200 | `SetStockLevelsResponse` | A successful response. |
| 400 | `Error` | The request was rejected because it failed validation — for example a row id that does not belong to this product, a colour not in the product's material, or a malformed `onHand` value. The body is a validation-failure envelope. |
| 401 | `Error` | The request is missing a valid bearer token, or the token is invalid or expired. |
| 403 | `Error` | The token is valid but does not permit this action, or the resource belongs to a company the token cannot act for. The error type is permission_denied. |
| 404 | `Error` | No catalogue product with the given id exists for the authenticated company, or the product does not have inventory tracking enabled. |
| 429 | `Error` | The request was throttled (rate_limited) or exceeded a size limit (resource_exhausted). When throttled, the Retry-After header says how many seconds to wait before retrying; a resource_exhausted request will fail the same way if retried unchanged. |
| default | `Error` | Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example "rate_limited" or "internal"), a human-readable message, and the request's idempotency key echoed back when one was supplied. |

## Returns

`SetStockLevelsResponse`

| Field | Type | Description |
| --- | --- | --- |
| `entries` | StockEntry[] | The stock entries for the product after the update — all entries, including those not changed by this call, with recomputed promised, on-the-way, and available. |
| `entries.productId` | string | The catalogue product this entry belongs to. |
| `entries.productName` | string | The product's display name. |
| `entries.productRowId` | string | The product variant row this entry is for — the same id the catalogue product's rows carry. Look the row up there when you need more than this entry shows, such as the variant's prices or colour options. |
| `entries.colour` | string | The colour this entry is for. Empty when the product is not colour-tracked — in that case there is one entry per row. |
| `entries.thickness` | string | The variant's thickness, when its catalogue row carries one. Empty otherwise. |
| `entries.attributes` | RowAttribute[] | The attribute pairs identifying this variant, e.g. [{name:"Size", value:"A4"}] — the same attributes the catalogue product's row carries. Together with thickness and colour they label the entry; a single-row product has no attributes to distinguish, so the list may be empty. |
| `entries.attributes.name` | string | The attribute name, e.g. "Size". |
| `entries.attributes.value` | string | The attribute value, e.g. "A4". |
| `entries.onHand` | string | Quantity currently in stock (on hand). The only directly maintained value; the others are derived from it and from order/purchase data. |
| `entries.promised` | string | Quantity committed to submitted orders that have not yet been received by the customer. Computed from open order line items. |
| `entries.onTheWay` | string | Quantity on submitted purchase orders that have not yet arrived. Computed from open purchase order items. |
| `entries.available` | string | Quantity available for new orders: `onHand` minus promised. Can be negative when more stock is committed than is on hand. |

---

Source: https://developer.factory.app/reference/inventory/set-stock-levels · Factory Sales API v1
