Product plane (catalog & keys)
The shared product-face commerce plane (F2). Vagary is tenant #0. Every capability is sold through this one plane; per-capability edge modules (a later wave) plug in on top. Product->capability edges only.
- Group: Sellable plane
- Contract:
contracts/product-plane/product/v1/openapi.yaml - Console: internal — operated by Vagary Labs; no customer console page for this capability.
- Runbook: operational checklist for
product-plane— internal (Vagary Labs ops; not part of this public site):docs/runbooks/capability-operations.md#product-plane - Public access: none — internal-only capability. It is NOT exposed on the public API gateway (
https://api.vagarylabs.com); it is reachable only inside the fleet (container/tailnet) by first-party callers. There is no customer-facing endpoint to call.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /internal/voice/orders/{order_id} | Read exact historical accepted terms through Billing's dedicated binding |
POST | /signup | Tenant self-onboarding — mint an org, provision the first user in identity, issue an org-scoped API key |
POST | /account/organization | Create an organisation for the signed-in principal, who becomes its owner |
GET | /signup/context | What this organisation's signup established — read by the product's own onboarding |
POST | /onboard | Alias of /signup (tenant self-onboarding + first key) |
GET | /api-keys | List the org's API keys (metadata only — never the raw key or hash) |
POST | /api-keys | Issue a new API key scoped to (org, product). Returns the raw key ONCE. |
POST | /api-keys/{prefix}/rotate | Rotate — mint a fresh raw key (preserving product/label/scopes), revoke the old prefix, chain rotated_to |
POST | /api-keys/{prefix}/suspend | Suspend an active key (ACTIVE -> SUSPENDED) |
POST | /api-keys/{prefix}/reactivate | Reactivate a suspended key (SUSPENDED -> ACTIVE) |
DELETE | /api-keys/{prefix} | Revoke a key permanently (terminal) |
GET | /api-keys/{prefix}/stats | Windowed usage stats for a key |
GET | /projects | List the org's projects (creates the default pair if the org has none) |
POST | /projects | Create a project |
GET | /projects/{project_id}/environments | List a project's environments |
POST | /projects/{project_id}/environments | Create an environment inside a project |
GET | /bindings | List the org's capability bindings, optionally narrowed to one project/environment |
POST | /bindings | Create a capability binding |
POST | /bindings/plan | Produce a server-derived lifecycle plan for a capability binding |
POST | /bindings/apply | Apply a previously planned binding lifecycle change |
GET | /bindings/{binding_id} | Get one capability binding by id |
POST | /bindings/{binding_id} | Unsupported on the binding id root |
POST | /bindings/{binding_id}/status | Set a binding lifecycle status directly |
GET | /bindings/{binding_id}/reconcile | Compose the binding row with its current effective grant and plan visibility |
GET | /products | Product catalog — each product's granted capabilities (the plane owns the catalog; the edge enforces cap ∈ product.caps). PUBLIC (no bearer required, for the pre-auth signup wizard); when a bearer IS presented, the caller's resolved org's bound catalog policy overlays its white-label display_name onto the same full list — see resolve_display_overrides. Not org-scope-FILTERING (that's GET /org-catalog); every product still appears. |
GET | /plans | Product-face priced plan catalog (I5 — named/priced plans over billing-metering's opaque tiers) |
GET | /voice/order-terms | Preview server-owned Voice terms for the verified organization owner |
POST | /voice/orders | Persist an immutable owner acceptance of complete server-owned terms |
GET | /voice/orders/{order_id} | Read immutable accepted terms for the verified organization owner |
POST | /checkout | Create a subscription/checkout intent — delegates the charge to billing-metering (402 = charge-OFF gate) |
GET | /usage | Usage summary (proxies billing-metering /v1/usage/summary for the resolved org) |
GET | /quota/check | Quota check (proxies billing-metering /v1/quota/check) — enforce before granting a metered action |
GET | /resale-pricing | List the caller's own org's current resale prices (optionally filtered to one sku) |
PUT | /resale-pricing/{sku} | Set (replacing any prior value) the caller's own org's current resale price for a SKU. Owner-gated. |
DELETE | /resale-pricing/{sku} | Stop reselling a SKU at a stated price (does not affect catalog_policy exposure). Owner-gated. |
GET | /health | liveness |
GET | /metrics | Prometheus text exposition |
Schemas
CreatedOrganization
| Field | Type | Description |
|---|---|---|
organization_id | string | |
name | string | |
realm | string | taken from the caller's verified token, never from the body |
role | string | the creator is always the owner |
plan | string | |
steps | object | The provisioning steps, reported rather than assumed. Reuses signup's own path so the org/owner/project/environment/entitlement relationship is the SAME one FA1 line 4 proves — a second implementation would be a second definition of correct. |
SignupContext
| Field | Type | Description |
|---|---|---|
organization_id | string | |
found | boolean | false when this organisation has no signup transaction — normal, not an error. Onboarding falls back to asking rather than treating an empty body as a failed lookup. |
context | object |
SignupRequest
| Field | Type | Description |
|---|---|---|
email | string | |
password | string | |
name | string | |
realm | string | identity realm (per-brand); defaults to the plane's DEFAULT_REALM |
organization_id | string | optional — omit to mint a fresh org (tenant self-onboarding) |
product | string | which capability/product the first key grants access to |
plan | string | product-face plan id (free/pro/enterprise) |
SignupResult
| Field | Type | Description |
|---|---|---|
organization_id | string | |
user | object | |
plan | string | the requested/selected plan id — always echoed, whether or not it was applied (see plan_applied) |
plan_applied | boolean | whether plan is actually held by billing-metering right now. True only for a $0 plan whose grant round-tripped 200 (POST /v1/entitlement, ADR-120); false for a paid plan (which stays unapplied until POST /checkout settles a charge — this route never free-grants a paid tier) or a degraded billing call. |
api_key | object | |
signup_mode | string | direct or invite — a caller cannot otherwise tell 'no key because this product mints none' from 'no key because you joined someone else's workspace', and those need different words on screen. |
invited_role | string | the role the INVITATION granted — identity's, never this plane's; null on a direct signup. |
onboarding | object | Where the customer goes next, and how that destination continues from this signup rather than starting over. PATHS, not URLs: the realm -> console-host mapping already has an owner, and an absolute URL here would make this a second authority for it — while a path lands the customer on the console they actually signed up on. |
IssueKeyRequest
| Field | Type | Description |
|---|---|---|
product | string | |
label | string | |
scopes | array | |
expires_at | number | epoch seconds; omit/null for a key that never expires. Must be strictly in the future — an already-past value is rejected with 422. |
project_id | string | Scope the key to one of the org's projects. Omit to use the org's default project. A project the caller's organization does not own is 404 — the id is checked against identity, never trusted. |
environment_id | string | Scope the key to one environment WITHIN project_id. Requires project_id (422 without it: an environment belongs to a project, and pairing a lone environment with the default project would attribute the key to a project the caller never named). An environment outside the named project is 404. |
Project
| Field | Type | Description |
|---|---|---|
id | string | |
organization_id | string | |
name | string | |
slug | string | |
product_id | string |
ProjectList
| Field | Type | Description |
|---|---|---|
projects | array |
CreateProjectRequest
| Field | Type | Description |
|---|---|---|
name | string | |
slug | string | Optional; derived from name when omitted. |
Generated by scripts/gen-capability-docs.py from contracts/product-plane/product/v1/openapi.yaml — the contract IS the source of truth; edit the contract, not this page. The Edge route table section is synced from the live edge catalog snapshot (v1) — re-sync with python3 scripts/sync-edge-catalog.py.