# PPM MCP Protocol

This file is published to <a href="https://mcp.g3nretailstack.com/ppm/PROTOCOL.md" target="_blank" rel="noopener noreferrer">https://mcp.g3nretailstack.com/ppm/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/ppm`
- Health check: `GET /ppm/stat` — requires `requireSession` (any authenticated user).

## Auth + tenancy
- 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).
- Optional cost attribution: provide `x-cccode` (or request field `cccode`).
- `session_guid` is never emitted in responses; use `stats.session_fingerprint` for correlation.
- Facility context: `x-logical-guid` (required for operational actions; see OpenAPI per-path). `x-channel-code` (required where documented; see OpenAPI per-path).

## Roles
- Read: `ppm_view`, `ppm_price_admin`, `ppm_promo_admin`, `ppm_approver`, `ppm_analyst`, `finance_audit` (owner implied). Note: `ppm_admin` and `ppm_approve` are legacy aliases for `ppm_approver`.
- Price admin: `ppm_price_admin`, `ppm_approver`.
- Promo admin: `ppm_promo_admin`, `ppm_approver`.
- Policy admin: `ppm_price_admin`, `ppm_approver`.
- Comment/inbox write: union of price/promo/policy admin roles.

PPM operates at **org level** — pricing rules, promotions, and policies apply across all facilities. There is no facility or channel scoping; a price admin's changes affect the entire organization.

## Surfaces
- Contract-only surface. Implemented and deployed; see `/ppm/openapi.yaml` for the current surface definition.

Notable additions:
- Price approvals: `/ppm/price/approval/*`
- Price lists: `/ppm/price-list/*` (assign/clear/resolve/assignments)
- Price zones: `/ppm/price-zone/*`
- Price recommendations: `/ppm/recommendation/*`
- Comments: `/ppm/comment` (add/get/list/revise/status/report)
- Inbox: `/ppm/inbox` (create/get/list/status/state)

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

| Method | Path | Request schema | Response schema |
| --- | --- | --- | --- |
| POST | /comment | CommentAddRequest | CommentEnvelope |
| POST | /comment/get | CommentGetRequest | CommentEnvelope |
| POST | /comment/list | CommentListRequest | CommentListEnvelope |
| POST | /comment/report | CommentReportRequest | CommentReportEnvelope |
| POST | /comment/revise | CommentReviseRequest | CommentEnvelope |
| POST | /comment/status | CommentStatusRequest | CommentEnvelope |
| POST | /commission-rule/get | (inline) | Envelope |
| POST | /commission-rule/list | (inline) | Envelope |
| POST | /commission-rule/set | (inline) | Envelope |
| POST | /commission-rule/status/set | (inline) | Envelope |
| POST | /coupon/create | (inline) | Envelope |
| POST | /coupon/get | (inline) | Envelope |
| POST | /coupon/list | (inline) | Envelope |
| POST | /coupon/redeem | (inline) | Envelope |
| POST | /coupon/status/set | (inline) | Envelope |
| POST | /coupon/validate | (inline) | Envelope |
| POST | /dynamic-rule/get | DynamicPricingRuleGetRequest | DynamicPricingRuleEnvelope |
| POST | /dynamic-rule/list | DynamicPricingRuleListRequest | DynamicPricingRuleListEnvelope |
| POST | /dynamic-rule/set | DynamicPricingRuleSetRequest | DynamicPricingRuleEnvelope |
| POST | /dynamic-rule/status/set | DynamicPricingRuleStatusSetRequest | DynamicPricingRuleEnvelope |
| POST | /fx/rate/get | FxRateGetRequest | FxRateEnvelope |
| POST | /fx/rate/list | FxRateListRequest | FxRateListEnvelope |
| POST | /fx/rate/set | FxRateSetRequest | FxRateEnvelope |
| POST | /inbox/create | InboxCreateRequest | InboxEnvelope |
| POST | /inbox/get | InboxGetRequest | InboxEnvelope |
| POST | /inbox/list | InboxListRequest | InboxListEnvelope |
| POST | /inbox/state | InboxStateRequest | InboxEnvelope |
| POST | /inbox/status | InboxStatusRequest | InboxEnvelope |
| POST | /installment-plan/get | (inline) | Envelope |
| POST | /installment-plan/list | (inline) | Envelope |
| POST | /installment-plan/set | (inline) | Envelope |
| POST | /installment-plan/status/set | (inline) | Envelope |
| POST | /policy/get | PricePolicyGetRequest | PricePolicyEnvelope |
| POST | /policy/set | PricePolicySetRequest | PricePolicyEnvelope |
| POST | /price-list/assign | PriceListAssignRequest | PriceListAssignmentEnvelope |
| POST | /price-list/assign/clear | PriceListAssignClearRequest | PriceListAssignmentEnvelope |
| POST | /price-list/assignments | PriceListAssignmentsRequest | PriceListAssignmentListEnvelope |
| POST | /price-list/get | PriceListGetRequest | PriceListEnvelope |
| POST | /price-list/list | PriceListListRequest | PriceListListEnvelope |
| POST | /price-list/resolve | PriceListResolveRequest | PriceListAssignmentEnvelope |
| POST | /price-list/set | PriceListSetRequest | PriceListEnvelope |
| POST | /price-list/status/set | PriceListStatusSetRequest | PriceListEnvelope |
| POST | /price-zone/assign | PriceZoneAssignRequest | PriceZoneAssignmentEnvelope |
| POST | /price-zone/assign/clear | PriceZoneAssignClearRequest | PriceZoneAssignmentEnvelope |
| POST | /price-zone/assignments | PriceZoneAssignmentsRequest | PriceZoneAssignmentListEnvelope |
| POST | /price-zone/get | PriceZoneGetRequest | PriceZoneEnvelope |
| POST | /price-zone/list | PriceZoneListRequest | PriceZoneListEnvelope |
| POST | /price-zone/resolve | PriceZoneResolveRequest | PriceZoneAssignmentEnvelope |
| POST | /price-zone/set | PriceZoneSetRequest | PriceZoneEnvelope |
| POST | /price-zone/status/set | PriceZoneStatusSetRequest | PriceZoneEnvelope |
| POST | /price/approval/approve | PriceApprovalApproveRequest | PriceEntryEnvelope |
| POST | /price/approval/reject | PriceApprovalRejectRequest | PriceEntryEnvelope |
| POST | /price/approval/submit | PriceApprovalSubmitRequest | PriceEntryEnvelope |
| POST | /price/get | PriceEntryGetRequest | PriceEntryEnvelope |
| POST | /price/list | PriceEntryListRequest | PriceEntryListEnvelope |
| POST | /price/resolve | PriceResolveRequest | PriceResolveEnvelope |
| POST | /price/set | PriceEntrySetRequest | PriceEntryEnvelope |
| POST | /promotion/create | PromotionCreateRequest | PromotionEnvelope |
| POST | /promotion/end | PromotionEndRequest | PromotionEnvelope |
| POST | /promotion/get | PromotionGetRequest | PromotionEnvelope |
| POST | /promotion/list | PromotionListRequest | PromotionListEnvelope |
| POST | /recommendation/get | PriceRecommendationGetRequest | PriceRecommendationEnvelope |
| POST | /recommendation/list | PriceRecommendationListRequest | PriceRecommendationListEnvelope |
| POST | /recommendation/set | PriceRecommendationSetRequest | PriceRecommendationEnvelope |
| POST | /recommendation/status/set | PriceRecommendationStatusSetRequest | PriceRecommendationEnvelope |
| POST | /rental-rate/get | RentalRateGetRequest | RentalRateEnvelope |
| POST | /rental-rate/list | RentalRateListRequest | RentalRateListEnvelope |
| POST | /rental-rate/set | RentalRateSetRequest | RentalRateEnvelope |
| POST | /rental-rate/status/set | RentalRateStatusSetRequest | RentalRateEnvelope |
| GET | /stat | — | — |
| POST | /subscription-instance/cancel | (inline) | Envelope |
| POST | /subscription-instance/create | (inline) | Envelope |
| POST | /subscription-instance/get | (inline) | Envelope |
| POST | /subscription-instance/list | (inline) | Envelope |
| POST | /subscription-instance/pause | (inline) | Envelope |
| POST | /subscription-instance/resume | (inline) | Envelope |
| POST | /subscription-plan/get | SubscriptionPlanGetRequest | SubscriptionPlanEnvelope |
| POST | /subscription-plan/list | SubscriptionPlanListRequest | SubscriptionPlanListEnvelope |
| POST | /subscription-plan/set | SubscriptionPlanSetRequest | SubscriptionPlanEnvelope |
| POST | /subscription-plan/status/set | SubscriptionPlanStatusSetRequest | SubscriptionPlanEnvelope |
| POST | /trade-agreement/get | (inline) | Envelope |
| POST | /trade-agreement/list | (inline) | Envelope |
| POST | /trade-agreement/set | (inline) | Envelope |
| POST | /trade-agreement/status/set | (inline) | Envelope |
| POST | /uom/get | UomConversionGetRequest | UomConversionEnvelope |
| POST | /uom/list | UomConversionListRequest | UomConversionListEnvelope |
| POST | /uom/set | UomConversionSetRequest | UomConversionEnvelope |

## Error tags
Common tags (see [/common/error-tags.html](https://doc.g3nretailstack.com/common/error-tags.html) for definitions): `invalid-input`, `unauthorized`, `forbidden`, `not-found`, `expected-revision-required`, `conflict`, `invalid-state`, `throttled`, `internal-error`.

## Example envelopes
Success envelope (shape-only):
```json
{
  "success": true,
  "data": { "example": "see schema for fields" },
  "stats": { "service": "ppm", "call": "ppm_example", "timestamp_utc": "2026-01-01T00:00:00Z", "build": { "build_major": "MONDAY", "build_minor": "0000000000", "build_id": "MONDAY-0000000000" } }
}
```

Error envelope (shape-only):
```json
{
  "success": false,
  "error": {
    "error_code": "ppm.validation_failed",
    "http_status": 400,
    "retryable": false,
    "major": { "tag": "invalid-input", "message": { "en_US": "Invalid request." } }
  },
  "stats": { "service": "ppm", "call": "ppm_example", "timestamp_utc": "2026-01-01T00:00:00Z", "build": { "build_major": "MONDAY", "build_minor": "0000000000", "build_id": "MONDAY-0000000000" } }
}
```


## 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".

## Internal jobs

| Job | Trigger | Description |
| --- | --- | --- |
| `subscription-cycle/process` | EventBridge scheduled | Processes subscription billing cycles — advances due instances, creates renewal records |

## OpenAPI
- Contract schema: <a href="https://doc.g3nretailstack.com/ppm/openapi.yaml" target="_blank" rel="noopener noreferrer">https://doc.g3nretailstack.com/ppm/openapi.yaml</a>
- Comment/inbox list defaults: `status=current` / `status=inbox`; use `status=all` to include archived/doomed.


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