> For the complete documentation index, see [llms.txt](https://docs.hesh.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hesh.app/manuals/public-api/products.md).

# Products

## List products

> \
> Returns products in the tenant, paginated. Only non-deleted drafts are returned in this iteration — published versions and soft-deleted records are excluded.\
> \
> \### Query\
> \
> \- \`search\` — case-insensitive substring on name.\
> \- \`category\_id\` — direct-parent category filter.\
> \- \`is\_active\` — filter by activation state.\
> \- \`page\` / \`limit\` — defaults \`page = 1\`, \`limit = 25\`, max \`limit = 100\`.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PaginatedResult":{"type":"object","properties":{"meta":{"description":"Pagination metadata","allOf":[{"$ref":"#/components/schemas/PaginationMetadata"}]}},"required":["meta"]},"PaginationMetadata":{"type":"object","properties":{"total":{"type":"number","description":"Total number of items"},"lastPage":{"type":"number","description":"Last page number"},"currentPage":{"type":"number","description":"Current page number"},"perPage":{"type":"number","description":"Items per page"},"prev":{"type":"number","description":"Previous page number","nullable":true},"next":{"type":"number","description":"Next page number","nullable":true}},"required":["total","lastPage","currentPage","perPage","prev","next"]},"PublicProductListItemDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"$ref":"#/components/schemas/IdNameDto"},"type":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/IdNameDto"}]},"vendors":{"description":"All vendors linked to the product meta (M2M).","type":"array","items":{"$ref":"#/components/schemas/IdNameDto"}},"status":{"allOf":[{"$ref":"#/components/schemas/PublicProductStatus"}]},"is_active":{"type":"boolean"},"created_at":{"type":"string","nullable":true},"modified_at":{"type":"string","nullable":true}},"required":["id","name","category","type","vendors","status","is_active","created_at","modified_at"]},"IdNameDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]},"PublicProductStatus":{"type":"string","enum":["draft","published"]}}},"paths":{"/api/v1/public/products":{"get":{"description":"\nReturns products in the tenant, paginated. Only non-deleted drafts are returned in this iteration — published versions and soft-deleted records are excluded.\n\n### Query\n\n- `search` — case-insensitive substring on name.\n- `category_id` — direct-parent category filter.\n- `is_active` — filter by activation state.\n- `page` / `limit` — defaults `page = 1`, `limit = 25`, max `limit = 100`.\n","operationId":"PublicProductsController_list_v1","parameters":[{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on name.","schema":{"maxLength":100,"type":"string"}},{"name":"category_id","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"is_active","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"page","required":false,"in":"query","schema":{"minimum":1,"default":1,"type":"number"}},{"name":"limit","required":false,"in":"query","schema":{"minimum":1,"maximum":100,"default":25,"type":"number"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedResult"},{"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicProductListItemDto"}}}}]}}}},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List products","tags":["Products"]}}}}
```

## Create product (optionally publish in one call)

> \
> Creates a fully-formed product in a single POST. Everything the internal editor would create piece-by-piece — configurations, options → auto-generated variants, workflow templates + task templates + responsibilities, links, BOM — can be supplied in one nested body. Anything omitted is filled with sensible defaults (a "Default" configuration, one default variant, one default workflow with a "Default" task, one "Responsibility 1"). Set \`publish: true\` to publish the product in the same request; a \*\*draft\*\* \`Product\` row is always created and, when publishing, a mirrored \*\*published\*\* row is created alongside it.\
> \
> \*\*Response\*\* is a flat record (see \`PublicCreatedProductDto\`): the draft \`id\`, resolved reference ids (\`category\_id\`, \`type\_id\`, \`vendor\_ids\`, \`tag\_ids\`), \`status\`, and (when publishing) the mirror's \`published\_id\`. No enrichment — no category breadcrumb, no tag rehydration, no hasPublishedVersion post-lookup. If you need the fully-hydrated shape (with breadcrumb path, tag names, etc.) hit \`GET /:id\` after create.\
> \
> \### Top-level fields\
> \
> \- \`name\` — required (1..255 chars).\
> \- \`category\_id\` — required UUID. Must reference an existing (non-deleted) category.\
> \- \`type\` — optional \*\*name\*\* string. Server finds (case-insensitive) or creates a matching \`product\_type\` row. Preserves the caller's original casing on create.\
> \- \`vendors\` — optional array of \*\*name\*\* strings (max 50). Server finds-or-creates each vendor and links them via \`product\_meta\_vendor\_relation\` (many-to-many).\
> \- \`tags\` — optional array of tag \*\*name\*\* strings (max 20). Server finds-or-creates and links via \`product\_tag\_relations\`.\
> \- \`configurations\` — optional array of nested configurations (see below). Omit to get one default configuration seeded.\
> \- \`publish\` — optional boolean, default \`false\`. When \`true\`, an in-memory publish-readiness check runs (every configuration must have ≥1 active variant, every variant must have a non-empty SKU); if it passes, the whole product is built as both draft and published in one Kysely transaction.\
> \
> \### Configurations\
> \
> Each entry:\
> \
> \- \`name\` — required (1..255 chars).\
> \- \`sku\` — optional fallback SKU for auto-generated variants that aren't overridden per-combination.\
> \- \`description\` — optional (max 2000 chars).\
> \- \`order\` — optional integer (defaults to array index).\
> \- \`options\` — optional array of \`{ name, order?, values: \[{ value, order? }] }\`, capped at \*\*3\*\* per configuration. When provided, the server computes the cartesian product of the option values and generates one variant per combination. Every generated variant inherits \`sku = configuration.sku\` and empty \`barcode\` unless overridden by an entry in \`variants\[]\`.\
> \- \`variants\` — optional \*\*sparse\*\* list of per-combination customizations. Each entry has \`{ combination: \[{ option\_name, value }], sku?, barcode?, is\_active? }\` — only the fields you send are overridden. Unmatched or duplicate combinations return \`400\`. For configs \*\*without\*\* \`options\`, send at most one entry and omit \`combination\` — the entry overrides the auto-seeded \`Default\` variant.\
> \- \`workflow\_templates\` — optional array (see below), capped at \*\*9\*\* per configuration. Omit to get one default workflow with a single default task.\
> \
> \### Workflow templates\
> \
> Each entry:\
> \
> \- \`name\` — required.\
> \- \`order\` — optional integer.\
> \- \`items\` — array of nodes. Each item is exactly one of:\
> &#x20; \- \`{ task: { name, description?, time\_limit?, responsibilities? } }\` — the server creates a new \`workflow\_task\_template\` and links it. If \`responsibilities\` is omitted, one \`Responsibility 1\` is auto-created with no relations.\
> &#x20; \- \`{ nested\_workflow\_template\_id: uuid }\` — references an existing \`workflow\_template\`.\
> &#x20; Items are auto-positioned vertically on the canvas (\`position\_x = 100\`, \`position\_y = 100 + index \* 150\`); edges are not accepted in this iteration.\
> \- \`bom\` — optional array of BOM entries \`{ variant\_combination?, items: \[{ material\_id, quantity? }] }\`. When the enclosing configuration has options, \`variant\_combination\` is required to identify which auto-generated variant the BOM attaches to. \`material\_id\` must reference an existing material.\
> \
> \### Responsibilities on a task\
> \
> Each responsibility supports:\
> \
> \- \`name\` — optional (defaults to \`"Responsibility 1"\`).\
> \- \`responsibility\_count\` — optional integer, defaults to \`1\`.\
> \- \`assigment\_type\` — optional enum (\`Manual\` / \`Auto\` / \`Self\_Assignment\`), defaults to \`Manual\`.\
> \- \`department\_ids\`, \`position\_type\_ids\`, \`user\_ids\` — optional UUID arrays; each id must exist in the tenant.\
> \
> \### Uniqueness rules enforced up-front\
> \
> \- \*\*SKUs\*\*: a configuration owns a SKU namespace (its own \`sku\` plus every one of its variants'). Two configurations in the same request can't share any SKU. Any SKU already used on another product's configuration or variant → \`409\`.\
> \- \*\*Barcodes\*\*: two variants can share a barcode only if they're the same logical variant across versions. Any duplicate barcode within the request, or one already used on another product's variant → \`409\`.\
> \
> \### Validation error catalogue\
> \
> \- \`400\` — missing required field, invalid enum, unmatched \`variants\[].combination\`, duplicate variant combination, workflow item with neither \`task\` nor \`nested\_workflow\_template\_id\` (or both), a configuration with more than 3 options, or a configuration with more than 9 workflow templates.\
> \- \`404\` — unknown \`category\_id\`, \`nested\_workflow\_template\_id\`, \`material\_id\`, \`department\_id\`, \`position\_type\_id\`, or \`user\_id\`.\
> \- \`409\` — SKU collision (within the request or against the tenant), barcode collision, or publish-readiness failure (missing SKU / configuration without an active variant).<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicCreateProductDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"category_id":{"type":"string","format":"uuid"},"type":{"type":"string","minLength":1,"maxLength":255,"description":"Product type NAME. Server finds (case-insensitive) or creates a ProductType."},"vendors":{"maxItems":50,"description":"Product vendor NAMES. Server finds (case-insensitive) or creates each vendor and links them all to the product meta.","type":"array","items":{"type":"string"}},"tags":{"maxItems":20,"description":"Tag names — server finds-or-creates.","type":"array","items":{"type":"string"}},"configurations":{"description":"Full nested product tree. When omitted, the server seeds one default configuration / variant / workflow / task.","type":"array","items":{"$ref":"#/components/schemas/PublicCreateProductConfigurationDto"}},"publish":{"type":"boolean","default":false,"description":"When true, publish the product in the same call. Creates a published Product row alongside the draft; readiness validated in-memory (each variant needs a non-empty SKU + ≥1 active variant per config)."}},"required":["name","category_id"]},"PublicCreateProductConfigurationDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"sku":{"type":"string","maxLength":255,"description":"Fallback SKU for auto-generated variants that aren't overridden by `variants[]`."},"description":{"type":"string","maxLength":2000},"order":{"type":"number","minimum":0,"default":0},"options":{"description":"Options produce cartesian-product variants. Maximum 3 per configuration.","type":"array","items":{"$ref":"#/components/schemas/PublicCreateProductOptionDto"}},"variants":{"description":"Sparse per-combination customizations. Unmatched or duplicate combinations => 400.","type":"array","items":{"$ref":"#/components/schemas/PublicCreateVariantDto"}},"workflow_templates":{"description":"Workflow templates for the configuration. Maximum 9 per configuration.","type":"array","items":{"$ref":"#/components/schemas/PublicCreateWorkflowTemplateDto"}}},"required":["name"]},"PublicCreateProductOptionDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"order":{"type":"number","minimum":0,"default":0},"values":{"minItems":1,"type":"array","items":{"$ref":"#/components/schemas/PublicCreateProductOptionValueDto"}}},"required":["name","values"]},"PublicCreateProductOptionValueDto":{"type":"object","properties":{"value":{"type":"string","minLength":1,"maxLength":255},"order":{"type":"number","minimum":0,"default":0}},"required":["value"]},"PublicCreateVariantDto":{"type":"object","properties":{"combination":{"minItems":1,"description":"Required for configs with `options`; omit for configs without options.","type":"array","items":{"$ref":"#/components/schemas/PublicVariantCombinationEntryDto"}},"sku":{"type":"string","maxLength":255},"barcode":{"type":"string","maxLength":255},"is_active":{"type":"boolean"}}},"PublicVariantCombinationEntryDto":{"type":"object","properties":{"option_name":{"type":"string","minLength":1,"maxLength":255},"value":{"type":"string","minLength":1,"maxLength":255}},"required":["option_name","value"]},"PublicCreateWorkflowTemplateDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"order":{"type":"number","minimum":0,"default":0},"items":{"type":"array","items":{"$ref":"#/components/schemas/PublicCreateWorkflowItemDto"}},"bom":{"type":"array","items":{"$ref":"#/components/schemas/PublicCreateBomEntryDto"}}},"required":["name"]},"PublicCreateWorkflowItemDto":{"type":"object","properties":{"task":{"$ref":"#/components/schemas/PublicCreateTaskDto"},"nested_workflow_template_id":{"type":"string","format":"uuid"}}},"PublicCreateTaskDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000},"time_limit":{"type":"number","minimum":0,"default":0},"responsibilities":{"type":"array","items":{"$ref":"#/components/schemas/PublicCreateTaskResponsibilityDto"}}},"required":["name"]},"PublicCreateTaskResponsibilityDto":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"Defaults to 'Responsibility 1' when omitted."},"responsibility_count":{"type":"number","minimum":1,"default":1},"assigment_type":{"default":"Manual","allOf":[{"$ref":"#/components/schemas/AssigmentType"}]},"department_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"position_type_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"user_ids":{"type":"array","items":{"type":"string","format":"uuid"}}}},"AssigmentType":{"type":"string","enum":["Manual","Auto","Self_Assignment"]},"PublicCreateBomEntryDto":{"type":"object","properties":{"variant_combination":{"description":"Omit for the default (single-variant) config; required for multi-variant configs.","type":"array","items":{"$ref":"#/components/schemas/PublicVariantCombinationEntryDto"}},"items":{"minItems":1,"type":"array","items":{"$ref":"#/components/schemas/PublicCreateBomItemDto"}}},"required":["items"]},"PublicCreateBomItemDto":{"type":"object","properties":{"material_id":{"type":"string","format":"uuid"},"quantity":{"type":"number","default":1}},"required":["material_id"]},"PublicCreatedProductDto":{"type":"object","properties":{"id":{"type":"string","description":"Draft product id — the canonical id the caller uses on every subsequent call."},"name":{"type":"string"},"category_id":{"type":"string"},"type_id":{"type":"string","nullable":true,"description":"Resolved `product_type` id or null."},"vendor_ids":{"type":"array","items":{"type":"string"}},"tag_ids":{"type":"array","items":{"type":"string"}},"status":{"allOf":[{"$ref":"#/components/schemas/PublicProductStatus"}]},"is_active":{"type":"boolean"},"has_published_version":{"type":"boolean"},"published_id":{"type":"string","nullable":true,"description":"When `publish=true` was set on create, the id of the published `Product` row (mirror). Null otherwise."},"created_at":{"type":"string","nullable":true}},"required":["id","name","category_id","type_id","vendor_ids","tag_ids","status","is_active","has_published_version","published_id","created_at"]},"PublicProductStatus":{"type":"string","enum":["draft","published"]}}},"paths":{"/api/v1/public/products":{"post":{"description":"\nCreates a fully-formed product in a single POST. Everything the internal editor would create piece-by-piece — configurations, options → auto-generated variants, workflow templates + task templates + responsibilities, links, BOM — can be supplied in one nested body. Anything omitted is filled with sensible defaults (a \"Default\" configuration, one default variant, one default workflow with a \"Default\" task, one \"Responsibility 1\"). Set `publish: true` to publish the product in the same request; a **draft** `Product` row is always created and, when publishing, a mirrored **published** row is created alongside it.\n\n**Response** is a flat record (see `PublicCreatedProductDto`): the draft `id`, resolved reference ids (`category_id`, `type_id`, `vendor_ids`, `tag_ids`), `status`, and (when publishing) the mirror's `published_id`. No enrichment — no category breadcrumb, no tag rehydration, no hasPublishedVersion post-lookup. If you need the fully-hydrated shape (with breadcrumb path, tag names, etc.) hit `GET /:id` after create.\n\n### Top-level fields\n\n- `name` — required (1..255 chars).\n- `category_id` — required UUID. Must reference an existing (non-deleted) category.\n- `type` — optional **name** string. Server finds (case-insensitive) or creates a matching `product_type` row. Preserves the caller's original casing on create.\n- `vendors` — optional array of **name** strings (max 50). Server finds-or-creates each vendor and links them via `product_meta_vendor_relation` (many-to-many).\n- `tags` — optional array of tag **name** strings (max 20). Server finds-or-creates and links via `product_tag_relations`.\n- `configurations` — optional array of nested configurations (see below). Omit to get one default configuration seeded.\n- `publish` — optional boolean, default `false`. When `true`, an in-memory publish-readiness check runs (every configuration must have ≥1 active variant, every variant must have a non-empty SKU); if it passes, the whole product is built as both draft and published in one Kysely transaction.\n\n### Configurations\n\nEach entry:\n\n- `name` — required (1..255 chars).\n- `sku` — optional fallback SKU for auto-generated variants that aren't overridden per-combination.\n- `description` — optional (max 2000 chars).\n- `order` — optional integer (defaults to array index).\n- `options` — optional array of `{ name, order?, values: [{ value, order? }] }`, capped at **3** per configuration. When provided, the server computes the cartesian product of the option values and generates one variant per combination. Every generated variant inherits `sku = configuration.sku` and empty `barcode` unless overridden by an entry in `variants[]`.\n- `variants` — optional **sparse** list of per-combination customizations. Each entry has `{ combination: [{ option_name, value }], sku?, barcode?, is_active? }` — only the fields you send are overridden. Unmatched or duplicate combinations return `400`. For configs **without** `options`, send at most one entry and omit `combination` — the entry overrides the auto-seeded `Default` variant.\n- `workflow_templates` — optional array (see below), capped at **9** per configuration. Omit to get one default workflow with a single default task.\n\n### Workflow templates\n\nEach entry:\n\n- `name` — required.\n- `order` — optional integer.\n- `items` — array of nodes. Each item is exactly one of:\n  - `{ task: { name, description?, time_limit?, responsibilities? } }` — the server creates a new `workflow_task_template` and links it. If `responsibilities` is omitted, one `Responsibility 1` is auto-created with no relations.\n  - `{ nested_workflow_template_id: uuid }` — references an existing `workflow_template`.\n  Items are auto-positioned vertically on the canvas (`position_x = 100`, `position_y = 100 + index * 150`); edges are not accepted in this iteration.\n- `bom` — optional array of BOM entries `{ variant_combination?, items: [{ material_id, quantity? }] }`. When the enclosing configuration has options, `variant_combination` is required to identify which auto-generated variant the BOM attaches to. `material_id` must reference an existing material.\n\n### Responsibilities on a task\n\nEach responsibility supports:\n\n- `name` — optional (defaults to `\"Responsibility 1\"`).\n- `responsibility_count` — optional integer, defaults to `1`.\n- `assigment_type` — optional enum (`Manual` / `Auto` / `Self_Assignment`), defaults to `Manual`.\n- `department_ids`, `position_type_ids`, `user_ids` — optional UUID arrays; each id must exist in the tenant.\n\n### Uniqueness rules enforced up-front\n\n- **SKUs**: a configuration owns a SKU namespace (its own `sku` plus every one of its variants'). Two configurations in the same request can't share any SKU. Any SKU already used on another product's configuration or variant → `409`.\n- **Barcodes**: two variants can share a barcode only if they're the same logical variant across versions. Any duplicate barcode within the request, or one already used on another product's variant → `409`.\n\n### Validation error catalogue\n\n- `400` — missing required field, invalid enum, unmatched `variants[].combination`, duplicate variant combination, workflow item with neither `task` nor `nested_workflow_template_id` (or both), a configuration with more than 3 options, or a configuration with more than 9 workflow templates.\n- `404` — unknown `category_id`, `nested_workflow_template_id`, `material_id`, `department_id`, `position_type_id`, or `user_id`.\n- `409` — SKU collision (within the request or against the tenant), barcode collision, or publish-readiness failure (missing SKU / configuration without an active variant).\n","operationId":"PublicProductsController_create_v1","parameters":[{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCreateProductDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCreatedProductDto"}}}},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Create product (optionally publish in one call)","tags":["Products"]}}}}
```

## Get product by id

> \
> Returns the draft product record with related entities: category (with breadcrumb path), type, vendors (all linked via the M2M relation, sorted by name), tags, and \`has\_published\_version\` — \`true\` once a publish has happened for this meta (either via \`publish=true\` on create or a future publish endpoint). 404 if the id is unknown or the record has been soft-deleted.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicProductDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"$ref":"#/components/schemas/PublicProductCategoryDto"},"type":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/IdNameDto"}]},"vendors":{"description":"All vendors linked to the product meta (M2M).","type":"array","items":{"$ref":"#/components/schemas/IdNameDto"}},"tags":{"type":"array","items":{"$ref":"#/components/schemas/IdNameDto"}},"status":{"allOf":[{"$ref":"#/components/schemas/PublicProductStatus"}]},"is_active":{"type":"boolean"},"has_published_version":{"type":"boolean","description":"True when at least one published version exists for this product."},"created_at":{"type":"string","nullable":true},"modified_at":{"type":"string","nullable":true}},"required":["id","name","category","type","vendors","tags","status","is_active","has_published_version","created_at","modified_at"]},"PublicProductCategoryDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"path":{"description":"Root→parent breadcrumbs; empty when the category is a root.","type":"array","items":{"$ref":"#/components/schemas/IdNameDto"}}},"required":["id","name","path"]},"IdNameDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]},"PublicProductStatus":{"type":"string","enum":["draft","published"]}}},"paths":{"/api/v1/public/products/{id}":{"get":{"description":"\nReturns the draft product record with related entities: category (with breadcrumb path), type, vendors (all linked via the M2M relation, sorted by name), tags, and `has_published_version` — `true` once a publish has happened for this meta (either via `publish=true` on create or a future publish endpoint). 404 if the id is unknown or the record has been soft-deleted.\n","operationId":"PublicProductsController_getById_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProductDto"}}}},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Get product by id","tags":["Products"]}}}}
```

## Soft-delete product

> \
> Marks the draft product as deleted by setting \`is\_deleted = true\`. Non-cascading — configurations, meta, and any published sibling versions remain untouched (they simply become unreachable from the public API list/get). 204 on success; 404 if the id is unknown or already deleted.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"paths":{"/api/v1/public/products/{id}":{"delete":{"description":"\nMarks the draft product as deleted by setting `is_deleted = true`. Non-cascading — configurations, meta, and any published sibling versions remain untouched (they simply become unreachable from the public API list/get). 204 on success; 404 if the id is unknown or already deleted.\n","operationId":"PublicProductsController_softDelete_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Soft-delete product","tags":["Products"]}}}}
```

## Update product

> \
> Updates any subset of the base product fields. Configurations, workflows, media, variants, and BOM live under separate endpoints (not exposed yet).\
> \
> \### Body semantics\
> \
> \- \`name\`, \`category\_id\` — value update. Category must exist.\
> \- \`type\` — vendor \*\*name\*\* string. Server finds (case-insensitive) or creates the type. Pass \`null\` to clear; omit to leave unchanged.\
> \- \`vendors\` — full-reconcile: the product ends up linked to exactly the supplied vendor \*\*names\*\* (M2M via \`product\_meta\_vendor\_relation\`). Missing names are created; existing rows are reused (case-insensitive match); rows not listed are unlinked. Send \`\[]\` to clear all vendors.\
> \- \`tags\` — full-reconcile: the product ends up linked to exactly the supplied tag \*names\*. Missing names are created; existing rows are reused (case-insensitive match); rows not listed are unlinked from the product (the underlying \`product\_tag\` row is left intact for reuse).\
> \- \`is\_active\` — toggles the \`Product.is\_active\` flag.\
> \
> Returns the refreshed product DTO with \`vendors\` as an array. 404 if unknown or soft-deleted.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicUpdateProductDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"category_id":{"type":"string","format":"uuid"},"type":{"type":"string","nullable":true,"minLength":1,"maxLength":255,"description":"Product type NAME. Server finds (case-insensitive) or creates a ProductType. Send null to clear."},"vendors":{"maxItems":50,"description":"Full-reconcile: exactly these vendor NAMES will be linked to the product meta after the call. Server finds-or-creates each vendor. Send `[]` to clear all vendor links.","type":"array","items":{"type":"string"}},"tags":{"maxItems":20,"description":"Full-reconcile: exactly these tag names will be on the product.","type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"}}},"PublicProductDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"category":{"$ref":"#/components/schemas/PublicProductCategoryDto"},"type":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/IdNameDto"}]},"vendors":{"description":"All vendors linked to the product meta (M2M).","type":"array","items":{"$ref":"#/components/schemas/IdNameDto"}},"tags":{"type":"array","items":{"$ref":"#/components/schemas/IdNameDto"}},"status":{"allOf":[{"$ref":"#/components/schemas/PublicProductStatus"}]},"is_active":{"type":"boolean"},"has_published_version":{"type":"boolean","description":"True when at least one published version exists for this product."},"created_at":{"type":"string","nullable":true},"modified_at":{"type":"string","nullable":true}},"required":["id","name","category","type","vendors","tags","status","is_active","has_published_version","created_at","modified_at"]},"PublicProductCategoryDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"path":{"description":"Root→parent breadcrumbs; empty when the category is a root.","type":"array","items":{"$ref":"#/components/schemas/IdNameDto"}}},"required":["id","name","path"]},"IdNameDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]},"PublicProductStatus":{"type":"string","enum":["draft","published"]}}},"paths":{"/api/v1/public/products/{id}":{"patch":{"description":"\nUpdates any subset of the base product fields. Configurations, workflows, media, variants, and BOM live under separate endpoints (not exposed yet).\n\n### Body semantics\n\n- `name`, `category_id` — value update. Category must exist.\n- `type` — vendor **name** string. Server finds (case-insensitive) or creates the type. Pass `null` to clear; omit to leave unchanged.\n- `vendors` — full-reconcile: the product ends up linked to exactly the supplied vendor **names** (M2M via `product_meta_vendor_relation`). Missing names are created; existing rows are reused (case-insensitive match); rows not listed are unlinked. Send `[]` to clear all vendors.\n- `tags` — full-reconcile: the product ends up linked to exactly the supplied tag *names*. Missing names are created; existing rows are reused (case-insensitive match); rows not listed are unlinked from the product (the underlying `product_tag` row is left intact for reuse).\n- `is_active` — toggles the `Product.is_active` flag.\n\nReturns the refreshed product DTO with `vendors` as an array. 404 if unknown or soft-deleted.\n","operationId":"PublicProductsController_update_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUpdateProductDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProductDto"}}}},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Update product","tags":["Products"]}}}}
```

## Publish product draft

> \
> Publishes an existing draft: creates a new \*\*published\*\* \`Product\` row that shares the draft's \`product\_meta\_id\` and clones its configurations onto the new row via the internal duplicate flow. Idempotent-safe against concurrent invocations — while a publish is in flight the draft is flagged \`is\_publish\_in\_progress\` and a second call returns 409.\
> \
> Validation runs before the snapshot: every configuration must have ≥1 active variant, every variant must have a non-empty SKU, and every workflow template must be internally valid. A failure returns 409 with the offending detail in the message; on success the draft's \`version\` is bumped so subsequent edits happen against the next version cycle.\
> \
> \### Errors\
> \
> \- \`404\` — draft not found, or the id belongs to something that isn't a draft.\
> \- \`409\` — publish already in progress, or the draft fails a publish-readiness check.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"MessageDto":{"type":"object","properties":{"message":{"type":"string","description":"Message returned from API confirming the operation"}},"required":["message"]}}},"paths":{"/api/v1/public/products/{id}/publish":{"post":{"description":"\nPublishes an existing draft: creates a new **published** `Product` row that shares the draft's `product_meta_id` and clones its configurations onto the new row via the internal duplicate flow. Idempotent-safe against concurrent invocations — while a publish is in flight the draft is flagged `is_publish_in_progress` and a second call returns 409.\n\nValidation runs before the snapshot: every configuration must have ≥1 active variant, every variant must have a non-empty SKU, and every workflow template must be internally valid. A failure returns 409 with the offending detail in the message; on success the draft's `version` is bumped so subsequent edits happen against the next version cycle.\n\n### Errors\n\n- `404` — draft not found, or the id belongs to something that isn't a draft.\n- `409` — publish already in progress, or the draft fails a publish-readiness check.\n","operationId":"PublicProductsController_publish_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageDto"}}}},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Publish product draft","tags":["Products"]}}}}
```

## List configurations for a draft product

> \
> Returns the configurations attached to the draft in \`order\` ascending. Only the draft's own configurations are returned; published versions of the product are not exposed here.\
> \
> \### Errors\
> \
> \- \`404\` — draft not found (or the id belongs to something that isn't a draft).<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicProductConfigurationDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sku":{"type":"string"},"description":{"type":"string"},"order":{"type":"number"},"is_active":{"type":"boolean"},"created_at":{"type":"string","nullable":true},"modified_at":{"type":"string","nullable":true}},"required":["id","name","sku","description","order","is_active","created_at","modified_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations":{"get":{"description":"\nReturns the configurations attached to the draft in `order` ascending. Only the draft's own configurations are returned; published versions of the product are not exposed here.\n\n### Errors\n\n- `404` — draft not found (or the id belongs to something that isn't a draft).\n","operationId":"PublicProductConfigurationsController_listConfigurations_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicProductConfigurationDto"}}}}},"404":{"description":"Product draft not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List configurations for a draft product","tags":["Products"]}}}}
```

## Add a configuration to a product draft

> \
> Creates a new configuration under the draft product and seeds one default variant so the configuration is publishable-ready. If \`order\` is omitted the server appends after the current configurations.\
> \
> \### Body\
> \
> \- \`name\` — required (1..255).\
> \- \`sku\` — required (1..255).\
> \- \`description\` — optional (max 2000).\
> \- \`order\` — optional integer ≥ 0. Defaults to current configuration count.\
> \- \`is\_active\` — optional boolean. Defaults to true.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure on the body.\
> \- \`404\` — draft not found.\
> \- \`409\` — product already has the maximum of 25 configurations, or the supplied \`sku\` collides with another configuration/variant in the tenant.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicCreateConfigurationDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"sku":{"type":"string","maxLength":255},"description":{"type":"string","maxLength":2000},"order":{"type":"number","minimum":0},"is_active":{"type":"boolean"}},"required":["name","sku"]},"PublicProductConfigurationDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sku":{"type":"string"},"description":{"type":"string"},"order":{"type":"number"},"is_active":{"type":"boolean"},"created_at":{"type":"string","nullable":true},"modified_at":{"type":"string","nullable":true}},"required":["id","name","sku","description","order","is_active","created_at","modified_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations":{"post":{"description":"\nCreates a new configuration under the draft product and seeds one default variant so the configuration is publishable-ready. If `order` is omitted the server appends after the current configurations.\n\n### Body\n\n- `name` — required (1..255).\n- `sku` — required (1..255).\n- `description` — optional (max 2000).\n- `order` — optional integer ≥ 0. Defaults to current configuration count.\n- `is_active` — optional boolean. Defaults to true.\n\n### Errors\n\n- `400` — validation failure on the body.\n- `404` — draft not found.\n- `409` — product already has the maximum of 25 configurations, or the supplied `sku` collides with another configuration/variant in the tenant.\n","operationId":"PublicProductConfigurationsController_createConfiguration_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCreateConfigurationDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProductConfigurationDto"}}}},"404":{"description":"Product draft not found."},"409":{"description":"Configuration cap reached (max 25 per product) or SKU collision."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Add a configuration to a product draft","tags":["Products"]}}}}
```

## Update a configuration (name / sku / description)

> \
> Partial-update of a configuration. Any subset of \`name\`, \`sku\`, \`description\` may be supplied.\
> \
> \- \`update\_name\_in\_all\_versions\` — when \`true\` and \`name\` is provided, the rename is broadcast to every configuration with the same current name across every published version of the product. When \`false\` or omitted, only the addressed configuration row is renamed.\
> \- \`sku\` and \`description\` \*\*always\*\* update just the addressed configuration — they are per-version snapshots and are never broadcast, even with \`update\_name\_in\_all\_versions\` set.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure on the body.\
> \- \`404\` — draft or configuration not found (configuration must belong to the given product).\
> \- \`409\` — SKU collision.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicUpdateConfigurationDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"sku":{"type":"string","maxLength":255},"description":{"type":"string","maxLength":2000},"update_name_in_all_versions":{"type":"boolean","default":false,"description":"When true and `name` is provided, the rename is broadcast to every configuration with the same current name across all published versions of the product. `sku` and `description` are always applied only to the addressed configuration (they are per-version snapshots)."}}},"PublicProductConfigurationDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sku":{"type":"string"},"description":{"type":"string"},"order":{"type":"number"},"is_active":{"type":"boolean"},"created_at":{"type":"string","nullable":true},"modified_at":{"type":"string","nullable":true}},"required":["id","name","sku","description","order","is_active","created_at","modified_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}":{"put":{"description":"\nPartial-update of a configuration. Any subset of `name`, `sku`, `description` may be supplied.\n\n- `update_name_in_all_versions` — when `true` and `name` is provided, the rename is broadcast to every configuration with the same current name across every published version of the product. When `false` or omitted, only the addressed configuration row is renamed.\n- `sku` and `description` **always** update just the addressed configuration — they are per-version snapshots and are never broadcast, even with `update_name_in_all_versions` set.\n\n### Errors\n\n- `400` — validation failure on the body.\n- `404` — draft or configuration not found (configuration must belong to the given product).\n- `409` — SKU collision.\n","operationId":"PublicProductConfigurationsController_updateConfiguration_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUpdateConfigurationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProductConfigurationDto"}}}},"404":{"description":"Product draft or configuration not found."},"409":{"description":"SKU collision."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Update a configuration (name / sku / description)","tags":["Products"]}}}}
```

## Delete a configuration

> \
> Hard-deletes the configuration together with its variants, workflow templates, and BOMs.\
> \
> Blocked in two situations, both returning 409:\
> \
> \- The configuration is the last remaining one on the product.\
> \- Any \`production\_workflow\` references one of the configuration's variants (regardless of status).\
> \
> \### Errors\
> \
> \- \`404\` — draft or configuration not found.\
> \- \`409\` — last configuration on the product, or a production workflow references its variants.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}":{"delete":{"description":"\nHard-deletes the configuration together with its variants, workflow templates, and BOMs.\n\nBlocked in two situations, both returning 409:\n\n- The configuration is the last remaining one on the product.\n- Any `production_workflow` references one of the configuration's variants (regardless of status).\n\n### Errors\n\n- `404` — draft or configuration not found.\n- `409` — last configuration on the product, or a production workflow references its variants.\n","operationId":"PublicProductConfigurationsController_deleteConfiguration_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""},"404":{"description":"Product draft or configuration not found."},"409":{"description":"Last configuration on the product, or a production workflow references its variants."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a configuration","tags":["Products"]}}}}
```

## List workflow templates for a configuration

> \
> Returns the workflow templates attached to the configuration, ordered by \`order\` ascending. Only the draft configuration's own workflow templates are returned; workflow templates on published versions are not exposed here.\
> \
> \### Errors\
> \
> \- \`404\` — draft or configuration not found (configuration must belong to the given product).<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicWorkflowTemplateDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number"},"is_active":{"type":"boolean"},"prefer_auto_assign_users_with_prior_experience":{"type":"boolean","description":"Prefer auto-assigning users with prior experience on this workflow."},"created_at":{"type":"string","nullable":true}},"required":["id","name","order","is_active","prefer_auto_assign_users_with_prior_experience","created_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows":{"get":{"description":"\nReturns the workflow templates attached to the configuration, ordered by `order` ascending. Only the draft configuration's own workflow templates are returned; workflow templates on published versions are not exposed here.\n\n### Errors\n\n- `404` — draft or configuration not found (configuration must belong to the given product).\n","operationId":"PublicWorkflowTemplatesController_listWorkflowTemplates_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicWorkflowTemplateDto"}}}}},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List workflow templates for a configuration","tags":["Products"]}}}}
```

## Add a workflow template to a configuration

> \
> Creates a bare workflow template row on the configuration. Task templates, template items, and edges are not seeded here — callers will wire those via a future set of endpoints.\
> \
> \### Body\
> \
> \- \`name\` — required (1..255).\
> \- \`order\` — optional integer ≥ 0. Defaults to the current workflow count.\
> \- \`is\_active\` — optional boolean. Defaults to true.\
> \- \`prefer\_auto\_assign\_users\_with\_prior\_experience\` — optional boolean. Defaults to true (matches the DB column default).\
> \
> \### Errors\
> \
> \- \`400\` — validation failure on the body.\
> \- \`404\` — draft or configuration not found.\
> \- \`409\` — configuration already has the maximum of 9 workflow templates.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicWorkflowTemplateCreateDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"order":{"type":"number","minimum":0},"is_active":{"type":"boolean"},"prefer_auto_assign_users_with_prior_experience":{"type":"boolean"}},"required":["name"]},"PublicWorkflowTemplateDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number"},"is_active":{"type":"boolean"},"prefer_auto_assign_users_with_prior_experience":{"type":"boolean","description":"Prefer auto-assigning users with prior experience on this workflow."},"created_at":{"type":"string","nullable":true}},"required":["id","name","order","is_active","prefer_auto_assign_users_with_prior_experience","created_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows":{"post":{"description":"\nCreates a bare workflow template row on the configuration. Task templates, template items, and edges are not seeded here — callers will wire those via a future set of endpoints.\n\n### Body\n\n- `name` — required (1..255).\n- `order` — optional integer ≥ 0. Defaults to the current workflow count.\n- `is_active` — optional boolean. Defaults to true.\n- `prefer_auto_assign_users_with_prior_experience` — optional boolean. Defaults to true (matches the DB column default).\n\n### Errors\n\n- `400` — validation failure on the body.\n- `404` — draft or configuration not found.\n- `409` — configuration already has the maximum of 9 workflow templates.\n","operationId":"PublicWorkflowTemplatesController_createWorkflowTemplate_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowTemplateCreateDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowTemplateDto"}}}},"404":{"description":"Product draft or configuration not found."},"409":{"description":"Configuration already has the maximum of 9 workflow templates."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Add a workflow template to a configuration","tags":["Products"]}}}}
```

## Update a workflow template (name / order / flags)

> \
> Partial-update of a workflow template. Any subset of \`name\`, \`order\`, \`is\_active\`, \`prefer\_auto\_assign\_users\_with\_prior\_experience\` may be supplied. Updates the addressed workflow template row only — no broadcast across product versions.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure on the body.\
> \- \`404\` — draft, configuration, or workflow not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicWorkflowTemplateUpdateDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"order":{"type":"number","minimum":0},"is_active":{"type":"boolean"},"prefer_auto_assign_users_with_prior_experience":{"type":"boolean"}}},"PublicWorkflowTemplateDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number"},"is_active":{"type":"boolean"},"prefer_auto_assign_users_with_prior_experience":{"type":"boolean","description":"Prefer auto-assigning users with prior experience on this workflow."},"created_at":{"type":"string","nullable":true}},"required":["id","name","order","is_active","prefer_auto_assign_users_with_prior_experience","created_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows/{workflow_id}":{"put":{"description":"\nPartial-update of a workflow template. Any subset of `name`, `order`, `is_active`, `prefer_auto_assign_users_with_prior_experience` may be supplied. Updates the addressed workflow template row only — no broadcast across product versions.\n\n### Errors\n\n- `400` — validation failure on the body.\n- `404` — draft, configuration, or workflow not found.\n","operationId":"PublicWorkflowTemplatesController_updateWorkflowTemplate_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workflow_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowTemplateUpdateDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowTemplateDto"}}}},"404":{"description":"Product draft, configuration, or workflow template not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Update a workflow template (name / order / flags)","tags":["Products"]}}}}
```

## Delete a workflow template

> \
> Hard-deletes the workflow template together with its task templates, template items, workflow edges, and BOMs.\
> \
> Blocked in two situations, both returning 409:\
> \
> \- The workflow is the last remaining one on the configuration.\
> \- Any \`production\_workflow\` references one of the configuration's variants (regardless of status) — same rule as configuration delete.\
> \
> \### Errors\
> \
> \- \`404\` — draft, configuration, or workflow not found.\
> \- \`409\` — last workflow on the configuration, or a production workflow references its variants.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows/{workflow_id}":{"delete":{"description":"\nHard-deletes the workflow template together with its task templates, template items, workflow edges, and BOMs.\n\nBlocked in two situations, both returning 409:\n\n- The workflow is the last remaining one on the configuration.\n- Any `production_workflow` references one of the configuration's variants (regardless of status) — same rule as configuration delete.\n\n### Errors\n\n- `404` — draft, configuration, or workflow not found.\n- `409` — last workflow on the configuration, or a production workflow references its variants.\n","operationId":"PublicWorkflowTemplatesController_deleteWorkflowTemplate_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workflow_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""},"404":{"description":"Product draft, configuration, or workflow template not found."},"409":{"description":"Last workflow on the configuration, or a production workflow references its variants."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a workflow template","tags":["Products"]}}}}
```

## List tasks on a workflow

> \
> Returns the task templates attached to the workflow, ordered by \`order\` ascending. Component items (nested workflow references) are excluded — this endpoint is task-only. Use POST/DELETE on \`.../tasks\` to manage component items.\
> \
> \### Query\
> \
> \- \`search\` — optional case-insensitive substring match on task name.\
> \
> \### Errors\
> \
> \- \`404\` — product draft, configuration, or workflow not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicWorkflowTaskDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"order":{"type":"number"},"time_limit":{"type":"number"},"basic_reward":{"type":"number"},"workflow_template_id":{"type":"string"},"created_at":{"type":"string","nullable":true}},"required":["id","name","description","order","time_limit","basic_reward","workflow_template_id","created_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows/{workflow_id}/tasks":{"get":{"description":"\nReturns the task templates attached to the workflow, ordered by `order` ascending. Component items (nested workflow references) are excluded — this endpoint is task-only. Use POST/DELETE on `.../tasks` to manage component items.\n\n### Query\n\n- `search` — optional case-insensitive substring match on task name.\n\n### Errors\n\n- `404` — product draft, configuration, or workflow not found.\n","operationId":"PublicWorkflowTasksController_listWorkflowTasks_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workflow_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"search","required":false,"in":"query","description":"Case-insensitive substring match on task name.","schema":{"maxLength":100,"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicWorkflowTaskDto"}}}}},"404":{"description":"Product draft, configuration, or workflow not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List tasks on a workflow","tags":["Products"]}}}}
```

## Add a task or component to a workflow

> \
> Creates a new canvas node on the addressed workflow. The endpoint runs in two modes, distinguished by the presence of \`nested\_workflow\_template\_id\` in the body.\
> \
> \### Task mode\
> \
> Send \`name\` (+ optional \`description\`, \`order\`, \`time\_limit\`, \`basic\_reward\`, \`responsibilities\`). The server creates a fresh \`workflow\_task\_template\`, its \`workflow\_template\_item\` canvas node, and either the responsibilities you supply or a default \`Responsibility 1\` (no relations).\
> \
> \### Component mode\
> \
> Send only \`nested\_workflow\_template\_id\` (must reference an existing workflow template in the tenant). The server creates a \`workflow\_template\_item\` that references that workflow as a nested component; no new task template is created.\
> \
> The new item is positioned at \`(position\_x = 100, position\_y = MAX(existing position\_y on this workflow) + 150)\` — items never share coordinates, even after prior deletes.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure, body specifies both modes (\`name\` + \`nested\_workflow\_template\_id\`), or specifies neither.\
> \- \`404\` — draft, configuration, workflow, \`nested\_workflow\_template\_id\`, or any \`department\_id\` / \`position\_type\_id\` / \`user\_id\` inside a responsibility not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicWorkflowTaskCreateDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"description":{"type":"string","maxLength":2000},"order":{"type":"number","minimum":0},"time_limit":{"type":"number","minimum":0},"basic_reward":{"type":"number","minimum":0},"responsibilities":{"description":"Optional responsibility list — each entry may specify department_ids / position_type_ids / user_ids (all validated for existence). When omitted the server seeds one default \"Responsibility 1\" with no relations.","type":"array","items":{"$ref":"#/components/schemas/PublicCreateTaskResponsibilityDto"}},"nested_workflow_template_id":{"type":"string","format":"uuid","description":"Component-mode discriminator. When present, the endpoint creates a workflow_template_item referencing this workflow template as a nested component; task-mode fields (name/description/…) are ignored. Exactly one of `name` or `nested_workflow_template_id` must be provided."}}},"PublicCreateTaskResponsibilityDto":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"description":"Defaults to 'Responsibility 1' when omitted."},"responsibility_count":{"type":"number","minimum":1,"default":1},"assigment_type":{"default":"Manual","allOf":[{"$ref":"#/components/schemas/AssigmentType"}]},"department_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"position_type_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"user_ids":{"type":"array","items":{"type":"string","format":"uuid"}}}},"AssigmentType":{"type":"string","enum":["Manual","Auto","Self_Assignment"]},"PublicWorkflowItemDto":{"type":"object","properties":{"id":{"type":"string","description":"ID of the workflow_template_item (canvas node)."},"type":{"allOf":[{"$ref":"#/components/schemas/PublicWorkflowItemType"}]},"task_template_id":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"order":{"type":"number","nullable":true},"time_limit":{"type":"number","nullable":true},"basic_reward":{"type":"number","nullable":true},"nested_workflow_template_id":{"type":"string","nullable":true},"position_x":{"type":"number"},"position_y":{"type":"number"},"created_at":{"type":"string","nullable":true}},"required":["id","type","task_template_id","name","description","order","time_limit","basic_reward","nested_workflow_template_id","position_x","position_y","created_at"]},"PublicWorkflowItemType":{"type":"string","enum":["task","component"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows/{workflow_id}/tasks":{"post":{"description":"\nCreates a new canvas node on the addressed workflow. The endpoint runs in two modes, distinguished by the presence of `nested_workflow_template_id` in the body.\n\n### Task mode\n\nSend `name` (+ optional `description`, `order`, `time_limit`, `basic_reward`, `responsibilities`). The server creates a fresh `workflow_task_template`, its `workflow_template_item` canvas node, and either the responsibilities you supply or a default `Responsibility 1` (no relations).\n\n### Component mode\n\nSend only `nested_workflow_template_id` (must reference an existing workflow template in the tenant). The server creates a `workflow_template_item` that references that workflow as a nested component; no new task template is created.\n\nThe new item is positioned at `(position_x = 100, position_y = MAX(existing position_y on this workflow) + 150)` — items never share coordinates, even after prior deletes.\n\n### Errors\n\n- `400` — validation failure, body specifies both modes (`name` + `nested_workflow_template_id`), or specifies neither.\n- `404` — draft, configuration, workflow, `nested_workflow_template_id`, or any `department_id` / `position_type_id` / `user_id` inside a responsibility not found.\n","operationId":"PublicWorkflowTasksController_createWorkflowTask_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workflow_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowTaskCreateDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowItemDto"}}}},"400":{"description":"Both or neither of `name` / `nested_workflow_template_id` supplied, or validation failure."},"404":{"description":"Product draft, configuration, workflow, nested workflow template, or responsibility relation id not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Add a task or component to a workflow","tags":["Products"]}}}}
```

## Update a task item (name / order / basic\_reward)

> \
> Partial-update of a task item. Any subset of \`name\`, \`order\`, \`basic\_reward\` may be supplied. Empty body is a no-op returning the current state.\
> \
> \- \`update\_in\_all\_versions\` — when \`true\`, every supplied field is broadcast to matching tasks (same current name, same workflow position across versions) on every published version's matching workflow. When \`false\` or omitted, only the addressed task row is updated.\
> \
> Only applies to task-mode items. Component items return 400 (components own no fields on the item itself — the referenced workflow is authoritative).\
> \
> \### Errors\
> \
> \- \`400\` — validation failure, or the addressed item is a component.\
> \- \`404\` — draft, configuration, workflow, or task item not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicWorkflowTaskUpdateDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"order":{"type":"number","minimum":0},"basic_reward":{"type":"number","minimum":0},"update_in_all_versions":{"type":"boolean","default":false,"description":"When true, every supplied field (name / order / basic_reward) is broadcast to matching tasks (same current name) on every published version's matching workflow. When false or omitted, only the addressed task row is updated."}}},"PublicWorkflowItemDto":{"type":"object","properties":{"id":{"type":"string","description":"ID of the workflow_template_item (canvas node)."},"type":{"allOf":[{"$ref":"#/components/schemas/PublicWorkflowItemType"}]},"task_template_id":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"description":{"type":"string","nullable":true},"order":{"type":"number","nullable":true},"time_limit":{"type":"number","nullable":true},"basic_reward":{"type":"number","nullable":true},"nested_workflow_template_id":{"type":"string","nullable":true},"position_x":{"type":"number"},"position_y":{"type":"number"},"created_at":{"type":"string","nullable":true}},"required":["id","type","task_template_id","name","description","order","time_limit","basic_reward","nested_workflow_template_id","position_x","position_y","created_at"]},"PublicWorkflowItemType":{"type":"string","enum":["task","component"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows/{workflow_id}/tasks/{task_id}":{"put":{"description":"\nPartial-update of a task item. Any subset of `name`, `order`, `basic_reward` may be supplied. Empty body is a no-op returning the current state.\n\n- `update_in_all_versions` — when `true`, every supplied field is broadcast to matching tasks (same current name, same workflow position across versions) on every published version's matching workflow. When `false` or omitted, only the addressed task row is updated.\n\nOnly applies to task-mode items. Component items return 400 (components own no fields on the item itself — the referenced workflow is authoritative).\n\n### Errors\n\n- `400` — validation failure, or the addressed item is a component.\n- `404` — draft, configuration, workflow, or task item not found.\n","operationId":"PublicWorkflowTasksController_updateWorkflowTask_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workflow_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"task_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowTaskUpdateDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicWorkflowItemDto"}}}},"400":{"description":"Validation failure, or the addressed item is a component."},"404":{"description":"Product draft, configuration, workflow, or task item not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Update a task item (name / order / basic_reward)","tags":["Products"]}}}}
```

## Delete a task or component from a workflow

> \
> Removes the canvas node.\
> \
> \- \*\*Task item\*\* — deletes the \`workflow\_template\_item\`, then cascades the task template (bonuses, files, tags, responsibilities). Blocked (409) when any \`task\` row (production task) references this template — i.e. a production launched from a published version already picked it up.\
> \- \*\*Component item\*\* — deletes only the \`workflow\_template\_item\` row. The referenced nested workflow template is untouched.\
> \
> \### Errors\
> \
> \- \`404\` — draft, configuration, workflow, or item not found.\
> \- \`409\` — task item is referenced by a production task.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/workflows/{workflow_id}/tasks/{task_id}":{"delete":{"description":"\nRemoves the canvas node.\n\n- **Task item** — deletes the `workflow_template_item`, then cascades the task template (bonuses, files, tags, responsibilities). Blocked (409) when any `task` row (production task) references this template — i.e. a production launched from a published version already picked it up.\n- **Component item** — deletes only the `workflow_template_item` row. The referenced nested workflow template is untouched.\n\n### Errors\n\n- `404` — draft, configuration, workflow, or item not found.\n- `409` — task item is referenced by a production task.\n","operationId":"PublicWorkflowTasksController_deleteWorkflowTask_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"workflow_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"task_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""},"404":{"description":"Product draft, configuration, workflow, or task item not found."},"409":{"description":"Task item is referenced by a production task (published version already used it)."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a task or component from a workflow","tags":["Products"]}}}}
```

## Upload a media file to a configuration

> \
> Downloads the image at \`url\` server-side (HTTPS, max 10 MB, \`Content-Type\` must start with \`image\`) and attaches it to the configuration's photo set.\
> \
> \### Body\
> \
> \- \`url\` — required HTTPS URL.\
> \- \`file\_name\` — optional; used when persisting the downloaded file.\
> \- \`is\_primary\` — optional, default \`false\`. When \`true\`, marks this photo as the configuration's default photo.\
> \- \`apply\_to\_all\_versions\` — optional, default \`false\`. When \`true\`, the same file is linked into every published version's matching configuration too. When \`false\` or omitted, only the draft configuration receives the new relation and \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Errors\
> \
> \- \`400\` — missing/invalid \`url\`, wrong content type, or exceeds size limit.\
> \- \`404\` — draft or configuration not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicUploadConfigurationMediaDto":{"type":"object","properties":{"url":{"type":"string","description":"HTTPS URL of the image to download."},"file_name":{"type":"string","description":"Optional filename to store the file as."},"is_primary":{"type":"boolean","default":false,"description":"When true, marks this new photo as the configuration default."},"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, mirror this add across every published version of the product (same file linked into each version's matching configuration). Default false — draft-only."}},"required":["url"]},"PublicConfigurationMediaDto":{"type":"object","properties":{"id":{"type":"string"},"file_id":{"type":"string"},"product_configuration_id":{"type":"string"},"is_show_by_default":{"type":"boolean"}},"required":["id","file_id","product_configuration_id","is_show_by_default"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/media":{"post":{"description":"\nDownloads the image at `url` server-side (HTTPS, max 10 MB, `Content-Type` must start with `image`) and attaches it to the configuration's photo set.\n\n### Body\n\n- `url` — required HTTPS URL.\n- `file_name` — optional; used when persisting the downloaded file.\n- `is_primary` — optional, default `false`. When `true`, marks this photo as the configuration's default photo.\n- `apply_to_all_versions` — optional, default `false`. When `true`, the same file is linked into every published version's matching configuration too. When `false` or omitted, only the draft configuration receives the new relation and `Product.modified_at` on the draft is bumped.\n\n### Errors\n\n- `400` — missing/invalid `url`, wrong content type, or exceeds size limit.\n- `404` — draft or configuration not found.\n","operationId":"PublicConfigurationMediaController_uploadConfigurationMedia_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUploadConfigurationMediaDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicConfigurationMediaDto"}}}},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Upload a media file to a configuration","tags":["Products"]}}}}
```

## Delete a media file from a configuration

> \
> Removes the photo relation from the configuration. Any variant that pointed at the removed relation has its \`configuration\_photo\_relation\_id\` cleared. If the underlying file has no remaining references, it is removed from the bucket.\
> \
> \### Body\
> \
> \- \`apply\_to\_all\_versions\` — optional, default \`false\`. When \`true\`, matching relations (same \`file\_id\`) across every version's configuration are removed too. When \`false\`, only the draft configuration's relation is removed and \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Errors\
> \
> \- \`404\` — draft, configuration, or media relation not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicDeleteConfigurationMediaDto":{"type":"object","properties":{"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, remove every matching relation (same file id) across all versions of the product too. Default false — draft-only."}}}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/media/{media_id}":{"delete":{"description":"\nRemoves the photo relation from the configuration. Any variant that pointed at the removed relation has its `configuration_photo_relation_id` cleared. If the underlying file has no remaining references, it is removed from the bucket.\n\n### Body\n\n- `apply_to_all_versions` — optional, default `false`. When `true`, matching relations (same `file_id`) across every version's configuration are removed too. When `false`, only the draft configuration's relation is removed and `Product.modified_at` on the draft is bumped.\n\n### Errors\n\n- `404` — draft, configuration, or media relation not found.\n","operationId":"PublicConfigurationMediaController_deleteConfigurationMedia_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"media_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicDeleteConfigurationMediaDto"}}}},"responses":{"204":{"description":""},"404":{"description":"Product draft, configuration, or media not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a media file from a configuration","tags":["Products"]}}}}
```

## Set a variant's primary photo (uploads a new image)

> \
> Downloads the image at \`url\` server-side, attaches it to the configuration's photo set, and points the addressed variant at the new relation. Always draft-only — variants do not carry a cross-version link. To broadcast, upload the file via \`POST .../media\` with \`apply\_to\_all\_versions=true\` first.\
> \
> Bumps \`Product.modified\_at\` on the draft.\
> \
> \### Errors\
> \
> \- \`400\` — missing/invalid \`url\`, wrong content type, or exceeds size limit.\
> \- \`404\` — draft, configuration, or variant not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicSetVariantPhotoDto":{"type":"object","properties":{"url":{"type":"string","description":"HTTPS URL of the image to download."},"file_name":{"type":"string","description":"Optional filename to store the file as."}},"required":["url"]},"MessageDto":{"type":"object","properties":{"message":{"type":"string","description":"Message returned from API confirming the operation"}},"required":["message"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/variants/{variant_id}/photo":{"post":{"description":"\nDownloads the image at `url` server-side, attaches it to the configuration's photo set, and points the addressed variant at the new relation. Always draft-only — variants do not carry a cross-version link. To broadcast, upload the file via `POST .../media` with `apply_to_all_versions=true` first.\n\nBumps `Product.modified_at` on the draft.\n\n### Errors\n\n- `400` — missing/invalid `url`, wrong content type, or exceeds size limit.\n- `404` — draft, configuration, or variant not found.\n","operationId":"PublicConfigurationMediaController_setVariantPhoto_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"variant_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSetVariantPhotoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageDto"}}}},"404":{"description":"Product draft, configuration, or variant not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Set a variant's primary photo (uploads a new image)","tags":["Products"]}}}}
```

## Upload a document to a configuration

> \
> Downloads the document at \`url\` server-side (HTTPS, max 25 MB, \`Content-Type\` must be in the shared public-attachment allow-list — pdf, doc/docx, xls/xlsx, plain text, png/jpeg/webp, model/obj, hp-hpgl) and attaches it to the configuration.\
> \
> \### Body\
> \
> \- \`url\` — required HTTPS URL.\
> \- \`file\_name\` — optional; used when persisting the downloaded file.\
> \- \`apply\_to\_all\_versions\` — optional, default \`false\`. When \`true\`, the same file is linked into every published version's matching configuration too. When \`false\` or omitted, only the draft configuration receives the new relation and \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Response\
> \
> Returns the draft's relation row with the stored file's metadata: \`url\` (signed download URL), \`file\_name\`, \`mime\_type\`, \`size\`.\
> \
> \### Errors\
> \
> \- \`400\` — invalid \`url\` or exceeds size limit.\
> \- \`404\` — draft or configuration not found.\
> \- \`409\` — content type not in the accepted document set.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicUploadDocumentDto":{"type":"object","properties":{"url":{"type":"string","description":"HTTPS URL of the document to download."},"file_name":{"type":"string","description":"Optional filename to store the file as."},"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, mirror the upload across every published version of the product (same file linked into each version's matching configuration). Default false — draft-only."}},"required":["url"]},"PublicProductDocumentDto":{"type":"object","properties":{"id":{"type":"string","description":"ID of the attachment relation row."},"product_configuration_id":{"type":"string"},"file_id":{"type":"string"},"url":{"type":"string","description":"Signed URL for downloading the file."},"file_name":{"type":"string"},"mime_type":{"type":"string"},"size":{"type":"number"}},"required":["id","product_configuration_id","file_id","url","file_name","mime_type","size"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/documents":{"post":{"description":"\nDownloads the document at `url` server-side (HTTPS, max 25 MB, `Content-Type` must be in the shared public-attachment allow-list — pdf, doc/docx, xls/xlsx, plain text, png/jpeg/webp, model/obj, hp-hpgl) and attaches it to the configuration.\n\n### Body\n\n- `url` — required HTTPS URL.\n- `file_name` — optional; used when persisting the downloaded file.\n- `apply_to_all_versions` — optional, default `false`. When `true`, the same file is linked into every published version's matching configuration too. When `false` or omitted, only the draft configuration receives the new relation and `Product.modified_at` on the draft is bumped.\n\n### Response\n\nReturns the draft's relation row with the stored file's metadata: `url` (signed download URL), `file_name`, `mime_type`, `size`.\n\n### Errors\n\n- `400` — invalid `url` or exceeds size limit.\n- `404` — draft or configuration not found.\n- `409` — content type not in the accepted document set.\n","operationId":"PublicConfigurationDocumentsController_uploadDocument_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUploadDocumentDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProductDocumentDto"}}}},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Upload a document to a configuration","tags":["Products"]}}}}
```

## Delete a document from a configuration

> \
> Removes the attachment relation. If the underlying file has no remaining references anywhere, it is removed from the bucket too.\
> \
> \### Body\
> \
> \- \`apply\_to\_all\_versions\` — optional, default \`false\`. When \`true\`, every matching relation (same \`file\_id\`) across every version's configuration is removed too. When \`false\` or omitted, only the draft's relation is removed and \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Errors\
> \
> \- \`404\` — draft, configuration, or document not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicDeleteDocumentDto":{"type":"object","properties":{"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, remove every matching relation (same file id) across all versions of the product too. Default false — draft-only."}}}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/documents/{document_id}":{"delete":{"description":"\nRemoves the attachment relation. If the underlying file has no remaining references anywhere, it is removed from the bucket too.\n\n### Body\n\n- `apply_to_all_versions` — optional, default `false`. When `true`, every matching relation (same `file_id`) across every version's configuration is removed too. When `false` or omitted, only the draft's relation is removed and `Product.modified_at` on the draft is bumped.\n\n### Errors\n\n- `404` — draft, configuration, or document not found.\n","operationId":"PublicConfigurationDocumentsController_deleteDocument_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"document_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicDeleteDocumentDto"}}}},"responses":{"204":{"description":""},"404":{"description":"Product draft, configuration, or document not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a document from a configuration","tags":["Products"]}}}}
```

## List links on a configuration

> \
> Returns the links attached to the configuration, ordered by \`created\_at\` ascending. Only the draft configuration's own links are returned.\
> \
> \### Errors\
> \
> \- \`404\` — draft or configuration not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicProductLinkDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"title":{"type":"string","maxLength":255},"url":{"type":"string","maxLength":2048},"created_at":{"type":"string"}},"required":["id","product_configuration_id","title","url","created_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/links":{"get":{"description":"\nReturns the links attached to the configuration, ordered by `created_at` ascending. Only the draft configuration's own links are returned.\n\n### Errors\n\n- `404` — draft or configuration not found.\n","operationId":"PublicConfigurationLinksController_listLinks_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicProductLinkDto"}}}}},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List links on a configuration","tags":["Products"]}}}}
```

## Add a link to a configuration

> \
> Adds a \`{ url, title }\` link row to the configuration.\
> \
> \### Body\
> \
> \- \`url\` — required (1..2048).\
> \- \`title\` — required (1..255).\
> \- \`apply\_to\_all\_versions\` — optional, default \`false\`. When \`true\`, the same \`{ url, title }\` is added to every version's matching configuration too. When \`false\` or omitted, only the draft's configuration receives the link and \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure on the body.\
> \- \`404\` — draft or configuration not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicCreateLinkDto":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":2048},"title":{"type":"string","minLength":1,"maxLength":255},"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, add the same link into every published version's matching configuration too. Default false — draft-only."}},"required":["url","title"]},"PublicProductLinkDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"title":{"type":"string","maxLength":255},"url":{"type":"string","maxLength":2048},"created_at":{"type":"string"}},"required":["id","product_configuration_id","title","url","created_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/links":{"post":{"description":"\nAdds a `{ url, title }` link row to the configuration.\n\n### Body\n\n- `url` — required (1..2048).\n- `title` — required (1..255).\n- `apply_to_all_versions` — optional, default `false`. When `true`, the same `{ url, title }` is added to every version's matching configuration too. When `false` or omitted, only the draft's configuration receives the link and `Product.modified_at` on the draft is bumped.\n\n### Errors\n\n- `400` — validation failure on the body.\n- `404` — draft or configuration not found.\n","operationId":"PublicConfigurationLinksController_createLink_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCreateLinkDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProductLinkDto"}}}},"400":{"description":"Validation failure."},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Add a link to a configuration","tags":["Products"]}}}}
```

## Update a link (title / url)

> \
> Partial-update of a link. Any subset of \`title\`, \`url\` may be supplied. Empty body is a no-op returning the current state.\
> \
> \- \`apply\_to\_all\_versions\` — when \`true\`, the change is broadcast to every sibling link (matched by current URL) on every published version's matching configuration. If a configuration has two links with the same URL, both are updated together. When \`false\` or omitted, only the addressed link row is updated and \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure on the body.\
> \- \`404\` — draft, configuration, or link not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicUpdateLinkDto":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":2048},"title":{"type":"string","minLength":1,"maxLength":255},"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, apply the change to every sibling link (matched by current URL) on every published version too. Default false — draft-only."}}},"PublicProductLinkDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"title":{"type":"string","maxLength":255},"url":{"type":"string","maxLength":2048},"created_at":{"type":"string"}},"required":["id","product_configuration_id","title","url","created_at"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/links/{link_id}":{"put":{"description":"\nPartial-update of a link. Any subset of `title`, `url` may be supplied. Empty body is a no-op returning the current state.\n\n- `apply_to_all_versions` — when `true`, the change is broadcast to every sibling link (matched by current URL) on every published version's matching configuration. If a configuration has two links with the same URL, both are updated together. When `false` or omitted, only the addressed link row is updated and `Product.modified_at` on the draft is bumped.\n\n### Errors\n\n- `400` — validation failure on the body.\n- `404` — draft, configuration, or link not found.\n","operationId":"PublicConfigurationLinksController_updateLink_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"link_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUpdateLinkDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicProductLinkDto"}}}},"400":{"description":"Validation failure."},"404":{"description":"Product draft, configuration, or link not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Update a link (title / url)","tags":["Products"]}}}}
```

## Delete a link

> \
> Removes the link row.\
> \
> \### Body\
> \
> \- \`apply\_to\_all\_versions\` — optional, default \`false\`. When \`true\`, every sibling link (matched by URL) on every published version's matching configuration is removed too. When \`false\` or omitted, only the draft's link is removed and \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Errors\
> \
> \- \`404\` — draft, configuration, or link not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicDeleteLinkDto":{"type":"object","properties":{"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, remove every sibling link (matched by URL) on every published version too. Default false — draft-only."}}}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/links/{link_id}":{"delete":{"description":"\nRemoves the link row.\n\n### Body\n\n- `apply_to_all_versions` — optional, default `false`. When `true`, every sibling link (matched by URL) on every published version's matching configuration is removed too. When `false` or omitted, only the draft's link is removed and `Product.modified_at` on the draft is bumped.\n\n### Errors\n\n- `404` — draft, configuration, or link not found.\n","operationId":"PublicConfigurationLinksController_deleteLink_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"link_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicDeleteLinkDto"}}}},"responses":{"204":{"description":""},"404":{"description":"Product draft, configuration, or link not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a link","tags":["Products"]}}}}
```

## List parameters on a configuration

> \
> Returns the parameters (internally: product options) attached to the draft configuration, sorted by \`order\` ascending. Each parameter carries its ordered list of values. Published versions of the product are not exposed here — parameters are per-version and each version's option/value/variant tree is independent.\
> \
> \### Errors\
> \
> \- \`404\` — draft or configuration not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicParameterDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number"},"values":{"type":"array","items":{"$ref":"#/components/schemas/PublicParameterValueDto"}}},"required":["id","product_configuration_id","name","order","values"]},"PublicParameterValueDto":{"type":"object","properties":{"id":{"type":"string"},"value":{"type":"string"},"order":{"type":"number"}},"required":["id","value","order"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/parameters":{"get":{"description":"\nReturns the parameters (internally: product options) attached to the draft configuration, sorted by `order` ascending. Each parameter carries its ordered list of values. Published versions of the product are not exposed here — parameters are per-version and each version's option/value/variant tree is independent.\n\n### Errors\n\n- `404` — draft or configuration not found.\n","operationId":"PublicProductParametersController_listParameters_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicParameterDto"}}}}},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List parameters on a configuration","tags":["Products"]}}}}
```

## Add a parameter to a configuration

> \
> Creates a new parameter under the draft configuration and expands the variant matrix (cartesian product of every parameter's values). The new parameter is appended (\`order\` = current parameter count). \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Body\
> \
> \- \`name\` — required (1..255).\
> \- \`values\` — required array of unique non-empty value strings (each 1..255). At least one entry.\
> \
> \### Cascade\
> \
> \- \*\*First parameter on the config\*\* — replaces the existing default variant with N variants (one per value). Each variant inherits the configuration's SKU and is active.\
> \- \*\*Additional parameter\*\* — every existing variant gets the new parameter's first value attached and is renamed; new variants are created for each remaining combination.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure (empty name, empty/duplicate values, missing \`values\`).\
> \- \`404\` — draft or configuration not found.\
> \- \`409\` — configuration already has the maximum of 3 parameters.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicCreateParameterDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"values":{"uniqueItems":true,"minItems":1,"description":"At least one value; each value is a non-empty string, unique within this parameter.","type":"array","items":{"type":"string","maxLength":255,"minLength":1}}},"required":["name","values"]},"PublicParameterDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number"},"values":{"type":"array","items":{"$ref":"#/components/schemas/PublicParameterValueDto"}}},"required":["id","product_configuration_id","name","order","values"]},"PublicParameterValueDto":{"type":"object","properties":{"id":{"type":"string"},"value":{"type":"string"},"order":{"type":"number"}},"required":["id","value","order"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/parameters":{"post":{"description":"\nCreates a new parameter under the draft configuration and expands the variant matrix (cartesian product of every parameter's values). The new parameter is appended (`order` = current parameter count). `Product.modified_at` on the draft is bumped.\n\n### Body\n\n- `name` — required (1..255).\n- `values` — required array of unique non-empty value strings (each 1..255). At least one entry.\n\n### Cascade\n\n- **First parameter on the config** — replaces the existing default variant with N variants (one per value). Each variant inherits the configuration's SKU and is active.\n- **Additional parameter** — every existing variant gets the new parameter's first value attached and is renamed; new variants are created for each remaining combination.\n\n### Errors\n\n- `400` — validation failure (empty name, empty/duplicate values, missing `values`).\n- `404` — draft or configuration not found.\n- `409` — configuration already has the maximum of 3 parameters.\n","operationId":"PublicProductParametersController_createParameter_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicCreateParameterDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicParameterDto"}}}},"400":{"description":"Validation failure."},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Add a parameter to a configuration","tags":["Products"]}}}}
```

## Update a parameter (rename / edit value set)

> \
> Partial-update of a parameter. Any subset of \`name\`, \`values\` may be supplied.\
> \
> \- \`name\` — rename only; leaves values / variants untouched.\
> \- \`values\` — \*\*full-replace by string match\*\*:\
> &#x20; \- Existing values whose \`value\` appears in the incoming array are kept (variants and \`product\_parameter\` wiring preserved).\
> &#x20; \- Missing existing values are deleted (their variants + parameters are removed).\
> &#x20; \- New strings become new values that expand the variant matrix.\
> &#x20; \- Array order becomes the new value \`order\`.\
> \
> Renaming a value string is not supported — sending \`values: \["Bilyi", "Kraft"]\` when the current set is \`\["Білий", "Крафт"]\` deletes both existing rows (and their variants) and creates two new ones. Use the same string to keep the row.\
> \
> \`Product.modified\_at\` on the draft is bumped. Empty body is a no-op returning the current state.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure (empty \`values\`, duplicates within \`values\`, empty name).\
> \- \`404\` — draft, configuration, or parameter not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicUpdateParameterDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"values":{"uniqueItems":true,"minItems":1,"description":"Full-replace of the value set. Existing values matched by string are kept (variants preserved); missing values are deleted (their variants + parameters are removed); new strings become new values that expand the variant matrix.","type":"array","items":{"type":"string","maxLength":255,"minLength":1}}}},"PublicParameterDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number"},"values":{"type":"array","items":{"$ref":"#/components/schemas/PublicParameterValueDto"}}},"required":["id","product_configuration_id","name","order","values"]},"PublicParameterValueDto":{"type":"object","properties":{"id":{"type":"string"},"value":{"type":"string"},"order":{"type":"number"}},"required":["id","value","order"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/parameters/{parameter_id}":{"put":{"description":"\nPartial-update of a parameter. Any subset of `name`, `values` may be supplied.\n\n- `name` — rename only; leaves values / variants untouched.\n- `values` — **full-replace by string match**:\n  - Existing values whose `value` appears in the incoming array are kept (variants and `product_parameter` wiring preserved).\n  - Missing existing values are deleted (their variants + parameters are removed).\n  - New strings become new values that expand the variant matrix.\n  - Array order becomes the new value `order`.\n\nRenaming a value string is not supported — sending `values: [\"Bilyi\", \"Kraft\"]` when the current set is `[\"Білий\", \"Крафт\"]` deletes both existing rows (and their variants) and creates two new ones. Use the same string to keep the row.\n\n`Product.modified_at` on the draft is bumped. Empty body is a no-op returning the current state.\n\n### Errors\n\n- `400` — validation failure (empty `values`, duplicates within `values`, empty name).\n- `404` — draft, configuration, or parameter not found.\n","operationId":"PublicProductParametersController_updateParameter_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"parameter_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUpdateParameterDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicParameterDto"}}}},"400":{"description":"Validation failure."},"404":{"description":"Product draft, configuration, or parameter not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Update a parameter (rename / edit value set)","tags":["Products"]}}}}
```

## Delete a parameter

> \
> Removes the parameter, its values, and every variant that referenced it.\
> \
> \- \*\*Non-last parameter\*\* — variants collapse to the remaining parameters' combinations; variant names are recomputed.\
> \- \*\*Last parameter on the config\*\* — the configuration is left with a single default variant named \`Default\` (SKU inherited from the configuration).\
> \
> \`Product.modified\_at\` on the draft is bumped.\
> \
> \### Errors\
> \
> \- \`404\` — draft, configuration, or parameter not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/parameters/{parameter_id}":{"delete":{"description":"\nRemoves the parameter, its values, and every variant that referenced it.\n\n- **Non-last parameter** — variants collapse to the remaining parameters' combinations; variant names are recomputed.\n- **Last parameter on the config** — the configuration is left with a single default variant named `Default` (SKU inherited from the configuration).\n\n`Product.modified_at` on the draft is bumped.\n\n### Errors\n\n- `404` — draft, configuration, or parameter not found.\n","operationId":"PublicProductParametersController_deleteParameter_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"parameter_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""},"404":{"description":"Product draft, configuration, or parameter not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a parameter","tags":["Products"]}}}}
```

## List variants on a configuration

> \
> Returns every non-deleted variant of the draft configuration together with its parameter breakdown — each entry carries a stable list of \`{ option\_name, value }\` pairs sorted by option name. Callers use this list to identify which variant is which when batch-setting barcodes.\
> \
> Configurations with no parameters return a single \`Default\` variant with \`has\_parameters=false\` and an empty \`parameters\` array.\
> \
> \### Errors\
> \
> \- \`404\` — draft or configuration not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicVariantDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"name":{"type":"string"},"sku":{"type":"string"},"barcode":{"type":"string"},"is_active":{"type":"boolean"},"has_parameters":{"type":"boolean"},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/PublicVariantParameterDto"}}},"required":["id","product_configuration_id","name","sku","barcode","is_active","has_parameters","parameters"]},"PublicVariantParameterDto":{"type":"object","properties":{"option_name":{"type":"string"},"value":{"type":"string"}},"required":["option_name","value"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/variants":{"get":{"description":"\nReturns every non-deleted variant of the draft configuration together with its parameter breakdown — each entry carries a stable list of `{ option_name, value }` pairs sorted by option name. Callers use this list to identify which variant is which when batch-setting barcodes.\n\nConfigurations with no parameters return a single `Default` variant with `has_parameters=false` and an empty `parameters` array.\n\n### Errors\n\n- `404` — draft or configuration not found.\n","operationId":"PublicProductVariantsController_listVariants_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicVariantDto"}}}}},"404":{"description":"Product draft or configuration not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List variants on a configuration","tags":["Products"]}}}}
```

## Batch-set variant barcodes

> \
> Applies one barcode per variant in a single request. Payload cap: \*\*500 items\*\* per call. An empty barcode string clears the field.\
> \
> \### Body\
> \
> \- \`items\` — array of \`{ variant\_id, barcode }\`. Every \`variant\_id\` must belong to the addressed configuration; duplicate ids in one request are rejected.\
> \- \`apply\_to\_all\_versions\` — optional, default \`false\`. When \`true\`, the same barcode also lands on the sibling variant on every published version of this product. \*\*Cross-version identity is matched by parameter signature\*\* — the sorted set of \`(option\_name, value)\` pairs the variant carries. Sibling versions whose configuration doesn't carry a matching variant (e.g. because the parameter set was restructured after publish) are skipped silently. Draft-only writes bump \`Product.modified\_at\`; broadcast writes don't.\
> \
> \### Barcode uniqueness\
> \
> Barcodes are enforced unique per tenant with one exception: the same barcode may live on multiple variants that are \*\*all siblings across versions of the same product\*\* (same \`product\_meta\_id\` + same parameter signature). Attempts to reuse a barcode elsewhere return \`409\`.\
> \
> \### Errors\
> \
> \- \`400\` — empty \`items\`, duplicate \`variant\_id\`, or the same barcode assigned to variants with different parameter combinations within one request.\
> \- \`404\` — draft, configuration, or any addressed variant not found on the config.\
> \- \`409\` — a barcode is already used by a non-sibling variant elsewhere in the tenant.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicSetVariantBarcodesDto":{"type":"object","properties":{"items":{"minItems":1,"maxItems":500,"type":"array","items":{"$ref":"#/components/schemas/PublicSetVariantBarcodeItemDto"}},"apply_to_all_versions":{"type":"boolean","default":false,"description":"When true, apply each barcode to the sibling variant on every published version of the product too (matched by parameter signature). Sibling versions whose config doesn't carry a matching variant are skipped. Draft-only writes bump `Product.modified_at`; broadcast writes don't."}},"required":["items"]},"PublicSetVariantBarcodeItemDto":{"type":"object","properties":{"variant_id":{"type":"string","format":"uuid"},"barcode":{"type":"string","maxLength":255,"description":"Empty string clears the barcode."}},"required":["variant_id","barcode"]},"PublicVariantDto":{"type":"object","properties":{"id":{"type":"string"},"product_configuration_id":{"type":"string"},"name":{"type":"string"},"sku":{"type":"string"},"barcode":{"type":"string"},"is_active":{"type":"boolean"},"has_parameters":{"type":"boolean"},"parameters":{"type":"array","items":{"$ref":"#/components/schemas/PublicVariantParameterDto"}}},"required":["id","product_configuration_id","name","sku","barcode","is_active","has_parameters","parameters"]},"PublicVariantParameterDto":{"type":"object","properties":{"option_name":{"type":"string"},"value":{"type":"string"}},"required":["option_name","value"]}}},"paths":{"/api/v1/public/products/{id}/configurations/{config_id}/variants":{"patch":{"description":"\nApplies one barcode per variant in a single request. Payload cap: **500 items** per call. An empty barcode string clears the field.\n\n### Body\n\n- `items` — array of `{ variant_id, barcode }`. Every `variant_id` must belong to the addressed configuration; duplicate ids in one request are rejected.\n- `apply_to_all_versions` — optional, default `false`. When `true`, the same barcode also lands on the sibling variant on every published version of this product. **Cross-version identity is matched by parameter signature** — the sorted set of `(option_name, value)` pairs the variant carries. Sibling versions whose configuration doesn't carry a matching variant (e.g. because the parameter set was restructured after publish) are skipped silently. Draft-only writes bump `Product.modified_at`; broadcast writes don't.\n\n### Barcode uniqueness\n\nBarcodes are enforced unique per tenant with one exception: the same barcode may live on multiple variants that are **all siblings across versions of the same product** (same `product_meta_id` + same parameter signature). Attempts to reuse a barcode elsewhere return `409`.\n\n### Errors\n\n- `400` — empty `items`, duplicate `variant_id`, or the same barcode assigned to variants with different parameter combinations within one request.\n- `404` — draft, configuration, or any addressed variant not found on the config.\n- `409` — a barcode is already used by a non-sibling variant elsewhere in the tenant.\n","operationId":"PublicProductVariantsController_setVariantBarcodes_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"config_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicSetVariantBarcodesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicVariantDto"}}}}},"400":{"description":"Empty items, duplicate variant_id, or duplicate barcode across non-sibling variants."},"404":{"description":"Product draft, configuration, or any addressed variant not found."},"409":{"description":"A barcode is already used by a non-sibling variant elsewhere in the tenant."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Batch-set variant barcodes","tags":["Products"]}}}}
```

## List BOMs for a product

> \
> Returns every non-empty BOM attached to the product (one per \`(product\_variant, workflow\_template)\` pair). Each entry carries its ordered items with the joined \`material\_name\` + \`unit\` (from the material's measurement unit). Optional filters: \`variant\_id\`, \`workflow\_id\`.\
> \
> BOMs are lazily created — a BOM row does not appear here until the first \`POST /bom/items\` call for that pair.\
> \
> \### Errors\
> \
> \- \`404\` — product not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicBomDto":{"type":"object","properties":{"id":{"type":"string"},"product_variant_id":{"type":"string"},"workflow_template_id":{"type":"string"},"is_published":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/PublicBomItemDto"}}},"required":["id","product_variant_id","workflow_template_id","is_published","items"]},"PublicBomItemDto":{"type":"object","properties":{"id":{"type":"string"},"bom_id":{"type":"string"},"material_id":{"type":"string","nullable":true},"component_bom_id":{"type":"string","nullable":true},"material_name":{"type":"string","nullable":true,"description":"Material display name; null when the item is a nested component."},"unit":{"type":"string","nullable":true,"description":"Unit of measure name from the material's measurement_unit; null for component items."},"quantity":{"type":"number"}},"required":["id","bom_id","material_id","component_bom_id","material_name","unit","quantity"]}}},"paths":{"/api/v1/public/products/{id}/bom":{"get":{"description":"\nReturns every non-empty BOM attached to the product (one per `(product_variant, workflow_template)` pair). Each entry carries its ordered items with the joined `material_name` + `unit` (from the material's measurement unit). Optional filters: `variant_id`, `workflow_id`.\n\nBOMs are lazily created — a BOM row does not appear here until the first `POST /bom/items` call for that pair.\n\n### Errors\n\n- `404` — product not found.\n","operationId":"PublicProductBomController_listBom_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"variant_id","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"workflow_id","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicBomDto"}}}}},"404":{"description":"Product not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List BOMs for a product","tags":["Products"]}}}}
```

## List BOM versions (placeholder)

> \
> Returns a single-entry array while native BOM versioning is deferred. The entry reports the current BOM count for the product. This endpoint is stable so future versioning support can extend it without breaking the client contract.\
> \
> \### Errors\
> \
> \- \`404\` — product not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicBomVersionDto":{"type":"object","properties":{"version":{"type":"number","description":"Placeholder version number; always 1 while versioning is deferred."},"is_current":{"type":"boolean"},"bom_count":{"type":"number"},"created_at":{"type":"string","nullable":true}},"required":["version","is_current","bom_count","created_at"]}}},"paths":{"/api/v1/public/products/{id}/bom/versions":{"get":{"description":"\nReturns a single-entry array while native BOM versioning is deferred. The entry reports the current BOM count for the product. This endpoint is stable so future versioning support can extend it without breaking the client contract.\n\n### Errors\n\n- `404` — product not found.\n","operationId":"PublicProductBomController_listBomVersions_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicBomVersionDto"}}}}},"404":{"description":"Product not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"List BOM versions (placeholder)","tags":["Products"]}}}}
```

## Add items to a BOM

> \
> Adds one or more items to the BOM identified by \`(variant\_id, workflow\_template\_id)\`. The BOM is auto-created if this is the first write for that pair.\
> \
> \### Body\
> \
> \- \`variant\_id\` — required UUID. Must belong to the addressed product.\
> \- \`workflow\_template\_id\` — required UUID. Must live on a configuration of the addressed product.\
> \- \`items\` — non-empty array (up to 500). Each item sets exactly one of \`material\_id\` or \`component\_bom\_id\`, plus a positive \`quantity\` (min 0.00001).\
> \- \`create\_new\_version\` — optional. Currently a \*\*no-op\*\* while versioning is deferred; when set to \`true\` the server logs a warning and processes the request in place. Accepted so future callers don't have to change the contract.\
> \
> Bumps \`Product.modified\_at\` on the addressed product.\
> \
> \### Errors\
> \
> \- \`400\` — empty \`items\`, or an item with both / neither of \`material\_id\` / \`component\_bom\_id\`.\
> \- \`404\` — product / variant / workflow\_template not found on this product.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicAddBomItemsDto":{"type":"object","properties":{"variant_id":{"type":"string","format":"uuid"},"workflow_template_id":{"type":"string","format":"uuid"},"items":{"minItems":1,"maxItems":500,"type":"array","items":{"$ref":"#/components/schemas/PublicAddBomItemDto"}},"create_new_version":{"type":"boolean","default":false,"description":"Versioning is not implemented yet. Accepted for forward compatibility; when true the server logs a warning and processes the request in place."}},"required":["variant_id","workflow_template_id","items"]},"PublicAddBomItemDto":{"type":"object","properties":{"material_id":{"type":"string","format":"uuid","description":"Material FK. Exactly one of `material_id` / `component_bom_id` must be set."},"component_bom_id":{"type":"string","format":"uuid","description":"Nested-BOM FK. Exactly one of `material_id` / `component_bom_id` must be set."},"quantity":{"type":"number","minimum":0.00001}},"required":["quantity"]},"PublicBomDto":{"type":"object","properties":{"id":{"type":"string"},"product_variant_id":{"type":"string"},"workflow_template_id":{"type":"string"},"is_published":{"type":"boolean"},"items":{"type":"array","items":{"$ref":"#/components/schemas/PublicBomItemDto"}}},"required":["id","product_variant_id","workflow_template_id","is_published","items"]},"PublicBomItemDto":{"type":"object","properties":{"id":{"type":"string"},"bom_id":{"type":"string"},"material_id":{"type":"string","nullable":true},"component_bom_id":{"type":"string","nullable":true},"material_name":{"type":"string","nullable":true,"description":"Material display name; null when the item is a nested component."},"unit":{"type":"string","nullable":true,"description":"Unit of measure name from the material's measurement_unit; null for component items."},"quantity":{"type":"number"}},"required":["id","bom_id","material_id","component_bom_id","material_name","unit","quantity"]}}},"paths":{"/api/v1/public/products/{id}/bom/items":{"post":{"description":"\nAdds one or more items to the BOM identified by `(variant_id, workflow_template_id)`. The BOM is auto-created if this is the first write for that pair.\n\n### Body\n\n- `variant_id` — required UUID. Must belong to the addressed product.\n- `workflow_template_id` — required UUID. Must live on a configuration of the addressed product.\n- `items` — non-empty array (up to 500). Each item sets exactly one of `material_id` or `component_bom_id`, plus a positive `quantity` (min 0.00001).\n- `create_new_version` — optional. Currently a **no-op** while versioning is deferred; when set to `true` the server logs a warning and processes the request in place. Accepted so future callers don't have to change the contract.\n\nBumps `Product.modified_at` on the addressed product.\n\n### Errors\n\n- `400` — empty `items`, or an item with both / neither of `material_id` / `component_bom_id`.\n- `404` — product / variant / workflow_template not found on this product.\n","operationId":"PublicProductBomController_addBomItems_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicAddBomItemsDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBomDto"}}}},"400":{"description":"Validation failure, or item with both/neither of `material_id` / `component_bom_id`."},"404":{"description":"Product, variant, or workflow template not found on this product."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Add items to a BOM","tags":["Products"]}}}}
```

## Update a BOM item quantity

> \
> Updates the item's \`quantity\`. Other item fields are immutable in the current schema (item is either \`material\_id\` or \`component\_bom\_id\`; those are set at create time).\
> \
> \- \`create\_new\_version\` — no-op, same as POST.\
> \
> Bumps \`Product.modified\_at\`.\
> \
> \### Errors\
> \
> \- \`400\` — validation failure.\
> \- \`404\` — product or item not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"PublicUpdateBomItemDto":{"type":"object","properties":{"quantity":{"type":"number","minimum":0.00001},"create_new_version":{"type":"boolean","default":false,"description":"No-op while versioning is deferred (same as POST)."}},"required":["quantity"]},"PublicBomItemDto":{"type":"object","properties":{"id":{"type":"string"},"bom_id":{"type":"string"},"material_id":{"type":"string","nullable":true},"component_bom_id":{"type":"string","nullable":true},"material_name":{"type":"string","nullable":true,"description":"Material display name; null when the item is a nested component."},"unit":{"type":"string","nullable":true,"description":"Unit of measure name from the material's measurement_unit; null for component items."},"quantity":{"type":"number"}},"required":["id","bom_id","material_id","component_bom_id","material_name","unit","quantity"]}}},"paths":{"/api/v1/public/products/{id}/bom/items/{item_id}":{"put":{"description":"\nUpdates the item's `quantity`. Other item fields are immutable in the current schema (item is either `material_id` or `component_bom_id`; those are set at create time).\n\n- `create_new_version` — no-op, same as POST.\n\nBumps `Product.modified_at`.\n\n### Errors\n\n- `400` — validation failure.\n- `404` — product or item not found.\n","operationId":"PublicProductBomController_updateBomItem_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"item_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUpdateBomItemDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBomItemDto"}}}},"400":{"description":"Validation failure."},"404":{"description":"Product or item not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Update a BOM item quantity","tags":["Products"]}}}}
```

## Delete a BOM item

> \
> Removes the item and cascades the BOM closure (drops the parent → component path when the item is a component). Marks the parent BOM's cost cache dirty. Bumps \`Product.modified\_at\`.\
> \
> Note: the parent BOM row remains after its last item is deleted (empty BOM). There's no public endpoint to delete the BOM row itself.\
> \
> \### Errors\
> \
> \- \`404\` — product or item not found.<br>

```json
{"openapi":"3.0.0","info":{"title":"Public API","version":"1.0"},"tags":[{"name":"Products"}],"servers":[{"url":"https://api-stage.hesh.tech"}],"security":[{"PublicApiKey":[]}],"components":{"securitySchemes":{"PublicApiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"paths":{"/api/v1/public/products/{id}/bom/items/{item_id}":{"delete":{"description":"\nRemoves the item and cascades the BOM closure (drops the parent → component path when the item is a component). Marks the parent BOM's cost cache dirty. Bumps `Product.modified_at`.\n\nNote: the parent BOM row remains after its last item is deleted (empty BOM). There's no public endpoint to delete the BOM row itself.\n\n### Errors\n\n- `404` — product or item not found.\n","operationId":"PublicProductBomController_deleteBomItem_v1","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"item_id","required":true,"in":"path","schema":{"type":"string"}},{"name":"x-tenant-id","in":"header","description":"Tenant id (uuid v4)","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":""},"404":{"description":"Product or item not found."},"429":{"description":"Returned when the rate limit is exceeded","headers":{"X-RateLimit-Limit":{"description":"Maximum number of allowed requests during the current window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Remaining number of requests before throttling occurs","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Number of seconds until the rate limit window resets","schema":{"type":"integer"}}}}},"summary":"Delete a BOM item","tags":["Products"]}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.hesh.app/manuals/public-api/products.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
