{"openapi":"3.0.3","info":{"title":"MiniPim","description":"Headless Product Information Management — public API (v1). See [docs/DEVELOPERS.md](https://github.com/Epic-Design-Labs/minipim/blob/main/docs/DEVELOPERS.md) for the narrative integration guide (auth, pagination, locale/channel resolution, webhooks).","version":"0.31.2"},"components":{"securitySchemes":{"organizationHeader":{"type":"apiKey","name":"x-organization-id","in":"header","description":"Tenant context. Phase 0 only — replaced by JWT/API key when auth lands."}},"schemas":{}},"paths":{"/healthz":{"get":{"summary":"Liveness probe","tags":["health"],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"service":{"type":"string"},"version":{"type":"string"}},"required":["status","service","version"],"additionalProperties":false}}}}}}},"/readyz":{"get":{"summary":"Readiness probe (DB connectivity)","tags":["health"],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"}},"required":["status"],"additionalProperties":false}}}}}}},"/v1/changelog":{"get":{"summary":"Release changelog","tags":["health"],"description":"Structured release notes, newest first. The `version` of the first entry is the deployed version (also reported by /healthz).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string"},"entries":{"type":"array","items":{"type":"object","properties":{"version":{"type":"string"},"date":{"type":"string"},"title":{"type":"string"},"highlights":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["feature","fix","perf","docs"]},"text":{"type":"string"}},"required":["type","text"],"additionalProperties":false}}},"required":["version","date","title","highlights"],"additionalProperties":false}}},"required":["version","entries"],"additionalProperties":false}}}}}}},"/v1/organizations":{"post":{"summary":"Create an organization (tenant)","tags":["organizations"],"description":"Creates a new tenant. Two callers:\n\n- **Self-serve** (a logged-in user already in some org): the caller becomes the new org's admin.\n- **Service provisioning** (`x-pim-service-key` header): the org is created *on behalf of* the `initialAdmin` (email or userId) — an existing user is seated as admin immediately; a not-yet-registered email gets a pending admin invite that auto-claims on sign-in. This is how the hosted control plane creates client orgs, and how OSS operators script tenant provisioning.\n\nEmits `organization.created` on both paths. Blocked entirely in `single` tenant mode — the deployment ships one bootstrap org and that's the entire surface.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$"},"settings":{"type":"object","properties":{"default_locale":{"type":"string"},"default_channel":{"type":"string"},"default_currency":{"type":"string"},"content_pages_enabled":{"type":"boolean"}},"additionalProperties":false},"initialAdmin":{"type":"object","properties":{"email":{"type":"string","format":"email"},"userId":{"type":"string","format":"uuid"}},"additionalProperties":false}},"required":["name","slug"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"settings":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","slug","settings","createdAt","updatedAt"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/organizations/me":{"get":{"summary":"Get the current tenant organization","tags":["organizations"],"description":"Includes `features`, the resolved state of optional features for this org. `entitled` is the plan-level answer; `enabled` is the org’s own preference. A feature is usable only when both are true.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"settings":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"features":{"type":"object","properties":{"content_pages":{"type":"object","properties":{"entitled":{"type":"boolean"},"enabled":{"type":"boolean"}},"required":["entitled","enabled"],"additionalProperties":false}},"required":["content_pages"],"additionalProperties":false},"billing":{"type":"object","properties":{"plan":{"type":"string","description":"Deployment-defined plan identifier, e.g. `pro_monthly`."},"status":{"type":"string","enum":["trialing","active","past_due","paused","cancelled"],"description":"Lifecycle state. `past_due` still grants access — entitlements are the authority on what an org may do, and dunning is the control plane's job."},"interval":{"type":"string","enum":["weekly","monthly","quarterly","yearly"],"nullable":true,"description":"Billing interval. Absent on plans that do not recur, e.g. a free tier."},"currentPeriodEnd":{"type":"string","nullable":true,"description":"ISO 8601 end of the current billing period; when the next charge falls."},"cancelAtPeriodEnd":{"type":"boolean","nullable":true,"description":"True when the subscription is set to lapse at `currentPeriodEnd` rather than renew. Access continues until then, so this is a banner, not a lockout."},"trialEnd":{"type":"string","nullable":true,"description":"ISO 8601 end of the trial, when one is running."}},"required":["plan","status"],"additionalProperties":false,"nullable":true,"description":"Subscription state for this organization, or `null` when the deployment does not bill — which is every self-hosted install. Read-only: written by the billing control plane, never by a client. Treat `null` as \"this deployment has no billing\" rather than \"no plan\", and hide billing UI entirely rather than rendering an empty state."}},"required":["id","name","slug","settings","createdAt","updatedAt","features","billing"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update the current tenant organization","tags":["organizations"],"description":"Emits `organization.updated`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$"},"settings":{"type":"object","properties":{"default_locale":{"type":"string"},"default_channel":{"type":"string"},"default_currency":{"type":"string"},"content_pages_enabled":{"type":"boolean"}},"additionalProperties":false}},"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"settings":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","name","slug","settings","createdAt","updatedAt"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/organizations/{id}":{"delete":{"summary":"Delete an organization (tenant) — irreversible","tags":["organizations"],"description":"Exports the tenant to storage, emits `organization.deleted`, then hard-cascade-deletes it. The caller must be an `admin` of the target org (the one they are currently in), and `confirm` must equal the org slug. Never calls out to connected platforms: the cascade is DB-local and emits no per-entity events, so no destructive webhooks are sent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"confirm":{"type":"string","minLength":1}},"required":["confirm"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"exportKey":{"type":"string"},"deletedCounts":{"type":"object","additionalProperties":{"type":"number"}}},"required":["exportKey","deletedCounts"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attribute-groups":{"get":{"summary":"List attribute groups","tags":["attributes"],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"position":{"type":"number"}},"required":["id","organizationId","createdAt","updatedAt","code","label","position"],"additionalProperties":false}}}}}}},"post":{"summary":"Create an attribute group","tags":["attributes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"label":{"type":"object","additionalProperties":{"type":"string"}},"position":{"type":"integer","default":0}},"required":["code","label"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"position":{"type":"number"}},"required":["id","organizationId","createdAt","updatedAt","code","label","position"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attributes":{"get":{"summary":"List attributes","tags":["attributes"],"description":"Defaults to product attributes. Pass `entityKind=content` for content-page fields.","parameters":[{"schema":{"type":"string","enum":["product","content","category","brand"],"default":"product"},"in":"query","name":"entityKind","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","richtext","number","decimal","boolean","date","datetime","select","multiselect","reference","media","measurement","url","email","json","money","tags","file"]},"groupId":{"type":"string","format":"uuid","nullable":true},"isLocalizable":{"type":"boolean"},"isScopable":{"type":"boolean"},"requirement":{"type":"string","enum":["required","suggested","available"]},"isRequired":{"type":"boolean"},"isVariantAxis":{"type":"boolean"},"validation":{"type":"object","additionalProperties":{},"nullable":true},"defaultValue":{"nullable":true},"position":{"type":"number"},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","type","groupId","isLocalizable","isScopable","requirement","isRequired","isVariantAxis","validation","position","createdBy","updatedBy"],"additionalProperties":false}}}}}}},"post":{"summary":"Create an attribute","tags":["attributes"],"description":"Defines a typed property. Adding an attribute does not require a migration — the validator layer compiles a Zod schema from the family + definitions at runtime.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"entityKind":{"type":"string","enum":["product","content","category","brand"],"default":"product"},"label":{"type":"object","additionalProperties":{"type":"string"}},"type":{"type":"string","enum":["text","textarea","richtext","number","decimal","boolean","date","datetime","select","multiselect","reference","media","measurement","url","email","json","money","tags","file"]},"groupId":{"type":"string","format":"uuid","nullable":true},"isLocalizable":{"type":"boolean","default":false},"isScopable":{"type":"boolean","default":false},"requirement":{"type":"string","enum":["required","suggested","available"],"description":"`required` must be filled and counts toward completeness. `suggested` is always shown on edit surfaces to encourage filling it in, but is not counted. `available` (the default) is on the family but hidden from edit surfaces until a record carries a value or someone adds it — the lever that keeps a fifty-attribute family from putting fifty fields on every product page."},"isRequired":{"type":"boolean","description":"Deprecated: send `requirement` instead. `true` = required, `false` = available."},"isVariantAxis":{"type":"boolean","default":false},"validation":{"type":"object","additionalProperties":{},"nullable":true},"defaultValue":{"nullable":true},"position":{"type":"integer","default":0}},"required":["code","label","type"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","richtext","number","decimal","boolean","date","datetime","select","multiselect","reference","media","measurement","url","email","json","money","tags","file"]},"groupId":{"type":"string","format":"uuid","nullable":true},"isLocalizable":{"type":"boolean"},"isScopable":{"type":"boolean"},"requirement":{"type":"string","enum":["required","suggested","available"]},"isRequired":{"type":"boolean"},"isVariantAxis":{"type":"boolean"},"validation":{"type":"object","additionalProperties":{},"nullable":true},"defaultValue":{"nullable":true},"position":{"type":"number"},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","type","groupId","isLocalizable","isScopable","requirement","isRequired","isVariantAxis","validation","position","createdBy","updatedBy"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attributes/{id}":{"get":{"summary":"Get an attribute","tags":["attributes"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","richtext","number","decimal","boolean","date","datetime","select","multiselect","reference","media","measurement","url","email","json","money","tags","file"]},"groupId":{"type":"string","format":"uuid","nullable":true},"isLocalizable":{"type":"boolean"},"isScopable":{"type":"boolean"},"requirement":{"type":"string","enum":["required","suggested","available"]},"isRequired":{"type":"boolean"},"isVariantAxis":{"type":"boolean"},"validation":{"type":"object","additionalProperties":{},"nullable":true},"defaultValue":{"nullable":true},"position":{"type":"number"},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","type","groupId","isLocalizable","isScopable","requirement","isRequired","isVariantAxis","validation","position","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update an attribute","tags":["attributes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"label":{"type":"object","additionalProperties":{"type":"string"}},"type":{"type":"string","enum":["text","textarea","richtext","number","decimal","boolean","date","datetime","select","multiselect","reference","media","measurement","url","email","json","money","tags","file"]},"groupId":{"type":"string","format":"uuid","nullable":true},"isLocalizable":{"type":"boolean","default":false},"isScopable":{"type":"boolean","default":false},"requirement":{"type":"string","enum":["required","suggested","available"],"description":"`required` must be filled and counts toward completeness. `suggested` is always shown on edit surfaces to encourage filling it in, but is not counted. `available` (the default) is on the family but hidden from edit surfaces until a record carries a value or someone adds it — the lever that keeps a fifty-attribute family from putting fifty fields on every product page."},"isRequired":{"type":"boolean","description":"Deprecated: send `requirement` instead. `true` = required, `false` = available."},"isVariantAxis":{"type":"boolean","default":false},"validation":{"type":"object","additionalProperties":{},"nullable":true},"defaultValue":{"nullable":true},"position":{"type":"integer","default":0}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","richtext","number","decimal","boolean","date","datetime","select","multiselect","reference","media","measurement","url","email","json","money","tags","file"]},"groupId":{"type":"string","format":"uuid","nullable":true},"isLocalizable":{"type":"boolean"},"isScopable":{"type":"boolean"},"requirement":{"type":"string","enum":["required","suggested","available"]},"isRequired":{"type":"boolean"},"isVariantAxis":{"type":"boolean"},"validation":{"type":"object","additionalProperties":{},"nullable":true},"defaultValue":{"nullable":true},"position":{"type":"number"},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","type","groupId","isLocalizable","isScopable","requirement","isRequired","isVariantAxis","validation","position","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete an attribute","tags":["attributes"],"description":"Deletes the definition AND every value stored under its code, in one\ntransaction: the key is removed from the `attributes` payload of each\nproduct and variant (or content page, category, brand — whichever kind the\nattribute describes) and those rows' `updated_at` is bumped. Family\nattachments cascade away.\n\nBefore 0.26 only the definition went, and the values lived on as ghosts:\nstill returned by every read, invisible in the admin form, and resurrected\nunder the wrong type if the code was ever recreated. Call\n`GET /v1/attributes/:id/usage` first to show the person deleting what they\nare about to remove. The `attribute.deleted` event carries the per-table\ncounts in `removed_from`.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attributes/{id}/usage":{"get":{"summary":"Count the rows still carrying values for an attribute","tags":["attributes"],"description":"How many products, variants, content pages, categories or brands\nhold a value under this attribute's code. Every key is always present — zero\nwhere the attribute's entity kind doesn't apply — plus `total`.\n\nRead this before `DELETE /v1/attributes/:id`: deleting an attribute removes\nthose values, and this is the number a confirmation should show.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"products":{"type":"integer"},"variants":{"type":"integer"},"contentPages":{"type":"integer"},"categories":{"type":"integer"},"brands":{"type":"integer"},"total":{"type":"integer"}},"required":["products","variants","contentPages","categories","brands","total"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attribute-families":{"get":{"summary":"List attribute families","tags":["attributes"],"description":"Defaults to product families. Pass `entityKind=content`\nfor content-page types, or `category` / `brand` for the schema behind those.\n\nAsking for `category` or `brand` provisions that schema if the org has\nnone — one family plus a `description` field — and returns it. That is the\nonly read here with a side effect, and it is deliberate: it is how an org that\npredates brands gets an editable description without an operator running\nanything. It is idempotent, so a second call is two no-op inserts.","parameters":[{"schema":{"type":"string","enum":["product","content","category","brand"],"default":"product"},"in":"query","name":"entityKind","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"description":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","description"],"additionalProperties":false}}}}}}},"post":{"summary":"Create an attribute family","tags":["attributes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"entityKind":{"type":"string","enum":["product","content"],"default":"product"},"label":{"type":"object","additionalProperties":{"type":"string"}},"description":{"type":"string"}},"required":["code","label"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"description":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","description"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attribute-families/{id}":{"get":{"summary":"Get an attribute family with its attached attributes","tags":["attributes"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"description":{"type":"string","nullable":true},"attributes":{"type":"array","items":{"type":"object","properties":{"familyId":{"type":"string","format":"uuid"},"attributeId":{"type":"string","format":"uuid"},"requirementOverride":{"type":"string","enum":["required","suggested","available"],"nullable":true},"isRequiredOverride":{"type":"boolean","nullable":true},"position":{"type":"number"},"code":{"type":"string","description":"The attribute code — what you key an `attributes` payload by."},"type":{"type":"string","enum":["text","textarea","richtext","number","decimal","boolean","date","datetime","select","multiselect","reference","media","measurement","url","email","json","money","tags","file"],"description":"Determines the shape of a written value."},"label":{"type":"object","additionalProperties":{"type":"string"}},"isLocalizable":{"type":"boolean","description":"When false, a written value must carry `locale: null`."},"isScopable":{"type":"boolean","description":"When false, a written value must carry `channel: null`."},"requirement":{"type":"string","enum":["required","suggested","available"],"description":"The attribute’s own default, BEFORE this family’s override. `required` must be filled and counts toward completeness. `suggested` is always shown on edit surfaces to encourage filling it in, but is not counted. `available` (the default) is on the family but hidden from edit surfaces until a record carries a value or someone adds it — the lever that keeps a fifty-attribute family from putting fifty fields on every product page."},"isRequired":{"type":"boolean","description":"Deprecated: `requirement === \"required\"`. Read `requirement` instead."},"isVariantAxis":{"type":"boolean"},"validation":{"type":"object","additionalProperties":{},"nullable":true},"effectiveRequirement":{"type":"string","enum":["required","suggested","available"],"description":"`requirementOverride` when the family sets one, otherwise the attribute’s own. This is the value an edit surface should act on."},"effectiveIsRequired":{"type":"boolean","description":"Deprecated: `effectiveRequirement === \"required\"`. Read `effectiveRequirement` instead."}},"required":["familyId","attributeId","requirementOverride","isRequiredOverride","position","code","type","label","isLocalizable","isScopable","requirement","isRequired","isVariantAxis","validation","effectiveRequirement","effectiveIsRequired"],"additionalProperties":false}}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","description","attributes"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update an attribute family (label, description)","tags":["attributes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"object","additionalProperties":{"type":"string"}},"description":{"type":"string","nullable":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"entityKind":{"type":"string","enum":["product","content","category","brand"]},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"description":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","entityKind","label","description"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete an attribute family","tags":["attributes"],"description":"Refuses (409) while any product still uses the family — the FK is ON DELETE RESTRICT. Reassign or delete those products first. Attribute attachments (the join rows) cascade away.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attribute-families/{id}/attributes":{"post":{"summary":"Attach an attribute to a family","tags":["attributes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"attributeId":{"type":"string","format":"uuid"},"requirementOverride":{"type":"string","enum":["required","suggested","available"],"nullable":true,"description":"Per-family override of the attribute’s own `requirement`. `null` (the default) inherits it."},"isRequiredOverride":{"type":"boolean","nullable":true,"description":"Deprecated: send `requirementOverride` instead. `true` = required, `false` = available, `null` = inherit."},"position":{"type":"integer","default":0}},"required":["attributeId"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"familyId":{"type":"string","format":"uuid"},"attributeId":{"type":"string","format":"uuid"},"requirementOverride":{"type":"string","enum":["required","suggested","available"],"nullable":true},"isRequiredOverride":{"type":"boolean","nullable":true},"position":{"type":"number"}},"required":["familyId","attributeId","requirementOverride","isRequiredOverride","position"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/attribute-families/{id}/attributes/{attributeId}":{"patch":{"summary":"Update an attached attribute (position, requirement override)","tags":["attributes"],"description":"Reorders an attachment or changes its requirement override without detaching it. Positions are not required to be unique or dense; ties break by attribute code. Emits `family_attribute.updated`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"requirementOverride":{"type":"string","enum":["required","suggested","available"],"nullable":true,"description":"Per-family override of the attribute’s own `requirement`. `null` (the default) inherits it."},"isRequiredOverride":{"type":"boolean","nullable":true,"description":"Deprecated: send `requirementOverride` instead. `true` = required, `false` = available, `null` = inherit."},"position":{"type":"integer"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"attributeId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"familyId":{"type":"string","format":"uuid"},"attributeId":{"type":"string","format":"uuid"},"requirementOverride":{"type":"string","enum":["required","suggested","available"],"nullable":true},"isRequiredOverride":{"type":"boolean","nullable":true},"position":{"type":"number"}},"required":["familyId","attributeId","requirementOverride","isRequiredOverride","position"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Detach an attribute from a family","tags":["attributes"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"attributeId","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}}}}},"/v1/products":{"get":{"summary":"List products with filtering, sort, and pagination","tags":["products"],"description":"Paginated read of the catalog. Add `?locale=` and `?channel=` to\nresolve attribute values for a specific market + storefront — without them\nthe response includes the raw multi-locale/channel value arrays.\n\n**Example request**\n```bash\ncurl 'https://api.minipim.com/v1/products?limit=2&sortBy=name&locale=en_US&channel=headless-main' \\\n  -H 'Authorization: Bearer pim_xxxxxxxxxxxx' \\\n  -H 'x-organization-id: 00000000-0000-0000-0000-000000000001'\n```\n\n**Example response**\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"0b8c5d3a-7f4e-4d3c-9b1a-e1f2a3b4c5d6\",\n      \"name\": \"Eco Tote Bag\",\n      \"slug\": \"eco-tote-bag\",\n      \"status\": \"active\",\n      \"familyId\": \"5a2c1d8e-...\",\n      \"thumbnailUrl\": \"https://cdn.minipim.com/<orgId>/<mediaId>/eco-tote-hero.jpg\",\n      \"categoryIds\": [\"8d2a1f3c-...\", \"9e3b2c4d-...\"],\n      \"createdAt\": \"2026-05-30T14:22:11.000Z\",\n      \"updatedAt\": \"2026-06-02T09:10:55.000Z\"\n    }\n  ],\n  \"limit\": 50,\n  \"offset\": 0,\n  \"hasMore\": true\n}\n```\n\nIterate by reading `hasMore`. Pass `?withTotal=true` to add a `total`\nfield (costs one COUNT query). Default `limit` is 50, **max 200**.\nReconcile after downtime with `?updatedSince=<iso8601>`.\n\n### Resolving a URL slug\n\n`?slug=` is an exact match on the unique `(organization_id, slug)` index —\none indexed lookup returning at most one row. This is how a storefront turns\nthe slug in a request path into the id that `/v1/products/{id}`,\n`/{id}/variants` and `/{id}/media` need:\n\n```bash\ncurl 'https://api.minipim.com/v1/products?slug=10ft-ez-tube-display&limit=1'\n```\n\nDo **not** use `?q=` for this. `q` is a relevance search across name, slug\nand attribute text — it returns near-misses by design, so a page router built\non it will occasionally render the wrong product rather than a 404.\n\n### Filtering by category\n\n`?categoryId=` is **self-only**: it matches products assigned directly to\nthat category and nothing filed beneath it. Many catalogs assign products\nonly to leaf categories, so a self-only filter on a parent legitimately\nreturns zero rows.\n\nAdd `?includeDescendants=true` to match the category **and its entire\nsubtree**, resolved server-side in a single query:\n\n```bash\n# Every product anywhere under \"Backdrops\", paginated normally\ncurl 'https://api.minipim.com/v1/products?categoryId=8d2a1f3c-...&includeDescendants=true&limit=50'\n```\n\nPrefer this over fetching the tree and issuing one request per descendant —\nit is one round trip, and it paginates as a single result set.\n\n### Sorting and filtering by price (or any numeric attribute)\n\nPrice is not a column — it is an attribute, and its **code is per-organization\ndata**, so there is no `sortBy=price`. Name the attribute instead:\n\n```bash\n# Cheapest first\ncurl '.../v1/products?sortBy=attribute&sortAttribute=price&sortDir=asc'\n\n# Between $10.00 and $50.00 — bounds are INTEGER CENTS for money attributes\ncurl '.../v1/products?filterAttribute=price&filterMin=1000&filterMax=5000'\n```\n\n- Works on `money`, `number` and `decimal` attributes. `measurement` is\n  refused: its values carry a unit, so ordering raw amounts would rank 5 g\n  above 2 kg.\n- For `money`, bounds and ordering use `amount_cents`. **`filterMin=1000`\n  is $10.00.** No currency conversion happens — mixed-currency catalogs are\n  compared by number, so scope the query to one currency if that matters.\n- The value read is the one at the **default scope** (no locale, no channel).\n  A product with no default-scope value sorts **last** in both directions and\n  is excluded by either bound — it has no price, rather than a price of zero.\n- Unknown code, or a code of the wrong type, is a **422** naming the problem.\n  It never silently degrades to `name` ordering.\n- `sortAttribute` and `filterAttribute` are independent; set both to the\n  same code for the usual \"cheapest first, within a budget\" query.","parameters":[{"schema":{"type":"string","enum":["draft","active","archived"]},"in":"query","name":"status","required":false},{"schema":{"type":"string"},"in":"query","name":"q","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"slug","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"familyId","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"categoryId","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}],"default":"false"},"in":"query","name":"includeDescendants","required":false},{"schema":{"type":"string"},"in":"query","name":"brand","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"brandId","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"ids","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"tag","required":false,"description":"Repeatable — `?tag=a&tag=b` matches records carrying ALL listed tags. Values are canonicalized exactly as writes are, so `?tag=Buying%20Guide` matches the stored `buying-guide`; you never have to pre-slugify a filter. Backed by the GIN index on `tags`."},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"connectorId","required":false},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"updatedSince","required":false},{"schema":{"type":"string","enum":["name","slug","status","updatedAt","createdAt","relevance","attribute"]},"in":"query","name":"sortBy","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"sortAttribute","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"filterAttribute","required":false},{"schema":{"type":"number"},"in":"query","name":"filterMin","required":false},{"schema":{"type":"number"},"in":"query","name":"filterMax","required":false},{"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"in":"query","name":"sortDir","required":false},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}]},"in":"query","name":"withTotal","required":false},{"schema":{"type":"string"},"in":"query","name":"locale","required":false},{"schema":{"type":"string"},"in":"query","name":"channel","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"brandId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true},"thumbnailUrl":{"type":"string","nullable":true},"categoryIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","brandId","attributes","tags","createdBy","updatedBy","thumbnailUrl","categoryIds"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"},"hasMore":{"type":"boolean"},"total":{"type":"integer"}},"required":["data","limit","offset","hasMore"],"additionalProperties":false}}}}}},"post":{"summary":"Create a product","tags":["products"],"description":"Validates the `attributes` payload against the family at write time. Emits `product.created`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"familyId":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"],"default":"draft"},"brandId":{"type":"string","format":"uuid","nullable":true},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}},"default":{}},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":80},"maxItems":50,"description":"Free-form tags, canonicalized on write and echoed back in canonical form — the response will NOT match your input verbatim. Each tag is lower-cased, stripped of diacritics, has every run of non-alphanumerics replaced with `-`, is trimmed of leading/trailing `-`, and is then truncated to 60 characters. So `\"Buying Guide\"` is stored and filtered as `buying-guide`. Tags that normalize to nothing (`\"\"`, `\"!!\"`) are dropped, and duplicates are removed keeping first-occurrence order, so the array you get back may be SHORTER than the one you sent. Note the asymmetry with `maxLength`: an item may be up to 80 characters on input but is truncated to 60 once canonicalized, which can collapse two long tags into one.","default":[]}},"required":["familyId","name"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"brandId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","brandId","attributes","tags","createdBy","updatedBy"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/facets":{"get":{"summary":"Faceted counts for the products list","tags":["products"],"description":"Counts per dimension (family, status, category, brand,\nconnector, tag). Each dimension is computed with all OTHER filters applied, so\nthe UI can show pivot-style counts.\n\n**Every dimension is capped.** By default: 30 entries for categories, brands and\ntags, 50 for families, 20 for connectors. Pass `?facetLimit=` (1–500) to raise\nall of them at once. A truncated dimension is indistinguishable from a complete\none in the response, so if you are asking a question that depends on seeing\nevery value — \"which of my categories are empty?\" — set it explicitly.\n\n**`categories` follows `includeDescendants`.** Off (the default), a category\ncounts only the products filed directly against it. On, it counts its entire\nsubtree, matching what `GET /v1/products?categoryId=X&includeDescendants=true`\nwould return. Catalogs that file against leaf categories only will see 0 for\nevery parent without it.","parameters":[{"schema":{"type":"string","enum":["draft","active","archived"]},"in":"query","name":"status","required":false},{"schema":{"type":"string"},"in":"query","name":"q","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"slug","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"familyId","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"categoryId","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}],"default":"false"},"in":"query","name":"includeDescendants","required":false},{"schema":{"type":"string"},"in":"query","name":"brand","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"brandId","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"ids","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"tag","required":false,"description":"Repeatable — `?tag=a&tag=b` matches records carrying ALL listed tags. Values are canonicalized exactly as writes are, so `?tag=Buying%20Guide` matches the stored `buying-guide`; you never have to pre-slugify a filter. Backed by the GIN index on `tags`."},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"connectorId","required":false},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"updatedSince","required":false},{"schema":{"type":"string","enum":["name","slug","status","updatedAt","createdAt","relevance","attribute"]},"in":"query","name":"sortBy","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"sortAttribute","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"filterAttribute","required":false},{"schema":{"type":"number"},"in":"query","name":"filterMin","required":false},{"schema":{"type":"number"},"in":"query","name":"filterMax","required":false},{"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"in":"query","name":"sortDir","required":false},{"schema":{"type":"string"},"in":"query","name":"locale","required":false},{"schema":{"type":"string"},"in":"query","name":"channel","required":false},{"schema":{"type":"integer","minimum":1,"maximum":500},"in":"query","name":"facetLimit","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}],"default":"false"},"in":"query","name":"includeEmpty","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"families":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"code":{"type":"string"},"label":{"type":"string","nullable":true},"count":{"type":"integer"}},"required":["id","code","label","count"],"additionalProperties":false}},"statuses":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"}},"required":["value","count"],"additionalProperties":false}},"categories":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string","nullable":true},"count":{"type":"integer"}},"required":["id","slug","name","count"],"additionalProperties":false}},"brands":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"value":{"type":"string"},"count":{"type":"integer"}},"required":["id","slug","value","count"],"additionalProperties":false}},"tags":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"}},"required":["value","count"],"additionalProperties":false}},"connectors":{"type":"array","items":{"type":"object","properties":{"connectorId":{"type":"string","format":"uuid"},"name":{"type":"string"},"type":{"type":"string"},"count":{"type":"integer"}},"required":["connectorId","name","type","count"],"additionalProperties":false}}},"required":["total","families","statuses","categories","brands","tags","connectors"],"additionalProperties":false}}}}}}},"/v1/products/tags":{"get":{"summary":"List all product tags in use, with counts","tags":["products"],"description":"Every distinct tag currently on at least one product, ordered by usage. Tags are free-form, normalized to lowercase slugs on write (`Featured` → `featured`). Filter products by tag with `GET /v1/products?tag=<tag>` (repeatable; multiple tags AND together).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"}},"required":["value","count"],"additionalProperties":false}}}}}}}},"/v1/products/{id}":{"get":{"summary":"Get a product","tags":["products"],"description":"Fetch a single product by id, including its attribute payload,\nthumbnail, gallery, and category links. `attributes` is always the raw\n`{ code: [{ locale, channel, value }] }` shape (the editing surface).\n\nPass `?locale=` and/or `?channel=` to additionally receive\n`resolvedAttributes` — a flat `{ code: value }` map for that\n(locale, channel) request. Fallback order: exact match →\n`(locale, null)` → `(null, channel)` → `(null, null)`. Missing\nattributes are simply absent from the resolved map (no nulls).\n\n`modifierCount` and `hasRequiredModifiers` summarize the product's\norder-line options (engraving, add-ons, print choices, …). A PDP with\n`hasRequiredModifiers: true` must not let a customer check out without\nresolving them — fetch the full list from\n`GET /v1/products/{id}/modifiers`. Modifiers do NOT create SKUs; see\nDEVELOPERS.md → \"Sellable optionality\" for how they relate to variants.\n\n**Example response (with ?locale=en_US&channel=headless-main)**\n```json\n{\n  \"id\": \"0b8c5d3a-...\",\n  \"name\": \"Eco Tote Bag\",\n  \"slug\": \"eco-tote-bag\",\n  \"status\": \"active\",\n  \"familyId\": \"5a2c1d8e-...\",\n  \"attributes\": {\n    \"description\": [\n      { \"locale\": \"en_US\", \"channel\": null,            \"value\": \"Default copy...\" },\n      { \"locale\": \"en_US\", \"channel\": \"headless-main\", \"value\": \"Override for the headless site...\" }\n    ],\n    \"price\":  [{ \"locale\": null, \"channel\": null, \"value\": { \"amount_cents\": 1899, \"currency\": \"USD\" } }],\n    \"weight\": [{ \"locale\": null, \"channel\": null, \"value\": { \"amount\": 0.4, \"unit\": \"lb\" } }]\n  },\n  \"resolvedAttributes\": {\n    \"description\": \"Override for the headless site...\",\n    \"price\":  { \"amount_cents\": 1899, \"currency\": \"USD\" },\n    \"weight\": { \"amount\": 0.4, \"unit\": \"lb\" }\n  },\n  \"thumbnailUrl\": \"https://cdn.minipim.com/<orgId>/<mediaId>/eco-tote-hero.jpg\",\n  \"categoryIds\": [\"8d2a1f3c-...\", \"9e3b2c4d-...\"],\n  \"media\": [\n    {\n      \"id\": \"11aa22bb-...\",\n      \"url\": \"https://cdn.minipim.com/<orgId>/11aa22bb-.../hero.jpg\",\n      \"role\": \"hero\", \"position\": 0, \"altText\": { \"en_US\": \"Front view\" }\n    }\n  ],\n  \"modifierCount\": 2,\n  \"hasRequiredModifiers\": true,\n  \"createdAt\": \"...\",\n  \"updatedAt\": \"...\"\n}\n```\n\nReturns 404 with `{ \"error\": { \"code\": \"not_found\", ... } }` when the\nproduct doesn't exist (or isn't visible to this tenant — RLS makes those\nindistinguishable on purpose).","parameters":[{"schema":{"type":"string"},"in":"query","name":"locale","required":false},{"schema":{"type":"string"},"in":"query","name":"channel","required":false},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"brandId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true},"thumbnailUrl":{"type":"string","nullable":true},"categoryIds":{"type":"array","items":{"type":"string","format":"uuid"}},"media":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","nullable":true},"storageKey":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"role":{"type":"string"},"position":{"type":"integer"},"altText":{"type":"object","additionalProperties":{"type":"string"},"nullable":true}},"required":["id","url","storageKey","filename","mimeType","width","height","role","position"],"additionalProperties":false}},"modifierCount":{"type":"integer"},"hasRequiredModifiers":{"type":"boolean"},"resolvedAttributes":{"type":"object","additionalProperties":{}}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","brandId","attributes","tags","createdBy","updatedBy","thumbnailUrl","categoryIds","media","modifierCount","hasRequiredModifiers"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update a product (partial)","tags":["products"],"description":"Partial update with PATCH semantics. Scalar fields (`name`, `slug`, `status`) replace if present, else preserve.\n\n`attributes` merges at the attribute-code granularity:\n- code present with values → replaces that code's entire record list\n- code present with `[]` → clears that code\n- code absent → preserved as-is\n\nThe validator runs against the *merged* payload, so required-attribute rules see the complete picture.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"brandId":{"type":"string","format":"uuid","nullable":true},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}}},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":80},"maxItems":50,"description":"Replaces the full tag list when present. Free-form tags, canonicalized on write and echoed back in canonical form — the response will NOT match your input verbatim. Each tag is lower-cased, stripped of diacritics, has every run of non-alphanumerics replaced with `-`, is trimmed of leading/trailing `-`, and is then truncated to 60 characters. So `\"Buying Guide\"` is stored and filtered as `buying-guide`. Tags that normalize to nothing (`\"\"`, `\"!!\"`) are dropped, and duplicates are removed keeping first-occurrence order, so the array you get back may be SHORTER than the one you sent. Note the asymmetry with `maxLength`: an item may be up to 80 characters on input but is truncated to 60 once canonicalized, which can collapse two long tags into one."}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"brandId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","brandId","attributes","tags","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a product","tags":["products"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/{id}/completeness":{"get":{"summary":"Completeness scores per channel","tags":["products"],"description":"For each channel on the org, compute how ready this product is for syndication. Required attributes per channel come from `channel.settings.required_attributes` plus every attribute whose effective `requirement` is `required` (the family override wins). `suggested` attributes are shown on edit surfaces but do not count. Localizable attributes are counted per locale.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"channelCode":{"type":"string"},"total":{"type":"integer"},"filled":{"type":"integer"},"score":{"type":"integer"},"missing":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"locale":{"type":"string","nullable":true},"label":{"type":"string"},"variantsMissing":{"type":"integer"},"variantsTotal":{"type":"integer"}},"required":["code","locale"],"additionalProperties":false}}},"required":["channelCode","total","filled","score","missing"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/{id}/duplicate":{"post":{"summary":"Duplicate a product","tags":["products"],"description":"Deep-copies a product into a new `draft`: family, attributes, tags, categories, modifiers, media associations, outbound relationships and variants. Identifiers (UPC/GTIN/MPN) are NOT copied — they identify a specific physical product. Slug and variant SKUs are regenerated to stay unique. Emits `product.created`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"not":{}},{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string"}},"additionalProperties":false}]}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"brandId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","brandId","attributes","tags","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/bulk":{"post":{"summary":"Bulk create products (per-row results)","tags":["products"],"description":"A single failure does not sink the rest of the batch. Each row reports ok/error independently. Emits `product.created` for each row that succeeds.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"products":{"type":"array","items":{},"maxItems":500}},"required":["products"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"index":{"type":"integer"},"id":{"type":"string","format":"uuid"}},"required":["ok","index","id"],"additionalProperties":false},{"type":"object","properties":{"ok":{"type":"boolean","enum":[false]},"index":{"type":"integer"},"error":{"type":"string"}},"required":["ok","index","error"],"additionalProperties":false}]}}},"required":["results"],"additionalProperties":false}}}}}}},"/v1/products/bulk-move-family":{"post":{"summary":"Bulk-reassign products to a different family","tags":["products"],"description":"Per-row results. In `preserve` mode, a row fails if the destination family does not include every attribute code currently on the product. In `drop_unsupported` mode, unsupported codes are removed from `attributes` and the row proceeds. Required-attribute validation runs on the merged payload (post-drop), so a move can also fail if the destination family requires attributes the product is missing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"familyId":{"type":"string","format":"uuid"},"mode":{"type":"string","enum":["preserve","drop_unsupported"],"default":"preserve"}},"required":["productIds","familyId"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"ok":{"type":"boolean"},"droppedCodes":{"type":"array","items":{"type":"string"}},"error":{"type":"string"}},"required":["productId","ok"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}}}}},"/v1/products/bulk-set-status":{"post":{"summary":"Bulk-set status (draft / active / archived) on a set of products","tags":["products"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"status":{"type":"string","enum":["draft","active","archived"]}},"required":["productIds","status"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"ok":{"type":"boolean"},"error":{"type":"string"}},"required":["productId","ok"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}}}}},"/v1/products/bulk-delete":{"post":{"summary":"Bulk-delete products","tags":["products"],"description":"Per-row results. Failures are reported individually; the rest still delete.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500}},"required":["productIds"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"ok":{"type":"boolean"},"error":{"type":"string"}},"required":["productId","ok"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}}}}},"/v1/products/bulk-set-attribute":{"post":{"summary":"Bulk-set a single attribute value across many products","tags":["products"],"description":"Writes the same `(code, locale, channel) → value` tuple onto each product, merging into the existing payload (other codes preserved). Each product is validated against its own family — a row fails if the attribute is not in that product's family or fails type/options checks.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"code":{"type":"string","minLength":1},"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{},"clear":{"type":"boolean","default":false}},"required":["productIds","code","locale","channel"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"ok":{"type":"boolean"},"error":{"type":"string"}},"required":["productId","ok"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}}}}},"/v1/products/bulk-set-categories":{"post":{"summary":"Bulk-modify category assignments across products","tags":["products"],"description":"`mode=add` inserts (idempotent), `mode=remove` deletes the listed assignments, `mode=replace` wipes existing assignments and inserts the new list.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"mode":{"type":"string","enum":["add","remove","replace"]},"categoryIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["productIds","mode","categoryIds"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"ok":{"type":"boolean"},"error":{"type":"string"}},"required":["productId","ok"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}}}}},"/v1/products/bulk-set-tags":{"post":{"summary":"Bulk-modify tags across products","tags":["products"],"description":"`mode=add` unions the listed tags into each product, `mode=remove` strips them, `mode=replace` overwrites the full list. Tags are normalized (slugified + deduped) exactly like single-product writes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":500},"mode":{"type":"string","enum":["add","remove","replace"]},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":80},"maxItems":50,"description":"Free-form tags, canonicalized on write and echoed back in canonical form — the response will NOT match your input verbatim. Each tag is lower-cased, stripped of diacritics, has every run of non-alphanumerics replaced with `-`, is trimmed of leading/trailing `-`, and is then truncated to 60 characters. So `\"Buying Guide\"` is stored and filtered as `buying-guide`. Tags that normalize to nothing (`\"\"`, `\"!!\"`) are dropped, and duplicates are removed keeping first-occurrence order, so the array you get back may be SHORTER than the one you sent. Note the asymmetry with `maxLength`: an item may be up to 80 characters on input but is truncated to 60 once canonicalized, which can collapse two long tags into one."}},"required":["productIds","mode","tags"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"ok":{"type":"boolean"},"error":{"type":"string"}},"required":["productId","ok"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}}}}},"/v1/products/{id}/variants":{"get":{"summary":"List variants of a product","tags":["products"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"productId":{"type":"string","format":"uuid"},"sku":{"type":"string"},"position":{"type":"number"},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"status":{"type":"string","enum":["draft","active","archived"]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","productId","sku","position","attributes","status","createdBy","updatedBy"],"additionalProperties":false}}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"post":{"summary":"Create a variant under a product","tags":["products"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"position":{"type":"integer","default":0},"status":{"type":"string","enum":["draft","active","archived"],"default":"draft"},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}},"default":{}}},"required":["sku"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"productId":{"type":"string","format":"uuid"},"sku":{"type":"string"},"position":{"type":"number"},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"status":{"type":"string","enum":["draft","active","archived"]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","productId","sku","position","attributes","status","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/variants/{id}":{"get":{"summary":"Get a variant by id","tags":["products"],"description":"Direct lookup — cart/checkout flows can resolve a\nvariant from a stored line-item id without already knowing the parent\nproduct. The returned row includes `productId`; fetch the parent with\n`GET /v1/products/{productId}` if you need product-level fields.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"productId":{"type":"string","format":"uuid"},"sku":{"type":"string"},"position":{"type":"number"},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"status":{"type":"string","enum":["draft","active","archived"]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","productId","sku","position","attributes","status","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update a variant (partial)","tags":["products"],"description":"Partial update with PATCH semantics. Scalar fields (`sku`, `position`, `status`) replace if present, else preserve.\n\n`attributes` merges at the attribute-code granularity:\n- code present with values → replaces that code's entire record list\n- code present with `[]` → clears that code\n- code absent → preserved as-is\n\nValidation runs against the *merged* payload, but required-attribute enforcement is relaxed for variants (per the variant-validation rule); product-level writes still enforce required.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1},"position":{"type":"integer"},"status":{"type":"string","enum":["draft","active","archived"]},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}}}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"productId":{"type":"string","format":"uuid"},"sku":{"type":"string"},"position":{"type":"number"},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"status":{"type":"string","enum":["draft","active","archived"]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","productId","sku","position","attributes","status","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a variant","tags":["products"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/{id}/variants/matrix":{"post":{"summary":"Generate a variant matrix from variant-axis attribute values","tags":["products"],"description":"Cartesian product across the supplied axes. Each axis code must be a variant-axis attribute on the product family. SKU collisions abort the whole batch (409).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"axes":{"type":"object","additionalProperties":{"type":"array","items":{},"minItems":1}},"skuTemplate":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"],"default":"draft"},"startPosition":{"type":"integer","minimum":0,"default":0}},"required":["axes"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"productId":{"type":"string","format":"uuid"},"sku":{"type":"string"},"position":{"type":"number"},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"status":{"type":"string","enum":["draft","active","archived"]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","productId","sku","position","attributes","status","createdBy","updatedBy"],"additionalProperties":false}}},"required":["created"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/variants/{variantId}/identifiers":{"get":{"summary":"List identifiers attached to a variant","tags":["products"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"variantId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"variantId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["sku","upc","gtin8","gtin12","gtin13","gtin14","ean","mpn","asin","isbn","custom"]},"customLabel":{"type":"string","nullable":true},"value":{"type":"string"}},"required":["id","organizationId","createdAt","updatedAt","variantId","type","customLabel","value"],"additionalProperties":false}}}}}}},"post":{"summary":"Attach an identifier (SKU/UPC/GTIN/etc.) to a variant","tags":["products"],"description":"GTIN-typed values are validated by check digit and length on write. Emits `variant.updated` (`identifiers`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["sku","upc","gtin8","gtin12","gtin13","gtin14","ean","mpn","asin","isbn","custom"]},"customLabel":{"type":"string"},"value":{"type":"string","minLength":1}},"required":["type","value"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"variantId","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"variantId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["sku","upc","gtin8","gtin12","gtin13","gtin14","ean","mpn","asin","isbn","custom"]},"customLabel":{"type":"string","nullable":true},"value":{"type":"string"}},"required":["id","organizationId","createdAt","updatedAt","variantId","type","customLabel","value"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"put":{"summary":"Replace the full identifier set for a variant","tags":["products"],"description":"Wipes the variant's current identifiers and inserts the provided list. Pass an empty array to clear all. Each item is validated like `POST /identifiers`. Emits `variant.updated` (`identifiers`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"identifiers":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["sku","upc","gtin8","gtin12","gtin13","gtin14","ean","mpn","asin","isbn","custom"]},"customLabel":{"type":"string"},"value":{"type":"string","minLength":1}},"required":["type","value"],"additionalProperties":false}}},"required":["identifiers"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"variantId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"variantId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["sku","upc","gtin8","gtin12","gtin13","gtin14","ean","mpn","asin","isbn","custom"]},"customLabel":{"type":"string","nullable":true},"value":{"type":"string"}},"required":["id","organizationId","createdAt","updatedAt","variantId","type","customLabel","value"],"additionalProperties":false}}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/identifiers/{id}":{"delete":{"summary":"Detach an identifier","tags":["products"],"description":"Emits `variant.updated` (`identifiers`) for the owning variant.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/content":{"get":{"summary":"List content pages","tags":["content"],"description":"Filters and pagination mirror `GET /v1/products`. `tag` is repeatable and matches pages carrying ALL listed tags.\n\nOrdering defaults to `updatedAt` descending. For a blog archive sort by `publishedAt`, which imports preserve from the source system:\n\n```\ncurl 'https://api.minipim.com/v1/content?sortBy=publishedAt&sortDir=desc&limit=20'\n```\n\nPages with no `publishedAt` sort LAST in both directions — an undated page is unscheduled, not newest.\n\n`categoryId` is SELF-ONLY. Content is filed against leaf categories, so filtering by a parent returns an empty page unless you add `includeDescendants=true` to match the whole subtree.","parameters":[{"schema":{"type":"string","enum":["draft","active","archived"]},"in":"query","name":"status","required":false},{"schema":{"type":"string"},"in":"query","name":"q","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"slug","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"ids","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"familyId","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"categoryId","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}],"default":"false"},"in":"query","name":"includeDescendants","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"channelId","required":false},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"in":"query","name":"tag","required":false,"description":"Repeatable — `?tag=a&tag=b` matches records carrying ALL listed tags. Values are canonicalized exactly as writes are, so `?tag=Buying%20Guide` matches the stored `buying-guide`; you never have to pre-slugify a filter."},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"updatedSince","required":false},{"schema":{"type":"string","enum":["publishedAt","updatedAt","createdAt","title"],"default":"updatedAt"},"in":"query","name":"sortBy","required":false},{"schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"in":"query","name":"sortDir","required":false},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"title":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"publishedAt":{"type":"string","format":"date-time","nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","title","slug","status","publishedAt","channelId","attributes","tags","createdBy","updatedBy"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"},"hasMore":{"type":"boolean"}},"required":["data","limit","offset","hasMore"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"post":{"summary":"Create a content page","tags":["content"],"description":"Validates `attributes` against the family at write time, exactly as products do — an undeclared attribute code is a 422. Required attributes are enforced only for `active` pages, so a draft may be saved incomplete. Emits `content_page.created`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"familyId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"],"default":"draft"},"publishedAt":{"type":"string","format":"date-time"},"channelId":{"type":"string","format":"uuid","nullable":true},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}},"default":{}},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":80},"maxItems":50,"description":"Free-form tags, canonicalized on write and echoed back in canonical form — the response will NOT match your input verbatim. Each tag is lower-cased, stripped of diacritics, has every run of non-alphanumerics replaced with `-`, is trimmed of leading/trailing `-`, and is then truncated to 60 characters. So `\"Buying Guide\"` is stored and filtered as `buying-guide`. Tags that normalize to nothing (`\"\"`, `\"!!\"`) are dropped, and duplicates are removed keeping first-occurrence order, so the array you get back may be SHORTER than the one you sent. Note the asymmetry with `maxLength`: an item may be up to 80 characters on input but is truncated to 60 once canonicalized, which can collapse two long tags into one.","default":[]}},"required":["familyId","title"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"title":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"publishedAt":{"type":"string","format":"date-time","nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","title","slug","status","publishedAt","channelId","attributes","tags","createdBy","updatedBy"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/content/tags":{"get":{"summary":"List all content-page tags in use, with counts","tags":["content"],"description":"Every distinct tag currently on at least one content page, ordered by usage. Powers tag autocomplete in the editor, which is what keeps a vocabulary from drifting into `guide` / `guides` / `Guides`. Filter pages by tag with `GET /v1/content?tag=<tag>` (repeatable; multiple tags AND together).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"value":{"type":"string"},"count":{"type":"integer"}},"required":["value","count"],"additionalProperties":false}}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/content/{id}":{"get":{"summary":"Get a content page","tags":["content"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"title":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"publishedAt":{"type":"string","format":"date-time","nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true},"categoryIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["id","organizationId","createdAt","updatedAt","familyId","title","slug","status","publishedAt","channelId","attributes","tags","createdBy","updatedBy","categoryIds"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update a content page","tags":["content"],"description":"`attributes` is merged per code, not replaced wholesale. Stamps `publishedAt` on the first transition to `active` unless the caller supplies one. Moving a page TO `active` enforces the family’s required attributes, even when the body carries no `attributes` — so a status-only publish of an incomplete page is a 422. Editing an already-active page does not re-validate. Emits `content_page.updated` carrying `previous_status`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"publishedAt":{"type":"string","format":"date-time","nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}}},"tags":{"type":"array","items":{"type":"string","minLength":1,"maxLength":80},"maxItems":50,"description":"Replaces the full tag list when present. Free-form tags, canonicalized on write and echoed back in canonical form — the response will NOT match your input verbatim. Each tag is lower-cased, stripped of diacritics, has every run of non-alphanumerics replaced with `-`, is trimmed of leading/trailing `-`, and is then truncated to 60 characters. So `\"Buying Guide\"` is stored and filtered as `buying-guide`. Tags that normalize to nothing (`\"\"`, `\"!!\"`) are dropped, and duplicates are removed keeping first-occurrence order, so the array you get back may be SHORTER than the one you sent. Note the asymmetry with `maxLength`: an item may be up to 80 characters on input but is truncated to 60 once canonicalized, which can collapse two long tags into one."}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"title":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"publishedAt":{"type":"string","format":"date-time","nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","title","slug","status","publishedAt","channelId","attributes","tags","createdBy","updatedBy"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a content page","tags":["content"],"description":"Emits `content_page.deleted`.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/content/by-slug/{slug}":{"get":{"summary":"Get a content page by slug","tags":["content"],"description":"Content is URL-addressed: a storefront route like `/blog/[slug]` has the slug and nothing else. Without this it has to list-and-filter to find an id, which is a full scan for a single page.\n\n**Slug uniqueness is scoped two ways**, via two partial unique indexes: channel-scoped pages are unique per `(organization, channel, slug)`, and org-wide pages (no channel) are unique per `(organization, slug)`. Those are SEPARATE namespaces, so a channel page and an org-wide page may legitimately share a slug.\n\nThis resolves that the way a storefront needs:\n\n- **With `channelId`** — the channel’s own page wins; if it has none, the org-wide page is returned. That makes a channel-specific page an override of shared content rather than a separate thing to look up.\n- **Without `channelId`** — org-wide pages only. A caller with no channel context should never be handed one channel’s page by accident.\n\n**By default this returns a page in ANY status**, exactly like `GET /v1/content/{id}` — a draft is reachable here on purpose, so admin and preview tooling can share one lookup. On a public storefront route that is a disclosure risk: an unpublished page is readable by anyone who guesses its slug, and the slug is usually the title.\n\n**Pass `?status=active` on any route a shopper can reach.** Unknown query parameters are now rejected with a 422 rather than ignored, so a filter that does not apply fails loudly instead of appearing to work.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"query","name":"channelId","required":false,"description":"Resolve within this channel first, falling back to the org-wide page."},{"schema":{"type":"string","enum":["draft","active","archived"]},"in":"query","name":"status","required":false,"description":"Restrict to this status. Omit to get the page in ANY status — which is what admin and preview tooling want, and what a storefront almost certainly does not. Pass `status=active` on a public route."},{"schema":{"type":"string","minLength":1},"in":"path","name":"slug","required":true,"description":"The page’s `slug`, not its id."}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid"},"title":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"publishedAt":{"type":"string","format":"date-time","nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"tags":{"type":"array","items":{"type":"string"},"description":"Tags in canonical form. These are normalized on write (lower-cased, slugified, deduplicated, truncated), so this array will not necessarily match what was submitted — see the `tags` field on the create/update request body for the exact rules. Filter with `?tag=` using either form."},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true},"categoryIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["id","organizationId","createdAt","updatedAt","familyId","title","slug","status","publishedAt","channelId","attributes","tags","createdBy","updatedBy","categoryIds"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/content/{id}/categories":{"put":{"summary":"Replace a content page’s categories","tags":["content"],"description":"Replaces the full set — pass an empty array to clear. Uses the same `categories` table products do, so a category means one thing across the whole org. Emits `content_category.unassigned` for each category removed and `content_category.assigned` for each added.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"categoryIds":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":100}},"required":["categoryIds"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"contentPageId":{"type":"string","format":"uuid"},"categoryId":{"type":"string","format":"uuid"},"position":{"type":"number"}},"required":["contentPageId","categoryId","position"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/brands":{"get":{"summary":"List brands","tags":["brands"],"description":"Brands are a first-class entity: a stable id, a slug, a\nstatus and whatever custom fields the org has declared (`description` out of\nthe box). Products point at one by `brandId`.\n\nPass `?withProductCounts=true` for a `productCount` per brand — one extra\ngrouped query, which is why it is off by default. The count includes products\nof every status; filter the products list if you need active-only.","parameters":[{"schema":{"type":"string","enum":["draft","active","archived"]},"in":"query","name":"status","required":false},{"schema":{"type":"string"},"in":"query","name":"q","required":false},{"schema":{"type":"string","minLength":1},"in":"query","name":"slug","required":false},{"schema":{"type":"string","enum":["name","slug","createdAt","updatedAt"],"default":"name"},"in":"query","name":"sortBy","required":false},{"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"in":"query","name":"sortDir","required":false},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false},{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}],"default":"false"},"in":"query","name":"withProductCounts","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true},"productCount":{"type":"integer"}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","attributes","createdBy","updatedBy"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"},"hasMore":{"type":"boolean"}},"required":["data","limit","offset","hasMore"],"additionalProperties":false}}}}}},"post":{"summary":"Create a brand","tags":["brands"],"description":"Validates `attributes` against the org brand family at write time, exactly as products and content pages do — an undeclared code is a 422. Emits `brand.created`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"status":{"type":"string","enum":["draft","active","archived"],"default":"active"},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}},"default":{}}},"required":["name"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","attributes","createdBy","updatedBy"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/brands/{id}":{"get":{"summary":"Get a brand","tags":["brands"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true},"productCount":{"type":"integer"}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","attributes","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update a brand","tags":["brands"],"description":"Merges `attributes` at attribute-code granularity, exactly as products do: a code with values replaces that code, a code with `[]` clears it, and a code left out is preserved. Emits `brand.updated`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1},"status":{"type":"string","enum":["draft","active","archived"]},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}}}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"familyId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string","enum":["draft","active","archived"]},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"createdBy":{"type":"string","format":"uuid","nullable":true},"updatedBy":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","familyId","name","slug","status","attributes","createdBy","updatedBy"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a brand","tags":["brands"],"description":"Products carrying this brand are **not** deleted — their\n`brandId` is set to null and they become unbranded. The count of products that\njust lost their brand rides on the `brand.deleted` event, because a connector\nmirroring brands downstream needs the blast radius, not just the name.\n\nPass `?force=true` to confirm when the brand still has products. Without it a\nbrand with products is a 422, so nobody unbrands 340 rows by clicking delete on\nthe wrong row.","parameters":[{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}],"default":"false"},"in":"query","name":"force","required":false},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/channels":{"get":{"summary":"List channels","tags":["taxonomy"],"description":"Channels are the surfaces your catalog publishes to —\nstorefronts, marketplaces, ad platforms. Used in attribute-value resolution\ntogether with locale: `(locale, channel)` keys each value. `settings` is\nfree-form per-channel config; `settings.required_attributes` drives the\nper-channel completeness score on products.\n\n**Example response**\n```json\n[\n  {\n    \"id\": \"3ac5ab02-93fd-49b7-91a3-681fc36eabdd\",\n    \"code\": \"headless-main\",\n    \"name\": \"Headless Storefront\",\n    \"locales\": [\"en_US\"],\n    \"settings\": null,\n    \"createdAt\": \"2026-06-02T22:28:01.926Z\",\n    \"updatedAt\": \"2026-06-02T22:28:01.926Z\"\n  },\n  {\n    \"id\": \"f1c2b3a4-...\",\n    \"code\": \"bc_pdo6b28kp8_default\",\n    \"name\": \"BigCommerce (default)\",\n    \"locales\": [\"en_US\"],\n    \"settings\": {\n      \"bc_channel_id\": 1,\n      \"bc_store_hash\": \"pdo6b28kp8\",\n      \"required_attributes\": [\"description\", \"price\", \"brand\", \"weight\"]\n    },\n    \"createdAt\": \"...\",\n    \"updatedAt\": \"...\"\n  }\n]\n```\n\nChannels seeded by connectors carry `bc_*` / `shopify_*` keys in\n`settings` so they can be identified and scoped-wiped on connector\nuninstall — leave those alone unless you know what you're doing.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"name":{"type":"string"},"locales":{"type":"array","items":{"type":"string"}},"settings":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","name","locales","settings"],"additionalProperties":false}}}}}}},"post":{"summary":"Create a channel","tags":["taxonomy"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"locales":{"type":"array","items":{"type":"string"},"default":[]},"settings":{"type":"object","additionalProperties":{}}},"required":["code","name"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"name":{"type":"string"},"locales":{"type":"array","items":{"type":"string"}},"settings":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","name","locales","settings"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/channels/{id}":{"patch":{"summary":"Update a channel (settings, locales, name)","tags":["taxonomy"],"description":"Partial update. `settings` replaces the entire object — to clear, pass `null`. Use `settings.required_attributes` to drive per-channel completeness scoring.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"locales":{"type":"array","items":{"type":"string"}},"settings":{"type":"object","additionalProperties":{},"nullable":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"name":{"type":"string"},"locales":{"type":"array","items":{"type":"string"}},"settings":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","name","locales","settings"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a channel (surface)","tags":["taxonomy"],"description":"Removes a channel, so it stops appearing as a completeness/readiness surface. Use this to clear a surface left behind after uninstalling the connector that seeded it (channels are decoupled from connectors by design).\n\nContent pages and categories may be BOUND to a channel, and both those FKs are ON DELETE CASCADE — so deleting the channel would delete the rows themselves, including ones a person wrote by hand. This route refuses with 409 rather than do that. Pass `?detach=true` to reattach them as org-wide (`channel_id = null`) and then delete the channel, which is what the BigCommerce pull does before its scoped wipe.\n\nChannel-scoped attribute VALUES are a different thing: they live inside each entity's `attributes` JSONB keyed by channel CODE, not as FKs. They are left untouched and simply stop resolving to a live channel. Recreating a channel with the same code makes them resolve again.","parameters":[{"schema":{"anyOf":[{"type":"boolean"},{"type":"string","enum":["true","false","1","0"]}]},"in":"query","name":"detach","required":false},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/locales":{"get":{"summary":"List locales","tags":["taxonomy"],"description":"Locales are language/region codes (IETF\n`language_REGION`) that scope attribute values. Used with channels to\nkey attribute values: `(locale, channel)`. Exactly one locale is\nmarked `isDefault` per org — that's the fallback when a fetch doesn't\nspecify `?locale=`.\n\n**Example response**\n```json\n[\n  { \"id\": \"1f8d9e3a-...\", \"code\": \"en_US\", \"label\": \"English (US)\", \"isDefault\": true,  \"createdAt\": \"...\", \"updatedAt\": \"...\" },\n  { \"id\": \"2a9c8d4b-...\", \"code\": \"fr_FR\", \"label\": \"Français (France)\", \"isDefault\": false, \"createdAt\": \"...\", \"updatedAt\": \"...\" }\n]\n```\n\nWhen passing `?locale=fr_FR` to product reads, MiniPim falls back\nthrough `(fr_FR, channel)` → `(fr_FR, null)` → `(en_US, channel)` →\n`(en_US, null)` until it finds a value. See\n[docs/DEVELOPERS.md](https://github.com/Epic-Design-Labs/minipim/blob/main/docs/DEVELOPERS.md#locale--channel-resolution)\nfor the full resolution order.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"type":"string"},"isDefault":{"type":"boolean"}},"required":["id","organizationId","createdAt","updatedAt","code","label","isDefault"],"additionalProperties":false}}}}}}},"post":{"summary":"Create a locale","tags":["taxonomy"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":2},"label":{"type":"string","minLength":1},"isDefault":{"type":"boolean","default":false}},"required":["code","label"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"type":"string"},"isDefault":{"type":"boolean"}},"required":["id","organizationId","createdAt","updatedAt","code","label","isDefault"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/locales/{id}":{"patch":{"summary":"Update a locale (label, default)","tags":["taxonomy"],"description":"Setting `isDefault: true` makes this the org’s sole default — every other locale is un-defaulted in the same transaction.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1},"isDefault":{"type":"boolean"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"type":"string"},"isDefault":{"type":"boolean"}},"required":["id","organizationId","createdAt","updatedAt","code","label","isDefault"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a locale","tags":["taxonomy"],"description":"Refuses to delete the default locale — set another default first. Attribute values scoped to this locale are stored in product JSONB (not FKs) and are left untouched; they simply no longer resolve to a live locale.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/categories":{"get":{"summary":"List categories","tags":["taxonomy"],"description":"Returns the full category tree, flat, ordered by\n`position`. Build the hierarchy client-side by joining on `parentId` —\nroots have `parentId: null`. `name` is a localized label map keyed by\nlocale code.\n\n**Example response**\n```json\n[\n  {\n    \"id\": \"8d2a1f3c-...\",\n    \"parentId\": null,\n    \"name\": { \"en_US\": \"Banner Stands\" },\n    \"slug\": \"banner-stands\",\n    \"position\": 0,\n    \"status\": \"active\",\n    \"createdAt\": \"...\",\n    \"updatedAt\": \"...\"\n  },\n  {\n    \"id\": \"9e3b2c4d-...\",\n    \"parentId\": \"8d2a1f3c-...\",\n    \"name\": { \"en_US\": \"24\\\" - 36\\\" Banner Stand Displays\" },\n    \"slug\": \"24-36-banner-stand-displays\",\n    \"position\": 0,\n    \"status\": \"active\",\n    \"createdAt\": \"...\",\n    \"updatedAt\": \"...\"\n  }\n]\n```\n\nTo get a single category's full ancestor path, walk `parentId` upwards\nclient-side — this endpoint returns the whole tree in one call, so no extra\nrequests are needed. (`GET /v1/categories/{id}` returns the same row shape\nfor one category; it does **not** resolve an ancestor path.)\n\nTo list the products under a category *including* its subtree, use\n`GET /v1/products?categoryId=<id>&includeDescendants=true` rather than\nresolving descendants here and issuing one request per id.\n\nReturns the full tree by default. `?limit=` / `?offset=` are supported\nfor large taxonomies (ordered by `position`); omit them to get everything.\n\nEvery category carries a `status` (`draft` / `active` / `archived`), the\nsame lifecycle products and brands have. Without `?status=` the list returns\nevery status, so an admin can see what is hidden; a storefront passes\n`?status=active`. A status applies to that category only — it does **not**\ncascade to descendants. A storefront that wants to hide a subtree checks each\nancestor's status while walking `parentId`, which it already does to build\nthe tree. Products under a non-active category are unaffected: they keep their\nown `status` and their other categories.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":1000},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false},{"schema":{"type":"string","enum":["draft","active","archived"]},"in":"query","name":"status","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"parentId":{"type":"string","format":"uuid","nullable":true},"name":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"slug":{"type":"string"},"position":{"type":"number"},"channelId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["draft","active","archived"]},"familyId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]}},"required":["id","organizationId","createdAt","updatedAt","parentId","name","slug","position","channelId","status","familyId","attributes"],"additionalProperties":false}}}}}}},"post":{"summary":"Create a category","tags":["taxonomy"],"description":"Validates `attributes` against the org category family at write time, exactly as products do — an undeclared code is a 422. Emits `category.created`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"parentId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"object","additionalProperties":{"type":"string"}},"slug":{"type":"string","minLength":1},"position":{"type":"integer","default":0},"channelId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["draft","active","archived"],"default":"active"},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}},"default":{}}},"required":["name","slug"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"parentId":{"type":"string","format":"uuid","nullable":true},"name":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"slug":{"type":"string"},"position":{"type":"number"},"channelId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["draft","active","archived"]},"familyId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]}},"required":["id","organizationId","createdAt","updatedAt","parentId","name","slug","position","channelId","status","familyId","attributes"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/categories/{id}":{"get":{"summary":"Get a category","tags":["taxonomy"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"parentId":{"type":"string","format":"uuid","nullable":true},"name":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"slug":{"type":"string"},"position":{"type":"number"},"channelId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["draft","active","archived"]},"familyId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]}},"required":["id","organizationId","createdAt","updatedAt","parentId","name","slug","position","channelId","status","familyId","attributes"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update a category","tags":["taxonomy"],"description":"`attributes` merges at attribute-code granularity (values replace that code, `[]` clears it, absent is preserved). `status` flips the category between `draft`, `active` and `archived` without touching its children. Emits `category.updated`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"parentId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"object","additionalProperties":{"type":"string"}},"slug":{"type":"string","minLength":1},"position":{"type":"integer"},"channelId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["draft","active","archived"]},"attributes":{"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"locale":{"type":"string","nullable":true},"channel":{"type":"string","nullable":true},"value":{}},"required":["locale","channel"],"additionalProperties":false}}}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"parentId":{"type":"string","format":"uuid","nullable":true},"name":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"slug":{"type":"string"},"position":{"type":"number"},"channelId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["draft","active","archived"]},"familyId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]}},"required":["id","organizationId","createdAt","updatedAt","parentId","name","slug","position","channelId","status","familyId","attributes"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a category","tags":["taxonomy"],"description":"Children re-parent to the deleted category's parent (or root) via the schema's ON DELETE SET NULL.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/{id}/categories":{"get":{"summary":"List categories assigned to a product","tags":["taxonomy"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"parentId":{"type":"string","format":"uuid","nullable":true},"name":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"slug":{"type":"string"},"position":{"type":"number"},"channelId":{"type":"string","format":"uuid","nullable":true},"status":{"type":"string","enum":["draft","active","archived"]},"familyId":{"type":"string","format":"uuid","nullable":true},"attributes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]}},"required":["id","organizationId","createdAt","updatedAt","parentId","name","slug","position","channelId","status","familyId","attributes"],"additionalProperties":false}}}}}}},"put":{"summary":"Replace a product's category assignments","tags":["taxonomy"],"description":"Wipes the product's current associations and writes the provided list in order. Empty array clears all assignments. Emits `category.unassigned` for each category removed, `category.assigned` for each added, then one `product.updated`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"categoryIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["categoryIds"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"assigned":{"type":"integer"}},"required":["assigned"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/modifiers":{"get":{"summary":"List modifier definitions","tags":["modifiers"],"description":"Modifiers are order-line options that do NOT produce new SKUs — engraving, gift wrap, monogram text, etc. The commerce engine consumes them at purchase time.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","select","multiselect","number","boolean","file"]},"validation":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","label","type","validation","config"],"additionalProperties":false}}}}}}},"post":{"summary":"Create a modifier definition","tags":["modifiers"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"label":{"type":"object","additionalProperties":{"type":"string"}},"type":{"type":"string","enum":["text","textarea","select","multiselect","number","boolean","file"]},"validation":{"type":"object","additionalProperties":{},"nullable":true},"config":{"type":"object","additionalProperties":{},"nullable":true}},"required":["code","label","type"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","select","multiselect","number","boolean","file"]},"validation":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","label","type","validation","config"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/modifiers/{id}":{"get":{"summary":"Get a modifier definition","tags":["modifiers"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","select","multiselect","number","boolean","file"]},"validation":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","label","type","validation","config"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update a modifier definition","tags":["modifiers"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^[a-z][a-z0-9_]*$"},"label":{"type":"object","additionalProperties":{"type":"string"}},"type":{"type":"string","enum":["text","textarea","select","multiselect","number","boolean","file"]},"validation":{"type":"object","additionalProperties":{},"nullable":true},"config":{"type":"object","additionalProperties":{},"nullable":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"code":{"type":"string"},"label":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"type":{"type":"string","enum":["text","textarea","select","multiselect","number","boolean","file"]},"validation":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","code","label","type","validation","config"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a modifier definition","tags":["modifiers"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/{id}/modifiers":{"get":{"summary":"List modifiers attached to a product","tags":["modifiers"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"modifierId":{"type":"string","format":"uuid"},"position":{"type":"number"},"isRequired":{"type":"boolean"}},"required":["productId","modifierId","position","isRequired"],"additionalProperties":false}}}}}}},"post":{"summary":"Attach a modifier to a product","tags":["modifiers"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"modifierId":{"type":"string","format":"uuid"},"position":{"type":"integer","default":0},"isRequired":{"type":"boolean","default":false}},"required":["modifierId"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"productId":{"type":"string","format":"uuid"},"modifierId":{"type":"string","format":"uuid"},"position":{"type":"number"},"isRequired":{"type":"boolean"}},"required":["productId","modifierId","position","isRequired"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/{id}/modifiers/{modifierId}":{"delete":{"summary":"Detach a modifier from a product","tags":["modifiers"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"modifierId","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}}}}},"/v1/products/{id}/media":{"get":{"summary":"List media attached to a product","tags":["media"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["image","video","document","model_3d"]},"storageKey":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"metadata":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"association":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false},"url":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","type","storageKey","filename","mimeType","sizeBytes","width","height","metadata","association","url"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/content/{id}/media":{"get":{"summary":"List media attached to a content page","tags":["media"],"description":"Same shape as the product/variant galleries. Lives here rather than under /v1/content so it sits with its siblings and reuses the same URL resolution; associations are tenant-scoped by RLS regardless.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["image","video","document","model_3d"]},"storageKey":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"metadata":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"association":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false},"url":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","type","storageKey","filename","mimeType","sizeBytes","width","height","metadata","association","url"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/variants/{id}/media":{"get":{"summary":"List media attached to a variant","tags":["media"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["image","video","document","model_3d"]},"storageKey":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"metadata":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"association":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false},"url":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","type","storageKey","filename","mimeType","sizeBytes","width","height","metadata","association","url"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/media":{"post":{"summary":"Upload media (multipart)","tags":["media"],"description":"Uploads a single file to object storage and creates a `media` row. If `entityType`+`entityId` are present in the multipart fields, also creates an association in the same transaction.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"The file to upload. Images, video, PDF, Office documents and glTF are accepted; the 422 response lists every allowed type in `details.allowed`. You do NOT have to get the content type right: when you send `application/octet-stream` (or no type at all), the leading bytes are sniffed and the detected type is used — so a PNG whose filename lost its extension uploads fine. SVG, CSV, plain text and Office formats have no distinguishing magic bytes and still need an explicit type. If a specific declared type contradicts the bytes, the bytes win."},"entityType":{"type":"string","enum":["product","variant","content_page"],"description":"Attach the upload in the same transaction. Requires `entityId`; supplying only one of the pair uploads the file WITHOUT associating it. A value outside the enum is a 422, not a silent miss."},"entityId":{"type":"string","format":"uuid","description":"The id of the entity named by `entityType`."},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"],"default":"gallery","description":"Association role. Ignored unless `entityType`+`entityId` are present. A value outside this enum is rejected with a 422 that lists the accepted values in `details.accepted`."},"position":{"type":"integer","minimum":0,"default":0,"description":"Sort position within the entity’s gallery. Must be a non-negative integer; anything else is a 422."},"altText":{"type":"string","description":"A JSON OBJECT keyed by locale, sent as a STRING — e.g. `{\"en_US\":\"Blue widget on white\"}`. Unparseable JSON, or JSON that is not an object of locale → string, does NOT fail the upload: the file is stored, no alt text is saved, and the 201 response carries a `warnings` entry saying so. Check `warnings` if you send alt text programmatically. Omit the field entirely if you have none.\n\n(0.20.0 briefly made this a 422; 0.21.0 restored the original behaviour and added `warnings` instead, so the problem is reported without breaking callers.)"}}}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["image","video","document","model_3d"]},"storageKey":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"metadata":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"association":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false},"url":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"},"description":"Non-fatal problems with this upload — currently only malformed `altText`. The upload succeeded; something you sent was ignored. Absent when there is nothing to report."}},"required":["id","organizationId","createdAt","updatedAt","type","storageKey","filename","mimeType","sizeBytes","width","height","metadata","url"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"413":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/media/associations":{"post":{"summary":"Attach a media to a product or variant","tags":["media"],"description":"Emits `media.attached`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"],"default":"gallery"},"position":{"type":"integer","minimum":0,"default":0},"altText":{"type":"object","additionalProperties":{"type":"string"}},"channelId":{"type":"string","format":"uuid"}},"required":["mediaId","entityType","entityId"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/media/associations/{id}":{"patch":{"summary":"Update a media association","tags":["media"],"description":"Emits `media.association_updated`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"integer","minimum":0},"altText":{"type":"object","additionalProperties":{"type":"string"},"nullable":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Detach media from an entity","tags":["media"],"description":"Emits `media.detached`.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/media/associations/reorder":{"put":{"summary":"Reorder an entity’s media","tags":["media"],"description":"Sets `position` for every association of the given entity from the order of `orderedIds`. Any `hero` association is pinned to position 0. `orderedIds` must contain exactly the entity’s association ids. Emits one `media.association_updated` per association.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"entityType":{"type":"string","enum":["product","variant","content_page"]},"entityId":{"type":"string","format":"uuid"},"orderedIds":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1}},"required":["entityType","entityId","orderedIds"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/media/associations/{id}/hero":{"post":{"summary":"Set an association as the hero (single hero, pinned first)","tags":["media"],"description":"Emits `media.association_updated` (`role`, `position`) for the promoted association and for the previous hero it demoted, if any.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"mediaId":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["product","variant","content_page","category","brand"]},"entityId":{"type":"string","format":"uuid"},"role":{"type":"string","enum":["hero","gallery","thumbnail","technical","lifestyle","swatch"]},"position":{"type":"number"},"altText":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"channelId":{"type":"string","format":"uuid","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","mediaId","entityType","entityId","role","position","altText","channelId"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/media/{id}":{"delete":{"summary":"Delete media (object + row)","tags":["media"],"description":"Emits `media.deleted`.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/me":{"get":{"summary":"Identity of the authenticated principal","tags":["auth"],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalProvider":{"type":"string"},"externalSubject":{"type":"string"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","externalProvider","externalSubject","email","name","avatarUrl","createdAt","updatedAt"],"additionalProperties":false},"organizationId":{"type":"string","format":"uuid","nullable":true},"organizationName":{"type":"string","nullable":true},"roles":{"type":"array","items":{"type":"string"}},"source":{"type":"string"},"deployment":{"type":"object","properties":{"tenantMode":{"type":"string","enum":["single","multi"]},"bootstrapOrgId":{"type":"string","format":"uuid"}},"required":["tenantMode","bootstrapOrgId"],"additionalProperties":false}},"required":["user","organizationId","organizationName","roles","source","deployment"],"additionalProperties":false}}}}}},"patch":{"summary":"Update the authenticated user's own profile","tags":["auth"],"description":"Sets the display name on the MiniPim user row. In hosted (Clerk) mode the IdP can overwrite this on a later sign-in if the session token carries a `name` claim; identity-provided fields win over locally-set ones by design. Emits `user.updated` into the active organization, when there is one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200}},"required":["name"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalProvider":{"type":"string"},"externalSubject":{"type":"string"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","externalProvider","externalSubject","email","name","avatarUrl","createdAt","updatedAt"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/me/orgs":{"get":{"summary":"List organizations the current user belongs to","tags":["auth"],"description":"Drives the admin's org switcher. Returns each org's id, name, slug, and the roles this user holds in it. In single-tenant mode this is always one row (the bootstrap org).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"isCurrent":{"type":"boolean"}},"required":["organizationId","name","slug","roles","isCurrent"],"additionalProperties":false}}}}}}}},"/v1/members":{"get":{"summary":"List members of the current organization","tags":["auth"],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid"},"roles":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"user":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalProvider":{"type":"string"},"externalSubject":{"type":"string"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","externalProvider","externalSubject","email","name","avatarUrl","createdAt","updatedAt"],"additionalProperties":false}},"required":["organizationId","userId","roles","createdAt","updatedAt","user"],"additionalProperties":false}}}}}}},"post":{"summary":"Add a member by email","tags":["auth"],"description":"If the email already has an account, the user is added to the org immediately. Otherwise a pending invitation is created and (in hosted mode) an account invite email is sent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"roles":{"type":"array","items":{"type":"string"},"default":["member"]}},"required":["email"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["member","invited"]},"userId":{"type":"string","format":"uuid","nullable":true},"invitationId":{"type":"string","format":"uuid","nullable":true},"link":{"type":"string","nullable":true}},"required":["status","userId","invitationId","link"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/members/{id}":{"patch":{"summary":"Update a member's roles","tags":["auth"],"description":"The `:id` here is the member's `user_id`. Replaces the entire `roles` array.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"roles":{"type":"array","items":{"type":"string"},"minItems":0}},"required":["roles"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid"},"roles":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"user":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"externalProvider":{"type":"string"},"externalSubject":{"type":"string"},"email":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"avatarUrl":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","externalProvider","externalSubject","email","name","avatarUrl","createdAt","updatedAt"],"additionalProperties":false}},"required":["organizationId","userId","roles","createdAt","updatedAt","user"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Remove a member from the current organization","tags":["auth"],"description":"The `:id` is the member's `user_id`. Deletes the membership row only — the user account and their memberships in other organizations are untouched. The last remaining admin cannot be removed (an org must never end up admin-less); demote/remove another admin first, or delete the organization instead.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/invitations":{"get":{"summary":"List invitations for the current organization","tags":["auth"],"description":"Returns pending invites by default; accepted/revoked/expired ones are included so admins can audit history.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"token":{"type":"string"},"email":{"type":"string","nullable":true},"roles":{"type":"array","items":{"type":"string"}},"invitedBy":{"type":"string","format":"uuid","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"acceptedByUserId":{"type":"string","format":"uuid","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["id","organizationId","token","email","roles","invitedBy","expiresAt","acceptedAt","acceptedByUserId","revokedAt","createdAt"],"additionalProperties":false}}}}}}},"post":{"summary":"Create an invitation","tags":["auth"],"description":"Returns the invite row including the `token`. Build the accept URL on the client (e.g. `https://admin.example.com/accept-invite?token=…`) and share it however you want — email, Slack, sticky note.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"roles":{"type":"array","items":{"type":"string"},"default":[]},"expiresInDays":{"type":"integer","minimum":1,"maximum":365}},"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"token":{"type":"string"},"email":{"type":"string","nullable":true},"roles":{"type":"array","items":{"type":"string"}},"invitedBy":{"type":"string","format":"uuid","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"acceptedByUserId":{"type":"string","format":"uuid","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["id","organizationId","token","email","roles","invitedBy","expiresAt","acceptedAt","acceptedByUserId","revokedAt","createdAt"],"additionalProperties":false}}}}}}},"/v1/invitations/{id}/revoke":{"post":{"summary":"Revoke an unaccepted invitation","tags":["auth"],"description":"Sets `revoked_at`. Already-accepted invites are no-ops.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"token":{"type":"string"},"email":{"type":"string","nullable":true},"roles":{"type":"array","items":{"type":"string"}},"invitedBy":{"type":"string","format":"uuid","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"acceptedAt":{"type":"string","format":"date-time","nullable":true},"acceptedByUserId":{"type":"string","format":"uuid","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["id","organizationId","token","email","roles","invitedBy","expiresAt","acceptedAt","acceptedByUserId","revokedAt","createdAt"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/invitations/{token}/accept":{"post":{"summary":"Accept an invitation token","tags":["auth"],"description":"Adds the authenticated user to the invite's organization with the invite's roles. An email-targeted invitation can only be accepted by a user whose verified email matches; an email-less (shareable-link) invitation can be accepted by anyone holding the link. Idempotent: re-accepting from the same user is a no-op.","parameters":[{"schema":{"type":"string","minLength":8},"in":"path","name":"token","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"organizationId":{"type":"string","format":"uuid"},"roles":{"type":"array","items":{"type":"string"}}},"required":["organizationId","roles"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/api-keys":{"get":{"summary":"List API keys for the current org","tags":["auth"],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdBy":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["id","organizationId","createdBy","name","prefix","scopes","lastUsedAt","expiresAt","revokedAt","createdAt"],"additionalProperties":false}}}}}}},"post":{"summary":"Create an API key (plaintext shown once)","tags":["auth"],"description":"The returned `key` is the only time the plaintext is exposed. Store it securely — only the sha256 hash is persisted server-side.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"scopes":{"type":"array","items":{"type":"string"},"default":["read","write"]},"expiresAt":{"type":"string","format":"date-time"}},"required":["name"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdBy":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"key":{"type":"string"}},"required":["id","organizationId","createdBy","name","prefix","scopes","lastUsedAt","expiresAt","revokedAt","createdAt","key"],"additionalProperties":false}}}}}}},"/v1/api-keys/{id}":{"patch":{"summary":"Rename an API key","tags":["auth"],"description":"Updates only the display name; scopes/expiry are immutable post-create.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1}},"required":["name"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdBy":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["id","organizationId","createdBy","name","prefix","scopes","lastUsedAt","expiresAt","revokedAt","createdAt"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/api-keys/{id}/revoke":{"post":{"summary":"Revoke an API key","tags":["auth"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdBy":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"lastUsedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"revokedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["id","organizationId","createdBy","name","prefix","scopes","lastUsedAt","expiresAt","revokedAt","createdAt"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/available":{"get":{"summary":"List connector types available in this deployment","tags":["connectors"],"description":"OSS deployments list only `availability: oss` types; hosted lists both.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"availability":{"type":"string","enum":["oss","hosted"]},"capabilities":{"type":"array","items":{"type":"object","properties":{"entity":{"type":"string","enum":["product","variant","media","category","content_page","brand"]},"direction":{"type":"string","enum":["push","pull","bidirectional"]}},"required":["entity","direction"],"additionalProperties":false}}},"required":["type","availability","capabilities"],"additionalProperties":false}}}}}}}},"/v1/connectors":{"get":{"summary":"List installed connector instances for the current org","tags":["connectors"],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","error"]},"availability":{"type":"string","enum":["oss","hosted"]},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"secretsRef":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","type","name","status","availability","config","secretsRef"],"additionalProperties":false}}}}}}},"post":{"summary":"Install a connector instance","tags":["connectors"],"description":"Creates a `connectors` row, calls the implementation's `install()`, then `configure()`. Fails atomically — if configure rejects (bad credentials), the row is removed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"config":{"type":"object","additionalProperties":{},"default":{}},"secretsRef":{"type":"string"}},"required":["type","name"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","error"]},"availability":{"type":"string","enum":["oss","hosted"]},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"secretsRef":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","type","name","status","availability","config","secretsRef"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/{id}":{"get":{"summary":"Get an installed connector","tags":["connectors"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","error"]},"availability":{"type":"string","enum":["oss","hosted"]},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"secretsRef":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","type","name","status","availability","config","secretsRef"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"summary":"Update connector config / name / secretsRef","tags":["connectors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"config":{"type":"object","additionalProperties":{}},"secretsRef":{"type":"string","nullable":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["active","paused","error"]},"availability":{"type":"string","enum":["oss","hosted"]},"config":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"secretsRef":{"type":"string","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","type","name","status","availability","config","secretsRef"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"summary":"Uninstall a connector","tags":["connectors"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/{id}/sync":{"post":{"summary":"Trigger a sync (pull / push / both)","tags":["connectors"],"description":"Asynchronous: returns the sync record immediately with status `running`; the work continues in the background. Poll `GET /v1/connectors/{id}/syncs` for the terminal status (`completed`/`partial`/`failed`) and stats. Pulls download media binaries and routinely take minutes — running in-request exceeded proxy timeouts, which severed responses mid-sync and invited retry double-pulls. Caveat: a server restart mid-sync leaves the row `running` forever; a BullMQ worker will make this durable later.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"direction":{"type":"string","enum":["pull","push","both"],"default":"pull"},"dryRun":{"type":"boolean","default":false},"since":{"type":"string","format":"date-time"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"connectorId":{"type":"string","format":"uuid"},"direction":{"type":"string","enum":["push","pull","bidirectional"]},"trigger":{"type":"string","enum":["manual","scheduled","event"]},"status":{"type":"string","enum":["queued","running","completed","failed","partial"]},"stats":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"error":{"type":"string","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","connectorId","direction","trigger","status","stats","error","startedAt","completedAt"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/catalog/wipe":{"post":{"summary":"Delete all catalog data for the current organization","tags":["connectors"],"description":"Destructive. Removes products, variants, identifiers, media, categories, brands, content pages, modifiers, attributes, families, groups, channels and locales for this org. Connectors, sync history, users, members, API keys, and audit log are preserved. Body must include `{ confirm: \"WIPE\" }` to proceed. Emits `catalog.wiped`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"confirm":{"type":"string","enum":["WIPE"]}},"required":["confirm"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"wiped":{"type":"object","properties":{"products":{"type":"integer"},"categories":{"type":"integer"},"brands":{"type":"integer"},"contentPages":{"type":"integer"},"modifiers":{"type":"integer"},"media":{"type":"integer"},"attributes":{"type":"integer"},"attributeFamilies":{"type":"integer"},"attributeGroups":{"type":"integer"},"channels":{"type":"integer"},"locales":{"type":"integer"}},"required":["products","categories","brands","contentPages","modifiers","media","attributes","attributeFamilies","attributeGroups","channels","locales"],"additionalProperties":false}},"required":["wiped"],"additionalProperties":false}}}}}}},"/v1/{entityType}/{entityId}/connector-metadata":{"get":{"summary":"Read connector metadata for an entity","tags":["connectors"],"description":"Returns a flat `{ key: value }` map of all connector_metadata rows attached to this product / variant across all connectors. Used by the admin to surface things like `bc_channels` on the product detail page.","parameters":[{"schema":{"type":"string","enum":["products","variants"]},"in":"path","name":"entityType","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"entityId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}}}}},"/v1/oauth-apps/shopify":{"get":{"summary":"Read Shopify App OAuth config (non-secret fields)","tags":["connectors"],"description":"Returns whether Shopify App OAuth is configured + the public client id and scopes. NEVER exposes the client secret. Source = env > app_settings > unset.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"configured":{"type":"boolean"},"source":{"type":"string","enum":["env","db","unset"]},"client_id":{"type":"string","nullable":true},"scopes":{"type":"string","nullable":true},"callback_url":{"type":"string","format":"uri"}},"required":["configured","source","client_id","scopes","callback_url"],"additionalProperties":false}}}}}},"put":{"summary":"Save Shopify App OAuth credentials","tags":["connectors"],"description":"Stores client_id + base64-encoded client_secret + scopes in app_settings. Takes effect immediately (no server restart). Env vars (if set) still win. Emits `oauth_app.updated`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string","minLength":1},"client_secret":{"type":"string","minLength":1},"scopes":{"type":"string","default":"read_products,write_products,read_product_listings"}},"required":["client_id","client_secret"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"source":{"type":"string","enum":["env","db"]}},"required":["ok","source"],"additionalProperties":false}}}}}},"delete":{"summary":"Clear stored Shopify App OAuth credentials","tags":["connectors"],"description":"Removes the DB row. Env vars (if set) still take effect. Emits `oauth_app.deleted`.","responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}}}}},"/v1/connectors/shopify/oauth/start":{"post":{"summary":"Begin Shopify App OAuth install","tags":["connectors"],"description":"Returns a URL the browser should redirect to (Shopify's authorize page). After the merchant authorizes, Shopify redirects back to the callback, which creates the connector row and stores the access token.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"shop_domain":{"allOf":[{"type":"string"},{"type":"string","pattern":"^[a-z0-9-]+\\.myshopify\\.com$"}]},"name":{"type":"string","minLength":1,"default":"Shopify"}},"required":["shop_domain"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"authorize_url":{"type":"string","format":"uri"}},"required":["authorize_url"],"additionalProperties":false}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/shopify/oauth/callback":{"get":{"summary":"Shopify OAuth callback","tags":["connectors"],"description":"Validates HMAC + state, exchanges code for access token, creates the connector, redirects to admin.","parameters":[{"schema":{"type":"string"},"in":"query","name":"code","required":true},{"schema":{"type":"string"},"in":"query","name":"shop","required":true},{"schema":{"type":"string"},"in":"query","name":"state","required":true},{"schema":{"type":"string"},"in":"query","name":"hmac","required":true},{"schema":{"type":"string"},"in":"query","name":"timestamp","required":false},{"schema":{"type":"string"},"in":"query","name":"host","required":false}],"responses":{"302":{"description":"Default Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/{id}/syncs":{"get":{"summary":"Sync history for a connector","tags":["connectors"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"connectorId":{"type":"string","format":"uuid"},"direction":{"type":"string","enum":["push","pull","bidirectional"]},"trigger":{"type":"string","enum":["manual","scheduled","event"]},"status":{"type":"string","enum":["queued","running","completed","failed","partial"]},"stats":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]},"error":{"type":"string","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true}},"required":["id","organizationId","createdAt","updatedAt","connectorId","direction","trigger","status","stats","error","startedAt","completedAt"],"additionalProperties":false}}}}}}}},"/v1/connectors/{id}/deliveries":{"get":{"summary":"Recent outbound webhook deliveries for a connector","tags":["connectors"],"description":"Last 50 delivery attempts (event name, HTTP status, ok, error) for push connectors that POST events to a destination (e.g. headless). Answers \"did any events actually arrive?\" without grepping receiver logs.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"eventName":{"type":"string"},"status":{"type":"integer","nullable":true},"ok":{"type":"boolean"},"error":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}},"required":["id","eventName","status","ok","error","createdAt"],"additionalProperties":false}}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/{id}/webhooks/test":{"post":{"summary":"Send a signed test event to the connector destination","tags":["connectors"],"description":"Delivers a synthetic `webhook.test` event (HMAC-signed like any real delivery) so integrators can verify endpoint wiring without mutating catalog data. Returns the delivery outcome and logs it like any other.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"status":{"type":"integer","nullable":true},"error":{"type":"string"}},"required":["ok","status"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/{id}/webhooks/enable":{"post":{"summary":"Enable inbound webhooks for this connector","tags":["connectors"],"description":"Generates a random per-connector secret. If the connector implements `subscribeWebhooks` AND a public URL is configured, also registers the subscription with the upstream platform. Otherwise returns the URL for the user to register manually. Emits `connector.updated` (`webhooks`).","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"secret":{"type":"string"},"subscribed":{"type":"boolean"},"note":{"type":"string"},"subscriptionIds":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"number"}]}}},"required":["url","secret","subscribed"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/{id}/webhooks/disable":{"post":{"summary":"Disable inbound webhooks for this connector","tags":["connectors"],"description":"Removes any platform-side subscriptions we registered and clears the local webhook secret. Best-effort on the platform side — we log and continue if a subscription fails to delete. Emits `connector.updated` (`webhooks`).","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/connectors/{id}/webhooks/{secret}":{"post":{"summary":"Inbound webhook receiver (platform → MiniPim)","tags":["connectors"],"description":"Accepts a JSON payload from the external platform (e.g. BigCommerce store/product/updated). The connector's `handleWebhook` decides what to do; typically: refetch the affected product and run it through the upsert pipeline. Emits `connector.webhook.received` once the secret checks out.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true},{"schema":{"type":"string","minLength":8},"in":"path","name":"secret","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"result":{"type":"object","additionalProperties":{}}},"required":["ok"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/products/{id}/relationships":{"get":{"summary":"List a product’s relationships","tags":["products"],"description":"Outbound by default (`source = :id`). Use direction=inbound or both to include rows where this product is the target.","parameters":[{"schema":{"type":"string","enum":["cross_sell","up_sell","related","accessory","replacement","bundle_component"]},"in":"query","name":"type","required":false},{"schema":{"type":"string","enum":["outbound","inbound","both"],"default":"outbound"},"in":"query","name":"direction","required":false},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"sourceProductId":{"type":"string","format":"uuid"},"targetProductId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["cross_sell","up_sell","related","accessory","replacement","bundle_component"]},"position":{"type":"number"},"metadata":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","sourceProductId","targetProductId","type","position","metadata"],"additionalProperties":false}}}}}}},"post":{"summary":"Create a relationship from this product to another","tags":["products"],"description":"Emits `product_relationship.created`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"targetProductId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["cross_sell","up_sell","related","accessory","replacement","bundle_component"]},"position":{"type":"integer","default":0},"metadata":{"type":"object","additionalProperties":{},"nullable":true}},"required":["targetProductId","type"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"sourceProductId":{"type":"string","format":"uuid"},"targetProductId":{"type":"string","format":"uuid"},"type":{"type":"string","enum":["cross_sell","up_sell","related","accessory","replacement","bundle_component"]},"position":{"type":"number"},"metadata":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true}},"required":["id","organizationId","createdAt","updatedAt","sourceProductId","targetProductId","type","position","metadata"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/relationships/{id}":{"delete":{"summary":"Delete a relationship","tags":["products"],"description":"Emits `product_relationship.deleted`.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v1/audit-log":{"get":{"summary":"Query the audit log","tags":["audit"],"description":"Returns audit entries newest-first. Filter by entity_type, entity_id, action, and `since`.","parameters":[{"schema":{"type":"string"},"in":"query","name":"entity_type","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"entity_id","required":false},{"schema":{"type":"string","enum":["create","update","delete"]},"in":"query","name":"action","required":false},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"since","required":false},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","minimum":0,"default":0},"in":"query","name":"offset","required":false}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"entityType":{"type":"string"},"entityId":{"type":"string","format":"uuid"},"action":{"type":"string","enum":["create","update","delete"]},"actorId":{"type":"string","format":"uuid","nullable":true},"actorType":{"type":"string","enum":["user","api_key","connector","system"]},"changes":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]},{"type":"array","items":{}},{"type":"object","additionalProperties":{}}],"nullable":true},"createdAt":{"type":"string","format":"date-time"},"actorLabel":{"type":"string","nullable":true,"description":"Display name for the actor: the user’s name or email for `user` rows, the connector’s name for `connector` rows, null when the actor is unattributed (`system`) or no longer resolvable."}},"required":["id","organizationId","entityType","entityId","action","actorId","actorType","changes","createdAt","actorLabel"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"},"hasMore":{"type":"boolean"}},"required":["data","limit","offset","hasMore"],"additionalProperties":false}}}}}}}},"servers":[{"url":"https://api.minipim.com","description":"this deployment"}],"security":[{"organizationHeader":[]}],"tags":[{"name":"health","description":"Liveness + readiness probes"},{"name":"organizations","description":"Tenant management"},{"name":"attributes","description":"Attribute schema — groups, definitions, families"},{"name":"products","description":"Products, variants, identifiers"},{"name":"taxonomy","description":"Channels, locales, categories"},{"name":"modifiers","description":"Order-line options (engraving, gift wrap, etc.) — no new SKUs"},{"name":"media","description":"Media (images, video) attached to products / variants"},{"name":"auth","description":"API keys + authenticated principal"},{"name":"connectors","description":"Inbound + outbound platform connectors"},{"name":"audit","description":"Mutation history per entity"}]}