# PVM MCP Protocol

Status: deployed (supplier layer, product metadata, taxonomy CRUD/status, comments with presigned attachments + retention cleanup, standard kits, configurable kits with slots/choices/constraint rules/price deltas, alternatives, supplementary links, status history, comment abuse reporting, and Brand). Docs/MCP are published (see [/common/start-here.html](https://doc.g3nretailstack.com/common/start-here.html) for the latest build ID). This document is contract-only; no infra/alarms/storage details. Canonical host: <a href="https://mcp.g3nretailstack.com/pvm/PROTOCOL.md" target="_blank" rel="noopener noreferrer">https://mcp.g3nretailstack.com/pvm/PROTOCOL.md</a> (mirrored to docs at <a href="https://doc.g3nretailstack.com/pvm/PROTOCOL.md" target="_blank" rel="noopener noreferrer">/pvm/PROTOCOL.md</a>).


## Usage patterns (headless)
- Stack-wide SOPs & operations catalog: <a href="https://doc.g3nretailstack.com/story/operations.html" target="_blank" rel="noopener noreferrer">/story/operations.html</a>.
- Super-usecase scenarios + QA status: <a href="https://doc.g3nretailstack.com/story/super-usecases.html" target="_blank" rel="noopener noreferrer">/story/super-usecases.html</a>.
- This protocol stays contract-only; use the catalogs for workflow expectations.

## Base URL
- API Gateway: `https://api.g3nretailstack.com/pvm`
- Health check: `GET /pvm/stat` — public, no auth required.
- MCP protocol: <a href="https://mcp.g3nretailstack.com/pvm/PROTOCOL.md" target="_blank" rel="noopener noreferrer">https://mcp.g3nretailstack.com/pvm/PROTOCOL.md</a>

## Auth, tenancy, pagination, roles
- Auth placement: header auth is canonical for org-scoped APIs; body auth is accepted for compatibility where documented. Exceptions: USM and UTL require body auth. See [/common/headers-identity.html](https://doc.g3nretailstack.com/common/headers-identity.html).
- Every tenant call requires `x-orgcode`.
- Auth is either:
  - `x-session-guid` (user session), OR
  - `x-api-key` (org-bound service account)
- Non-associated callers receive `404 not-found` (anti-enumeration).
- `session_guid` is never emitted in responses; use `stats.session_fingerprint` for correlation.
- Roles: org owners full access. Members need (resolved via OFM `member/resolve` using the session + orgcode):
  - Product Model Administrator → all PVM mutations (styles/variants/OGM/identifiers/barcodes/aliases/kits/links/comments status).
  - Vendor Contract Administrator → supplier ops (vendors/manufacturers).
  - Product and Vendor Viewer → read/comment-only.
- Optional cost attribution: provide `x-cccode` (or request field `cccode`). Format `^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$`, canonicalized to uppercase. Invalid values (or body/header mismatch) return HTTP 400. (PVM currently treats this as telemetry-only passthrough; it does not change PVM authorization or data scope.)
- Pagination: default `limit` 8 (clamp 1–256); `next_token` opaque cursor.
- List endpoints return summary fields only (reduced payload/RCU). Use the matching `*/get` endpoint for full record detail.
- List search/ordering: list endpoints accept optional `text` (≤96) for code/caption search (`GET /style`, `GET /variant/list`, suppliers, brand, option groups/options, taxonomy, OGM). Supplier/brand/option/taxonomy `text` uses the tokenized search plane (eventually consistent); orgs can opt into OpenSearch via `search_plane.pvm=opensearch`. When `text` is code-like, it is treated as a prefix search on `code` where the list sort key supports it and results are ordered by `code` (asc). Otherwise, `text` is a fuzzy contains match on `code`/`caption` (variants also match `sku`). Category ordering is parent+code; option list scoped to a group orders by option_id.
- Codes: `^[A-Z][A-Z0-9_-]{0,9}$`, immutable. Aliases up to 16 per record, `{tag ≤24 [A-Za-z0-9_.-], value ≤96}`, unique tag/value per org. IDs are opaque 16-char base36 (uppercase). Revisions are GUIDs.
- Code generation (create): for create endpoints that accept `code`, the request may omit `code` and instead provide `code_pattern?` (≤10 chars, `A-Z 0-9 _ - ?`, `?` = wildcard) plus optional `code_max_attempts?` (int 1–64). On exhaustion: `code-generation-exhausted` (HTTP 409).
- Status: active↔inactive; any → doomed (terminal). Edits only while inactive. Style doom is blocked while non-doomed variants exist. Variant doom is blocked while outbound kit components/slots/alternatives/supplementary links exist, and while active inbound links exist.

## Core rules
- Record revisions: stateful records carry a per-record `revision` GUID. Mutations that update an existing revisioned record require `expected_revision`; missing → `expected-revision-required` (HTTP 428), mismatch → `conflict` (HTTP 409) with the current record snapshot + current `revision`.
- Org currency + FX (template econ): each org has an immutable `org_currency` (stored internally by PVM). It is established the first time a `TemplateEcon` is written for the org: either from `template_econ.currency` (when `fx` is omitted) or from `template_econ.fx.org_currency` (when `fx` is provided). Rules for `template_econ` and `template_overrides.econ`:
  - If any monetary fields are set, `currency` is required.
  - If `currency == org_currency`: `fx` must be omitted.
  - If `currency != org_currency`: `fx` is required and must include `{ org_currency, rate_to_org, as_of_utc }` (and `org_currency` must match the org’s configured currency).
- Suppliers: style requires ≥1 vendor AND ≥1 manufacturer with primaries, plus a valid non-doomed taxonomy `category_id`. Optional `brand_id`; when present, the brand must be linked to **all** referenced `vendor_ids` and `manufacturer_ids` (see Brand links). When a brand has supplier links and is set to `status=active`, it must have primary vendor/manufacturer links set via `POST /brand/link/set_primary`. `vendor_ids[]` and `manufacturer_ids[]` are capped at 128 each; if a write would exceed the cap, the oldest non-primary IDs are evicted (primary is never evicted). Supplier-scoped option groups must match style suppliers.
- Taxonomy: Category is a tree inside a department; parent must be in the same department. Enforced: acyclic, max depth 16. Dooming a division/department/category is blocked while it still has non-doomed children.
- OGM: revisioned and immutable per rev. Edits require clone; style switches via `style/ogm/set`. `ogm.groups[]` defines required option group codes and their priority order (used for variant signature ordering). If `ogm.groups[]` is empty, then `variant/create` requires empty selections and signature is empty (so only one non-doomed variant can exist per style).
- Defaults (template/UOM/lifecycle): OGM revisions can carry default fields (`template_econ`, `tax_hints`, `compliance`, `uom_dims_defaults`, `lifecycle_defaults`). Style creation inherits from the referenced OGM (and fills missing keys from OGM when defaults are provided partially); variant creation inherits from style (with `template_overrides` and `lifecycle` as per-variant overlays).
  - UOM dims: `base_uom` required; `sell_uom` defaults to `ea`; `conversion_factor` defaults to 1 when `sell_uom==base_uom` and is required otherwise. If `control_type` is `serial`/`both`, then `serial_mode` is required.
  - Dimensions/weight: canonical mm/g (ints). `dims` accepts either `*_mm` or `{ length/width/height + uom }`; weights accept either `*_weight_g` or `{ weight + weight_uom }`. Optional `source_uom` / `source_weight_uom` fields preserve original units when converting (and are validated when provided alongside canonical fields). Shipping unit is optional via `shipping_dims` / `shipping_weight_g` and preserves `shipping_source_weight_uom`; optional `package_levels` supports per-packaging-level dims/weight (e.g., case/pallet) and preserves `source_weight_uom`.
  - Compliance overlay: `template_overrides.compliance` extends nested collections: `certifications[]` are unioned and `attributes{}` keys are merged (variant values win per-key). Objects like `battery`, `age_restriction`, `recall`, `stop_sale`, `restricted_distribution`, `delivery_constraints`, `allergens`, `ingredients`, and `labels_by_market` are merged by defined keys (labels are merged per market key). Other compliance fields override normally.
- Variant sellable-now: variant responses include derived boolean `is_sellable_now` (status must be `active` and pass lifecycle/compliance gating). `recall.status=active` or `stop_sale.status=active` forces `is_sellable_now=false`.
  - Service UOM constraint: if `service_flag=true`, then `uom_dims.base_uom` and `uom_dims.sell_uom` must be `service_unit` or `hour` (and conversely, using service UOMs requires `service_flag=true`).
  - Template strictness: unknown keys in `template_econ`/`tax_hints`/`compliance`/`uom_dims`/`lifecycle` (and nested objects like `template_econ.fx`, `uom_dims.dims`) are rejected. Hazmat: if `compliance.is_hazmat=true`, then `compliance.un_number` and `compliance.hazard_class` are required. Recall/stop-sale: `status=active` requires `reason` + `started_at`; `status=cleared` requires `cleared_at`.
- Variants: selections are resolved/validated against Option Group/Option records (by `group_code` + `option_code`). Signature is built in OGM group order (`GROUP=OPTION|...`), unique among non-doomed variants per style (freed on doom). `selections[].size` may include `{ waist, inseam, cup, width }` (ignored for signature) and selections are persisted on the variant record. Size values are stored as-provided (no unit conversion); interpretation is domain-specific. Variant edits inactive-only.
- Identifiers/Aliases: uniqueness across non-doomed variants; changes recorded in history. Identifier transfer explicitly moves ownership and requires `reason`.
- Barcodes: digits-only with valid check digit; `(org, value)` unique among non-doomed. Reuse only if prior value inactive/doomed AND `allow_reuse=true` + `reason` (reason required when reassigning to a different variant); otherwise 409. One primary per `packaging_level`. `issued_by` ∈ {gs1, vendor, org, unknown}.
- Comments: immutable body; attachments ≤128 MB each with filename/content_type/size_bytes/caption, uploaded via presigned S3 URLs. Status {current, archived, doomed}. Attachment retention/cleanup defaults: current 90d, archived 30d, doomed 7d. Reporting endpoint `/comment/report` lists the largest comments by attachment size (no backfill).
- Standard Kits: kit components carry qty/uom; edits allowed while source variant inactive. Set `kit_flag=true` on variant create.
- Configurable Kits: set `kit_type=configurable` on variant create (auto-sets `kit_flag=true`). Define slots with qty ranges and homogeneous flags, then add variant choices to each slot. Constraint rules (include/exclude/qty_override/require) restrict choices based on other slot selections. Price deltas on choices adjust kit pricing. Optional `pricing_mode` (base_plus_delta|component_sum) on the kit variant. Nested configurable kits supported up to depth 3.
  - Slot CRUD: `POST /kit/slot/add`, `POST /kit/slot/remove`, `GET /kit/slot/list`. Kit must be `kit_type=configurable` and `status=inactive`.
  - Choice CRUD: `POST /kit/slot/choice/add`, `POST /kit/slot/choice/remove`, `GET /kit/slot/choice/list`. Choice variants must not be doomed. Choices create inbound pointers (doom blocking).
  - Constraint Rules: `POST /kit/rule/add`, `POST /kit/rule/remove`, `GET /kit/rule/list`. Rule types: `include` (intersect available choices), `exclude` (remove from available), `qty_override` (change min/max), `require` (force min_qty≥1). Rules are priority-sorted; evaluated when a trigger slot+variant is selected.
  - Validation: `POST /kit/configure/validate` — read-only. Input: `{ kit_variant_id, configuration: { slot_code: [{ variant_id, qty }] } }`. Returns `{ valid, errors[], available_choices, effective_qty }`. When constraint rules exist, returns post-rule `available_choices` and `effective_qty`.
- Links: alternatives/supplementary are variant→variant links with priority/time windows; edits allowed while source variant inactive.

## Surfaces (API Gateway, POST unless noted)
- Taxonomy: `/division` (POST create, GET list), `/division/get` (GET), `/division/update` (POST), `/division/status` (POST); `/department` (POST create, GET list), `/department/get` (GET), `/department/update` (POST), `/department/status` (POST); `/category` (POST create, GET list), `/category/get` (GET), `/category/update` (POST), `/category/status` (POST); `/season` (POST create, GET list), `/season/get` (GET), `/season/update` (POST), `/season/status` (POST).
- Suppliers: `/vendor` (POST create, GET list), `/vendor/get` (GET), `/vendor/update` (POST), `/vendor/status` (POST); `/manufacturer` (POST create, GET list), `/manufacturer/get` (GET), `/manufacturer/update` (POST), `/manufacturer/status` (POST).
- Brand: `/brand` (POST create, GET list), `/brand/get` (GET), `/brand/update` (POST), `/brand/status` (POST).
- Brand links: `POST /brand/link/add`, `POST /brand/link/remove`, `POST /brand/link/set_primary`, `GET /brand/link/list` (list by `brand_id` with optional `supplier_type` filter, or list by `supplier_type+supplier_id`).
- Option groups: `/option_group` (POST create, GET list), `/option_group/get` (GET), `/option_group/update` (POST), `/option_group/status` (POST).
- Options: `/option` (POST create, GET list), `/option/get` (GET), `/option/update` (POST), `/option/status` (POST).
- Style: `/style` (create), `/style/get` (GET), `/style` (GET list), `/style/update`, `/style/status`, `/style/ogm/set`.
- OGM: `/ogm` (create), `/ogm/clone`, `/ogm/get` (GET), `/ogm/list` (GET), `/ogm/status`.
- Variant: `/variant` (create), `/variant/list` (GET), `/variant/get` (GET), `/variant/status`, `/variant/stale/list` (GET), `/variant/recreate`, plus `POST /variant/update` (inactive-only update).
- Identifiers/Aliases: `/identifier/add`, `/identifier/transfer`, `/identifier/history` (GET), `/identifier/resolve` (GET), `/identifier/list` (GET); `/alias/set`, `/alias/remove`, `/alias/list` (GET), `/resolve/alias` (GET).
- Barcodes: `/barcode/add`, `/barcode/set_primary`, `/barcode/status`, `/barcode/list` (GET), `/barcode/get` (GET), `/barcode/resolve` (GET), `/resolve/barcode` (GET).
- Resolve/Code: `/resolve/code` (GET) resolves codes across multiple entities (priority order) and returns `{ code, target_type, target_id }`.
- History: `GET /history/status` lists status transitions for styles/variants (no backfill).
- Comments: `/comment` (add), `/comment/list` (GET), `/comment/status` (archive/doomed), plus `GET /comment/report` (no backfill).
- Standard Kits: `/kit/component/add` (POST), `/kit/component/remove` (POST), `/kit/component/list` (GET).
- Configurable Kits: `/kit/slot/add` (POST), `/kit/slot/remove` (POST), `/kit/slot/list` (GET); `/kit/slot/choice/add` (POST), `/kit/slot/choice/remove` (POST), `/kit/slot/choice/list` (GET); `/kit/configure/validate` (POST); `/kit/rule/add` (POST), `/kit/rule/remove` (POST), `/kit/rule/list` (GET).
- Links: `/alternative/add` (POST), `/alternative/status` (POST), `/alternative/list` (GET); `/supplementary/add` (POST), `/supplementary/status` (POST), `/supplementary/list` (GET).
- Product search: `POST /product/search` — full-text search across styles/variants (requires search plane configuration; returns matching items with pagination).
- Compliance check: `POST /product/compliance/check` — batch compliance/sellability check for up to 50 variants (returns `is_sellable_now`, recall/stop-sale status, and market-specific restrictions per variant).
- Product recommendations: `POST /product/recommend` — returns recommendation matches for a set of variant IDs based on active recommendation rules (filterable by `rule_type`).
- Recommendation rules: `POST /recommendation/rule/set` (create/update), `GET /recommendation/rule/get`, `GET /recommendation/rule/list`, `POST /recommendation/rule/status` — CRUD and status management for org-scoped recommendation rules (cross-sell, upsell, bundle, accessory, complementary, seasonal).

## Endpoint inventory (OpenAPI parity)
The endpoints below are implemented and defined in `/pvm/openapi.yaml`. Request/response schema names reference OpenAPI component schemas.

| Method | Path | Request schema | Response schema |
| --- | --- | --- | --- |
| POST | /alias/remove | (inline) | Envelope + AliasState |
| POST | /alias/set | (inline) | Envelope + AliasState |
| GET | /alias/list | (inline) | Envelope + AliasListResult |
| POST | /alternative/add | (inline) | Envelope + AlternativeLink |
| GET | /alternative/list | (inline) | Envelope |
| POST | /alternative/status | (inline) | Envelope + AlternativeLink |
| POST | /barcode/add | (inline) | Envelope + Barcode |
| GET | /barcode/get | (inline) | Envelope + Barcode |
| GET | /barcode/list | (inline) | Envelope |
| GET | /barcode/resolve | (inline) | Envelope + BarcodeResolution |
| POST | /barcode/set_primary | (inline) | Envelope + BarcodePrimaryResult |
| POST | /barcode/status | (inline) | Envelope + Barcode |
| GET | /brand | (inline) | Envelope |
| POST | /brand | (inline) | Envelope + Brand |
| GET | /brand/get | (inline) | Envelope + Brand |
| POST | /brand/link/add | (inline) | Envelope + BrandLink |
| GET | /brand/link/list | (inline) | Envelope |
| POST | /brand/link/remove | (inline) | Envelope |
| POST | /brand/link/set_primary | (inline) | Envelope + BrandPrimarySupplier |
| POST | /brand/status | (inline) | Envelope + Brand |
| POST | /brand/update | (inline) | Envelope + Brand |
| GET | /category | (inline) | Envelope |
| POST | /category | (inline) | Envelope + Category |
| GET | /category/get | (inline) | Envelope + Category |
| POST | /category/status | (inline) | Envelope + Category |
| POST | /category/update | (inline) | Envelope + Category |
| POST | /comment | (inline) | Envelope |
| GET | /comment/list | (inline) | Envelope |
| GET | /comment/report | (inline) | Envelope |
| POST | /comment/status | (inline) | Envelope + Comment |
| GET | /department | (inline) | Envelope |
| POST | /department | (inline) | Envelope + Department |
| GET | /department/get | (inline) | Envelope + Department |
| POST | /department/status | (inline) | Envelope + Department |
| POST | /department/update | (inline) | Envelope + Department |
| GET | /division | (inline) | Envelope |
| POST | /division | (inline) | Envelope + Division |
| GET | /division/get | (inline) | Envelope + Division |
| POST | /division/status | (inline) | Envelope + Division |
| POST | /division/update | (inline) | Envelope + Division |
| GET | /history/status | (inline) | Envelope |
| POST | /identifier/add | (inline) | Envelope + IdentifierState |
| GET | /identifier/history | (inline) | Envelope |
| GET | /identifier/list | (inline) | Envelope |
| GET | /identifier/resolve | (inline) | Envelope + IdentifierResolution |
| POST | /identifier/transfer | (inline) | Envelope + IdentifierTransferResult |
| POST | /kit/component/add | (inline) | Envelope + KitComponent |
| GET | /kit/component/list | (inline) | Envelope |
| POST | /kit/component/remove | (inline) | Envelope |
| POST | /kit/configure/validate | (inline) | Envelope |
| POST | /kit/rule/add | (inline) | Envelope |
| GET | /kit/rule/list | (inline) | Envelope |
| POST | /kit/rule/remove | (inline) | Envelope |
| POST | /kit/slot/add | (inline) | Envelope |
| POST | /kit/slot/choice/add | (inline) | Envelope |
| GET | /kit/slot/choice/list | (inline) | Envelope |
| POST | /kit/slot/choice/remove | (inline) | Envelope |
| GET | /kit/slot/list | (inline) | Envelope |
| POST | /kit/slot/remove | (inline) | Envelope |
| GET | /manufacturer | (inline) | Envelope |
| POST | /manufacturer | (inline) | Envelope + Manufacturer |
| GET | /manufacturer/get | (inline) | Envelope + Manufacturer |
| POST | /manufacturer/status | (inline) | Envelope + Manufacturer |
| POST | /manufacturer/update | (inline) | Envelope + Manufacturer |
| POST | /ogm | (inline) | Envelope + Ogm |
| POST | /ogm/clone | (inline) | Envelope + Ogm |
| GET | /ogm/get | (inline) | Envelope + Ogm |
| GET | /ogm/list | (inline) | Envelope |
| POST | /ogm/status | (inline) | Envelope + Ogm |
| GET | /option | (inline) | Envelope |
| POST | /option | (inline) | Envelope |
| GET | /option/get | (inline) | Envelope |
| POST | /option/status | (inline) | Envelope |
| POST | /option/update | (inline) | Envelope |
| GET | /option_group | (inline) | Envelope |
| POST | /option_group | (inline) | Envelope + OptionGroup |
| GET | /option_group/get | (inline) | Envelope |
| POST | /option_group/status | (inline) | Envelope |
| POST | /option_group/update | (inline) | Envelope |
| POST | /product/compliance/check | (inline) | Envelope |
| POST | /product/recommend | (inline) | Envelope |
| POST | /product/search | (inline) | Envelope |
| GET | /recommendation/rule/get | (inline) | Envelope |
| GET | /recommendation/rule/list | (inline) | Envelope |
| POST | /recommendation/rule/set | (inline) | Envelope |
| POST | /recommendation/rule/status | (inline) | Envelope |
| GET | /resolve/alias | (inline) | Envelope + AliasResolution |
| GET | /resolve/barcode | (inline) | Envelope + BarcodeResolution |
| GET | /resolve/code | (inline) | Envelope + CodeResolution |
| GET | /season | (inline) | Envelope |
| POST | /season | (inline) | Envelope + Season |
| GET | /season/get | (inline) | Envelope + Season |
| POST | /season/status | (inline) | Envelope + Season |
| POST | /season/update | (inline) | Envelope + Season |
| GET | /stat | — | Envelope |
| GET | /style | (inline) | Envelope |
| POST | /style | (inline) | Envelope + Style |
| GET | /style/get | (inline) | Envelope + Style |
| POST | /style/ogm/set | (inline) | Envelope + StyleOgmUpdate |
| POST | /style/status | (inline) | Envelope + Style |
| POST | /style/update | (inline) | Envelope + Style |
| POST | /supplementary/add | (inline) | Envelope + SupplementaryLink |
| GET | /supplementary/list | (inline) | Envelope |
| POST | /supplementary/status | (inline) | Envelope + SupplementaryLink |
| POST | /variant | (inline) | Envelope + Variant |
| GET | /variant/get | (inline) | Envelope + Variant |
| GET | /variant/list | (inline) | Envelope |
| POST | /variant/recreate | (inline) | Envelope + VariantRecreateResult |
| GET | /variant/stale/list | (inline) | Envelope |
| POST | /variant/status | (inline) | Envelope + Variant |
| POST | /variant/update | (inline) | Envelope + Variant |
| GET | /vendor | (inline) | Envelope |
| POST | /vendor | (inline) | Envelope + Vendor |
| GET | /vendor/get | (inline) | Envelope + Vendor |
| POST | /vendor/status | (inline) | Envelope + Vendor |
| POST | /vendor/update | (inline) | Envelope + Vendor |

## CLI parity
- Direct API Gateway commands under `g3n pvm ...` mirror the surfaces above plus generic `pvm post/get`. Require `--session-guid` and `--orgcode`; base defaults to `https://api.g3nretailstack.com/pvm`.

## Request/response envelope
All responses use `{ success, data?, error?, stats, revision? }`. Build metadata (`{ build_major, build_minor, build_id }`) is inside `stats`. `revision` is the per-record revision GUID when a single stateful record is returned. Mutations that update an existing revisioned record require `expected_revision`; missing → `expected-revision-required` (HTTP 428), mismatch → `conflict` (HTTP 409) with the current record snapshot + current `revision`. Stats include call, service `pvm`, request_id, timestamp_utc, latency_ms, bandwidth in/out, and passthrough actor/session/user/org when provided. Errors carry tags/messages (e.g., `invalid-input`, `forbidden`, `not-found`, `conflict`, `invalid-state`, `invalid-check-digit`, `code-generation-exhausted`, `unauthorized`).

## Vendor / manufacturer FSM
- `unverified` → `verified` | `doomed`
- `verified` → `suspended` | `archived` | `doomed`
- `suspended` → `verified` | `doomed`
- `archived` → `verified` | `doomed`
- `doomed` → (terminal)

Both vendor and manufacturer share the same transition rules. Only `verified` suppliers can be linked to styles.

## Representative shapes (draft)
- OGM create: `{ code?, code_pattern?, code_max_attempts?, groups?, template_econ?, tax_hints?, compliance?, uom_dims_defaults?, lifecycle_defaults? }` → `{ ogm_id, ogm_rev, code, status=inactive }`
- Option group create: `{ code?, code_pattern?, code_max_attempts?, caption?, vendor_code?, manufacturer_code?, vendor_caption?, manufacturer_caption?, supplier_scope? }` → `{ option_group_id, code, status=inactive }` (response also includes `normalized_caption`)
- Option create: `{ code?, code_pattern?, code_max_attempts?, group_code|option_group_id, caption?, vendor_code?, manufacturer_code?, vendor_caption?, manufacturer_caption?, size? }` → `{ option_id, option_group_id, code, status=inactive }` (response also includes `normalized_caption`)
- Style create: `{ code?, code_pattern?, code_max_attempts?, category_id, brand_id?, vendor_ids[], manufacturer_ids[], primary_vendor_id, primary_manufacturer_id, ogm_id?, ogm_rev?, template_econ?, tax_hints?, compliance?, uom_dims_defaults?, lifecycle_defaults?, commercial_policy?, packaging_requirements?, seasons?, caption? }` → `{ style_id, code, status=inactive }` (supplier ID arrays are capped at 128 each; oldest non-primary evicted if exceeded)
- Style OGM set: `{ style_id, ogm_id, ogm_rev }` → `{ style_id, ogm_id, ogm_rev, previous_ogm_rev, stale_variant_ids[] }`
- Variant create: `{ style_id, code?, code_pattern?, code_max_attempts?, selections[] (group_code/option_code, optional size), template_overrides?, service_flag?, kit_flag?, kit_type? (standard|configurable), pricing_mode? (base_plus_delta|component_sum), lifecycle?, sku?, caption? }` → `{ variant_id, code, signature, status=inactive }`
- Variant recreate: `{ variant_id, target_ogm_rev }` → `{ source_variant_id, target_ogm_rev, variant }`
- Identifier transfer: `{ type, value, from_style_id, from_variant_id, to_style_id, to_variant_id, expected_revision, reason }` → transfer result + history entry.
- Barcode add: `{ style_id, variant_id, value, scheme?, packaging_level?, issued_by?, caption?, allow_reuse?, reason? }` → barcode record (reason required when `allow_reuse=true` and the value is being reassigned)
- Comment add: `{ target_type, target_id, text, parent_comment_id?, attachments[] }` → comment list for target.

## Roles
- Read (Product and Vendor Viewer): `pvv` / `Product and Vendor Viewer` (owner implied).
- Write (Product Model Administrator): `pma` / `Product Model Administrator`.
- Vendor management (Vendor Contract Administrator): `vca` / `Vendor Contract Administrator`.
- Owner operations: `owner` (org owner).
- Comment/inbox: same as read roles.

## Error tags
Service-specific tags: `invalid-input`, `forbidden`, `unauthorized`, `not-found`, `conflict`, `invalid-state`, `invalid-check-digit`, `code-generation-exhausted`.
Common tags (see [/common/error-tags.html](https://doc.g3nretailstack.com/common/error-tags.html)): `throttled`, `internal-error`, `expected-revision-required`.


## Idempotency & retries
- All **GET / list / resolve / search** calls are safe to retry with identical inputs (read-only, no side effects).
- **POST mutations** that accept `expected_revision` use optimistic concurrency: on `409 conflict` or `428 expected-revision-required`, re-read the record, obtain the current `revision`, and retry with the updated value.
- Creates are generally **not** idempotent. Prefer caller-provided `code` (where supported) and verify existence before retrying a failed create.
- Bulk or scheduled jobs that accept an `idempotency_key` will de-duplicate within the documented time window.

## Known pitfalls
- **Missing `expected_revision`**: most state-changing operations require it; omitting it returns `428` with the current revision in `error.details`.
- **Stale revision**: reading a record, waiting, then writing with an outdated `revision` triggers `409`. Always use the latest revision from the most recent read.
- **Pagination cursors**: `next_token` is opaque JSON. Do not modify, decode, or persist cursors across sessions — they may change format between deploys.
- **Anti-enumeration 404**: some org-scoped reads return `404` even when the record exists, if the caller is not associated with the org. Treat `404` as ambiguous; verify caller association before assuming "not found".

## OpenAPI
- Contract schema: <a href="https://doc.g3nretailstack.com/pvm/openapi.yaml" target="_blank" rel="noopener noreferrer">https://doc.g3nretailstack.com/pvm/openapi.yaml</a>
- History/status list: `{ target_type (style|variant), target_id, limit?, next_token? }` → transitions with actor/reason.

## Notes
- Clean URLs are disabled on docs; use `.html` or trailing slash when browsing mirrored docs.
- This protocol stays contract-only; infra, storage, and alarm details remain internal. Republish docs and MCP (with CloudFront invalidation) after any content change.


_Build MONDAY-1776194870 • 2026-04-14T19:27:50.000Z • [© 1999 Microhouse Systems Inc. All rights reserved.](https://doc.g3nretailstack.com/common/copyright-license.html)_
