Products API
Auto-generated from the OpenAPI spec. Run
node docs-site/scripts/fetch-openapi.jsto regenerate.
POST /api/v1/products
Create Product
Create a new product. Manager-only.
Request body
| Field | Required | Type | Description |
|---|---|---|---|
name | Yes | string | |
sku | No | string | |
price | Yes | number | |
stock_quantity | No | integer | |
is_active | No | boolean |
Responses
- 201 — Successful Response
- 422 — Validation Error
GET /api/v1/products
List Products
List all products. Manager-only.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
active_only | query | No | boolean | |
status | query | No | "active" | "inactive" | "all". Takes precedence over active_only when provided — "inactive" is evaluated fully server-side, so the response's total/pagination reflect the filtered count rather than the broad set. Omit to keep today's active_only-only behavior. | |
removed | query | No | boolean | Default false excludes removed products. true shows only removed products (the "Removed" tab), regardless of active_only. |
page | query | No | integer | |
page_size | query | No | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error
GET /api/v1/products/{product_id}
Get Product
Get a product by ID. Manager-only.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
product_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error
PUT /api/v1/products/{product_id}
Update Product
Update a product. Manager-only.
stock_quantity is intentionally absent from ProductUpdate — the only
code path allowed to move stock is POST /products/{id}/stock-adjustment.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
product_id | path | Yes | integer |
Request body
| Field | Required | Type | Description |
|---|---|---|---|
name | No | string | |
sku | No | string | |
price | No | number | |
is_active | No | boolean |
Responses
- 200 — Successful Response
- 422 — Validation Error
DELETE /api/v1/products/{product_id}
Deactivate Product
Deactivate a product (set is_active=False). Manager-only. Soft-delete — never a real row delete.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
product_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error
DELETE /api/v1/products/{product_id}/remove
Remove Product
Soft-delete a product. Manager-only.
Never blocked on historical references — past ProductSale/
ProductSaleItem rows keep resolving the product's name/sku fine since
the row is never actually deleted, only hidden from the default/active
list (reports/sale-detail views join on product_id without filtering
on is_active/removed_at). Removed products are hidden from the
default/active list but remain in the DB (visible via
GET /products?removed=true) and can be restored via PATCH /restore.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
product_id | path | Yes | integer |
Responses
- 204 — Successful Response
- 404
PRODUCT_NOT_FOUND— Product not found. - 409
PRODUCT_ALREADY_REMOVED— This product has already been removed. - 422 — Validation Error
PATCH /api/v1/products/{product_id}/restore
Restore Product
Restore a previously-removed product. Manager-only.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
product_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 404
PRODUCT_NOT_FOUND— Product not found. - 409
PRODUCT_NOT_REMOVED— This product has not been removed. - 422 — Validation Error
POST /api/v1/products/{product_id}/photo
Upload Product Photo
Upload (or replace) a product's photo. Manager-only — Products has no client-facing surface at all, so unlike Client/Instructor this has no dual-role authorization resolver to port.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
product_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error
POST /api/v1/products/{product_id}/stock-adjustment
Adjust Product Stock
Restock (or manually correct) a product's stock_quantity by a delta.
A single atomic UPDATE (stock_quantity += delta), safe under concurrent calls — this is why it takes a delta rather than an absolute "set to N", which would be a lost-update hazard between two concurrently-restocking managers. Rejects if the delta would drive stock below 0.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
product_id | path | Yes | integer |
Request body
| Field | Required | Type | Description |
|---|---|---|---|
delta | Yes | integer | |
reason | No | string |
Responses
- 200 — Successful Response
- 422 — Validation Error
POST /api/v1/product-sales
Create Product Sale
Record a completed in-person sale of one or more products.
sold_by_user_id is always set server-side from the resolved manager
user, never accepted from the request body. total_amount and every
line's line_total are always computed server-side from
unit_price_at_sale * quantity — never trusted from the request.
Request body
| Field | Required | Type | Description |
|---|---|---|---|
client_id | No | integer | |
items | Yes | array | |
payment_method | Yes | string | |
notes | No | string |
Responses
- 201 — Successful Response
- 422 — Validation Error
GET /api/v1/product-sales
List Product Sales
List product sales, newest first. Manager-only.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
client_id | query | No | ||
start_date | query | No | ||
end_date | query | No |
Responses
- 200 — Successful Response
- 422 — Validation Error
GET /api/v1/product-sales/{sale_id}
Get Product Sale
Get a product sale by ID, with nested line items. Manager-only.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
sale_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error