Products
Products (also called parts or inventory items) are the line-item catalog entries in BenchKey. Each product has a name, optional SKU, pricing, and stock settings. Inventory quantities are tracked separately per location, see Inventory for adjusting stock. Deleting a product archives it (sets it inactive) rather than permanently removing it.
The product object
Section titled “The product object”{ "object": "product", "id": 42, "name": "iPhone 14 Screen Assembly", "sku": "SCR-IP14-OEM", "description": "OEM-grade OLED screen assembly for iPhone 14", "condition": "new", "unit": "each", "category_id": 3, "category_name": "Screens", "qty_on_hand": 5, "min_stock": 2, "low_stock": false, "serial_tracked": false, "active": true, "sell_price": { "amount_cents": 8999, "amount": "89.99", "currency": "USD" }, "last_cost": { "amount_cents": 4500, "amount": "45.00", "currency": "USD" }, "avg_cost": { "amount_cents": 4750, "amount": "47.50", "currency": "USD" }, "default_vendor_id": 7, "vendor_sku": "AAPL-SCR-14", "notes": null, "created_at": "2025-09-01T10:00:00.000Z", "updated_at": "2025-11-15T14:30:00.000Z"}| Field | Type | Description |
|---|---|---|
id | integer | Unique product ID |
name | string | Display name |
sku | string|null | Your internal SKU, or null if not set |
description | string|null | Optional long description |
condition | string | "new", "used", "refurbished", etc. |
unit | string | Unit of measure (e.g. "each", "pair") |
category_id | integer|null | Category ID, or null |
category_name | string|null | Category display name, or null |
qty_on_hand | integer | Total units on hand across all locations |
min_stock | integer | Reorder threshold; 0 means no threshold set |
low_stock | boolean | true when min_stock > 0 and qty_on_hand ≤ min_stock |
serial_tracked | boolean | Whether serial numbers are required at point of sale |
active | boolean | false if the product has been archived |
sell_price | Money|null | Retail sell price (amount_cents, amount, currency) |
last_cost | Money|null | Last purchase cost (amount_cents, amount, currency) |
avg_cost | Money|null | Moving-average cost, read-only (amount_cents, amount, currency) |
default_vendor_id | integer|null | Preferred vendor ID for purchase orders |
vendor_sku | string|null | Vendor’s part number |
notes | string|null | Internal notes |
created_at | string | ISO-8601 creation timestamp |
updated_at | string | ISO-8601 last-updated timestamp |
List products
Section titled “List products”GET /api/v1/productsReturns a cursor-paginated list of products ordered by most recently created first. Active products are returned by default; pass active=all to include archived items.
Scope required: products.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
q | string | Partial-match search across name and sku, max 255 chars |
sku | string | Exact SKU match, max 255 chars |
category_id | integer | Filter by category (positive integer ≤ 2,147,483,647; out-of-range values return 400 invalid_query) |
low_stock | boolean | Pass true to return only items at or below min_stock |
active | string | Pass "all" to include inactive (archived) products |
limit | integer | Page size, 1–100 (default: 20) |
cursor | string | Opaque cursor from a previous response’s next_cursor |
curl "https://app.benchkey.com/api/v1/products?q=iphone&limit=20" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/products?q=iphone&limit=20", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const { data, has_more, next_cursor } = await res.json();Response
Section titled “Response”{ "object": "list", "data": [ { "object": "product", "id": 42, "name": "iPhone 14 Screen Assembly", "sku": "SCR-IP14-OEM", "description": null, "condition": "new", "unit": "each", "category_id": 3, "category_name": "Screens", "qty_on_hand": 5, "min_stock": 2, "low_stock": false, "serial_tracked": false, "active": true, "sell_price": { "amount_cents": 8999, "amount": "89.99", "currency": "USD" }, "last_cost": { "amount_cents": 4500, "amount": "45.00", "currency": "USD" }, "avg_cost": { "amount_cents": 4750, "amount": "47.50", "currency": "USD" }, "default_vendor_id": 7, "vendor_sku": null, "notes": null, "created_at": "2025-09-01T10:00:00.000Z", "updated_at": "2025-11-15T14:30:00.000Z" } ], "has_more": false, "next_cursor": null}See Pagination for how to page through results.
Retrieve a product
Section titled “Retrieve a product”GET /api/v1/products/:idReturns a single product by ID.
Scope required: products.read
curl "https://app.benchkey.com/api/v1/products/42" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/products/42", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const product = await res.json();Response
Section titled “Response”Returns a single product object. Returns 404 if no product with that ID exists or if the product is inactive, archived products are hidden by default, matching the list behavior. Pass ?active=all to retrieve an inactive product by ID.
Create a product
Section titled “Create a product”POST /api/v1/productsCreates a new product in your catalog. Prices are supplied in integer cents (sell_price_cents, last_cost_cents).
Scope required: products.write
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
name | yes | Product display name |
sku | no | Internal SKU |
description | no | Long description |
condition | no | "new", "used", "refurbished", etc. |
unit | no | Unit of measure (default: "each") |
category_id | no | Category ID to assign |
min_stock | no | Reorder threshold (non-negative integer) |
sell_price_cents | no | Retail sell price in integer cents (e.g. 8999 = $89.99) |
last_cost_cents | no | Last purchase cost in integer cents |
default_vendor_id | no | Preferred vendor ID |
serial_tracked | no | true to require serial numbers at sale |
notes | no | Internal notes |
curl -X POST "https://app.benchkey.com/api/v1/products" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "name": "iPhone 14 Battery", "sku": "BAT-IP14", "condition": "new", "category_id": 5, "sell_price_cents": 3999, "last_cost_cents": 1200, "min_stock": 3 }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/products", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ name: "iPhone 14 Battery", sku: "BAT-IP14", condition: "new", category_id: 5, sell_price_cents: 3999, last_cost_cents: 1200, min_stock: 3, }),});const product = await res.json(); // 201 CreatedResponse
Section titled “Response”Returns the created product object with HTTP 201 Created.
Update a product
Section titled “Update a product”PATCH /api/v1/products/:idUpdates one or more fields on a product. Only fields you include in the request body are changed. To update sell price or cost, supply sell_price_cents or last_cost_cents in integer cents.
Quantity adjustments are not made here, use POST /inventory/:product_id/adjust instead.
Scope required: products.write
Request body
Section titled “Request body”All fields are optional. Supply only the fields you want to change.
| Field | Description |
|---|---|
name | New display name |
sku | New SKU |
description | New description |
condition | New condition string |
unit | New unit of measure |
category_id | New category ID (null to unset) |
min_stock | New reorder threshold |
sell_price_cents | New sell price in integer cents |
last_cost_cents | New last cost in integer cents |
default_vendor_id | New preferred vendor ID |
serial_tracked | Toggle serial-number tracking |
notes | New notes |
curl -X PATCH "https://app.benchkey.com/api/v1/products/42" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "sell_price_cents": 7999, "min_stock": 5 }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/products/42", { method: "PATCH", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ sell_price_cents: 7999, min_stock: 5 }),});const product = await res.json();Response
Section titled “Response”Returns the updated product object.
Archive a product
Section titled “Archive a product”DELETE /api/v1/products/:idArchives a product by setting it to inactive. Archived products are hidden from list results by default (pass active=all to include them). Hard deletion is not supported, BenchKey preserves historical line-item data on past tickets and invoices.
Scope required: products.write
curl -X DELETE "https://app.benchkey.com/api/v1/products/42" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/products/42", { method: "DELETE", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" },});const result = await res.json();// { "object": "product", "id": 42, "archived": true }Response
Section titled “Response”{ "object": "product", "id": 42, "archived": true}To restore an archived product, use the BenchKey app, the public API does not expose an unarchive endpoint.
Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_query | A list filter parameter was supplied as an array or object instead of a single scalar value |
400 | invalid_cursor | cursor is malformed |
400 | invalid_id | Path :id is not a valid integer |
400 | missing_field | Required field name was not provided |
400 | invalid_field | sell_price_cents, last_cost_cents, or min_stock is not a non-negative integer; q or sku exceeds 255 characters; category_id does not exist; default_vendor_id does not exist or belongs to an inactive vendor |
400 | nothing_to_update | PATCH body contained no updatable fields |
404 | not_found | No product with that ID exists |
422 | create_failed | Internal error prevented product creation |
422 | update_failed | Internal error prevented product update |
422 | archive_failed | Internal error prevented archiving |
403 | insufficient_scope | API key lacks products.read or products.write |
See Errors for the full error envelope format.