From 67fcfaf96353fa75af9ce780a42b5c9c8acedc9d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 10 Jul 2026 00:13:14 +0000 Subject: [PATCH] docs: sync Mintlify guides with pymthouse 0.3.2 Builder API Align integrator docs with the latest pymthouse contract: - Credential prefixes: pmth_, app_*.pmth_*, pmth_cs_* (drop pmth_ak_*) - SignerSession exchange via POST .../apps/{clientId}/oidc/token - Remove auth/api-key/*, app-level /keys, and GET /auth/validate - Document OpenAPI/Scalar (openapi.json + /docs) - Update deprecations, README org link, and related guides Co-authored-by: John | Elite Encoder --- README.md | 10 +- api-reference/introduction.mdx | 21 ++++- index.mdx | 4 +- integration/api-keys.mdx | 144 ++++++++++++++--------------- integration/billing.mdx | 4 +- integration/client-model.mdx | 17 ++-- integration/deprecated.mdx | 116 ++++++++++++++++------- integration/manifest.mdx | 10 +- integration/sdk.mdx | 13 ++- integration/signer-routing.mdx | 24 ++--- integration/token-exchange.mdx | 159 +++++++++++++++++--------------- integration/user-management.mdx | 33 ++----- integration/user-tokens.mdx | 3 +- 13 files changed, 315 insertions(+), 243 deletions(-) diff --git a/README.md b/README.md index f575801..07efaa3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Public integrator documentation for PymtHouse. Production URL: **https://docs.pymthouse.com**. -This repository is the **Mintlify site root** (`docs.json` lives here). The main application and canonical contract markdown live in the **[pymthouse](https://github.com/eliteprox/pymthouse)** monorepo. +This repository is the **Mintlify site root** (`docs.json` lives here). The main application and canonical contract markdown live in the **[pymthouse](https://github.com/pymthouse/pymthouse)** monorepo. ## Prerequisites @@ -40,6 +40,10 @@ npm run validate Canonical contract text for code review still lives in the pymthouse repo: - `docs/builder-api.md` -- `docs/naap-oidc-integration.md` -When those files change, update the matching pages under `integration/` in this repository in the same release cycle (same PR on each repo, or coordinated merges). +When that file changes, update the matching pages under `integration/` and `api-reference/` in this repository in the same release cycle (same PR on each repo, or coordinated merges). + +Machine-readable endpoints are also available on every deployment: + +- `GET /api/v1/openapi.json` +- `GET /api/v1/docs` (Scalar UI) diff --git a/api-reference/introduction.mdx b/api-reference/introduction.mdx index 89ebb0f..e9c7751 100644 --- a/api-reference/introduction.mdx +++ b/api-reference/introduction.mdx @@ -3,7 +3,7 @@ title: API reference description: How the PymtHouse HTTP APIs and OIDC surface fit together, and where to find endpoint-level documentation. --- -PymtHouse exposes a **unified OpenID Connect issuer** plus **REST-style Builder and billing APIs** under a single base URL. This tab will collect machine-readable **OpenAPI** definitions as they become available; the live **authorization, token, and user-management** paths are also listed in your tenant's [OIDC discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) document so your client always tracks endpoint rotations. +PymtHouse exposes a **unified OpenID Connect issuer** plus **REST-style Builder and billing APIs** under a single base URL. A machine-readable **OpenAPI 3.1 document** and interactive **Scalar UI** are served directly from your PymtHouse instance. The live **authorization, token, and user-management** paths are also listed in your tenant's [OIDC discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) document so your client always tracks endpoint rotations. ## Base URL and discovery @@ -12,15 +12,32 @@ PymtHouse exposes a **unified OpenID Connect issuer** plus **REST-style Builder Always resolve OAuth and OIDC URLs from discovery in production. Hard-coded paths are brittle when routes or hostnames change. +## Live API reference + +Two self-documentation endpoints are available on every PymtHouse deployment: + +| Endpoint | Description | +| --- | --- | +| `GET /api/v1/openapi.json` | OpenAPI 3.1 document generated from scanned route handlers and per-route metadata. | +| `GET /api/v1/docs` | Interactive Scalar API reference UI — browse, try requests, and inspect schemas. | + +OIDC issuer metadata remains at `{issuer}/.well-known/openid-configuration`. Signer session exchange is documented at `POST /api/v1/apps/{clientId}/oidc/token`. The OIDC provider token endpoint (`POST /api/v1/oidc/token`) covers standard OAuth grants only. + + + After adding route handlers in the pymthouse app, run `npm run openapi:generate` to refresh the inventory. CI runs `npm run check:openapi` to fail on metadata drift. + + ## Where to find endpoint documentation -All endpoint guides currently live in the **Builder guides** tab: +Narrative guides live in the **Builder guides** tab: | Need | Go to | | --- | --- | | End-to-end auth flows, PKCE, device code, token exchange | [Builder guides](/quickstart) tab | | **Usage API** (aggregated usage, billing analytics) | [Usage API](/integration/usage-api) in Builder guides | | User management, app tokens, billing endpoints | [Builder guides](/integration/user-management) and related pages in that tab | +| Interactive OpenAPI browser | `GET /api/v1/docs` on your PymtHouse instance | +| Removed routes and migrations | [Deprecated routes](/integration/deprecated) | ## Next steps diff --git a/index.mdx b/index.mdx index 5e643f4..f05c00c 100644 --- a/index.mdx +++ b/index.mdx @@ -20,7 +20,7 @@ All integrations share the same identity model: every registered app gets a **pu TypeScript client for the Builder API, OIDC issuer, and signer integration. Install in one line. - Long-lived pmth_* keys for per-user and app-level integrations. Exchange for short-lived JWTs on demand. + Long-lived per-user `pmth_*` keys. Exchange at `POST .../oidc/token` for a SignerSession. @@ -77,7 +77,7 @@ Choose the authentication pattern that matches your integration: ## Migration - Migration guide for removed endpoints: signer proxy (410), synchronous ingest (410), and deprecated credits/subscriptions. + Migration guide for removed endpoints: auth/api-key/*, app-level keys, GET validate, signer proxy, and credits/subscriptions. ## Discovery diff --git a/integration/api-keys.mdx b/integration/api-keys.mdx index 8d389df..057d82a 100644 --- a/integration/api-keys.mdx +++ b/integration/api-keys.mdx @@ -1,21 +1,26 @@ --- title: API keys -description: Long-lived opaque pmth_* API keys for app-level and per-user integrations. Exchange for short-lived JWTs or signer sessions. +description: Long-lived per-user pmth_* API keys. Exchange them at the app-scoped OIDC token endpoint for SignerSession envelopes. --- -PymtHouse issues **long-lived opaque API keys** prefixed `pmth_*`. These complement short-lived JWTs: instead of repeating a device login or a full OAuth flow, your integration exchanges a stored API key for a short-lived JWT on demand. +PymtHouse issues **long-lived opaque API keys** for per-app users. Use them as the `subject_token` on the app-scoped token exchange endpoint when you need a short-lived `SignerSession` without repeating a full OAuth login. -Two types of API keys exist: +Do **not** mix credential types. Prefixes have fixed roles: -| Type | Prefix | Scope | Created via | -| --- | --- | --- | --- | -| **M2M client secret** | `pmth_cs_…` | App-level (all users) | App credentials endpoint | -| **Per-user API key** | `pmth_ak_…` | Scoped to one `externalUserId` | `POST .../users/{id}/keys` | -| **App-level API key** | `pmth_ak_…` | Tied to an app subscription | `POST .../keys` | +| Prefix | Role | RFC usage | +| --- | --- | --- | +| `pmth_` | Per-app-user **API key** (stored / hashed form) | `subject_token` on `POST /api/v1/apps/{clientId}/oidc/token` | +| `app_.pmth_` | **Presented** API key (issuance + remote-signer Bearer) | Same as bare `pmth_*`; the `app_*` prefix routes the key to the correct app | +| `pmth_cs_` | Confidential **M2M client secret** | HTTP Basic with `m2m_…` client id — never pass as `subject_token` | +| `app_…` / `m2m_…` | Public / confidential OAuth client ids | Path params and token endpoint `client_id` | + +Newly issued keys are returned as `app_.pmth_` (RFC 6750-safe `.` separator). Bare `pmth_*` remains valid as `subject_token` when the path already supplies `{clientId}`. + +App-level keys at `GET`/`POST`/`DELETE /api/v1/apps/{clientId}/keys` were **removed**. Use per-user keys only. See [Deprecated routes](/integration/deprecated). ## Per-user API keys -Issue long-lived API keys to specific end-users. These are suitable for CLI tools and service accounts where the user authenticates once and stores the key. +Issue long-lived API keys to specific end-users. These suit CLI tools and service accounts where the user authenticates once and stores the key. ### Create a key @@ -24,7 +29,7 @@ POST /api/v1/apps/{clientId}/users/{externalUserId}/keys Authorization: Basic base64(m2m_id:m2m_secret) ``` -The full `pmth_*` secret is returned **once only** at creation time. Store it securely. +The full secret is returned **once only** at creation time. Store it securely. ```bash export BASE_URL="https://your-pymthouse.example" @@ -42,7 +47,7 @@ Response: ```json { "keyId": "key-uuid", - "apiKey": "pmth_ak_abc123...", + "apiKey": "app_yourClientId.pmth_a1b2c3...", "createdAt": "2026-04-01T00:00:00.000Z" } ``` @@ -54,7 +59,7 @@ GET /api/v1/apps/{clientId}/users/{externalUserId}/keys Authorization: Basic base64(m2m_id:m2m_secret) ``` -Returns key IDs and creation timestamps. The `pmth_*` value is never returned after creation. +Returns key IDs and creation timestamps. The secret value is never returned after creation. ### Revoke a key @@ -67,37 +72,36 @@ Revocation is immediate. Any exchange request using the revoked key returns `401 --- -## App-level API keys +## Exchange API key → SignerSession -App-level keys are tied to a subscription and suitable for app-wide machine access without a specific user context. +Exchange a `pmth_*` (or presented `app_….pmth_*`) API key for a clearinghouse-compatible **`SignerSession`** via RFC 8693 token exchange: ```http -GET /api/v1/apps/{clientId}/keys -POST /api/v1/apps/{clientId}/keys -DELETE /api/v1/apps/{clientId}/keys +POST /api/v1/apps/{clientId}/oidc/token +Content-Type: application/x-www-form-urlencoded ``` -Auth: provider dashboard session. Returns the same `pmth_ak_…` format. - ---- - -## Exchange API key → short-lived JWT - -Exchange a `pmth_*` API key for a short-lived user JWT using Bearer auth: - -```http -POST /api/v1/apps/{clientId}/auth/api-key/token -Authorization: Bearer pmth_ak_abc123... -Content-Type: application/json -``` +| Field | Value | +| --- | --- | +| `grant_type` | `urn:ietf:params:oauth:grant-type:token-exchange` | +| `subject_token` | User API key (`pmth_*` or `app_….pmth_*`) | +| `subject_token_type` | `urn:ietf:params:oauth:token-type:access_token` | +| `audience` / `resource` | Optional; when set must match the configured signer audience | -Optional request body: +Optional HTTP Basic with the M2M client (`m2m_*` + secret). When omitted, the `subject_token` alone authenticates the exchange. -```json -{ "scope": "sign:job" } +```bash +API_KEY="pmth_a1b2c3..." # or app_yourClientId.pmth_a1b2c3... + +curl -sS \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + --data-urlencode "subject_token=${API_KEY}" \ + --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ + "${BASE_URL}/api/v1/apps/${CLIENT_ID}/oidc/token" ``` -Response: OIDC token bundle. +Response — canonical **`SignerSession`** envelope: ```json { @@ -105,71 +109,67 @@ Response: OIDC token bundle. "token_type": "Bearer", "expires_in": 300, "scope": "sign:job", - "externalUserId": "user-123" + "signer_url": "https://dmz.example.com", + "discovery_url": "https://discovery.livepeer.cloud", + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + "correlation_id": "…", + "balanceUsdMicros": "4200000", + "lifetimeGrantedUsdMicros": "5000000" } ``` -```bash -API_KEY="pmth_ak_abc123..." +| Field | Description | +| --- | --- | +| `access_token` | Short-lived signer JWT for the remote DMZ. | +| `signer_url` | Optional remote signer DMZ URL. | +| `discovery_url` | Optional Livepeer network discovery URL (not OIDC metadata). | +| `issued_token_type` | RFC 8693 issued token type when present. | +| `correlation_id` | Optional request correlation id. | +| `balanceUsdMicros` | Current OpenMeter entitlement balance (USD micros). | +| `lifetimeGrantedUsdMicros` | Lifetime granted allowance (USD micros). | -curl -sS -X POST \ - -H "Authorization: Bearer ${API_KEY}" \ - -H "Content-Type: application/json" \ - -d '{"scope":"sign:job"}' \ - "${BASE_URL}/api/v1/apps/${CLIENT_ID}/auth/api-key/token" -``` +The removed routes `POST .../auth/api-key/token` and `POST .../auth/api-key/signer-session` are gone — always use `POST .../oidc/token`. -SDK: - -```ts -const tokens = await client.exchangeApiKeyForUserAccessToken({ - apiKey: process.env.PMTH_API_KEY!, -}); -// tokens.access_token — short-lived JWT for the user -// tokens.externalUserId — resolved external user id -``` - ---- - -## Exchange API key → signer session - -Skip the separate signer session exchange by using the SDK helper, which calls the API key token endpoint and then performs the RFC 8693 exchange in one step: +SDK (when available in your `@pymthouse/builder-sdk` version): ```ts const session = await client.exchangeApiKeyForSignerSession({ apiKey: process.env.PMTH_API_KEY!, - facadeUrl: process.env.DASHBOARD_ORIGIN!, // e.g. https://dashboard.example.com - scope: "sign:job", }); -// session.access_token — opaque pmth_… signer bearer for the DMZ +// session.access_token — signer JWT for the DMZ ``` +Prefer calling `POST .../oidc/token` directly if your SDK still documents a facade URL or the old `auth/api-key/*` paths. + --- -## Validate an API key (subscription-backed) +## Validate an API key -To validate a Bearer `pmth_*` key and check the associated plan and capabilities, use the internal validation endpoint. This is used by integrations that need to gate access before processing a request: +Validate a presented key with a JSON body (GET validation was removed): ```http -GET /api/v1/auth/validate -Authorization: Bearer pmth_ak_abc123... +POST /api/v1/auth/validate +Content-Type: application/json + +{ "key": "pmth_…" } ``` -Returns `{ valid: true, planId: "...", capabilities: [...] }` on success. +Requires `BPP_VALIDATE_V2=1` on the PymtHouse deployment. See live schemas at `GET /api/v1/openapi.json` or the Scalar UI at `GET /api/v1/docs`. --- ## Security guidance -- Store API keys in a secret manager, not in source code or environment files committed to version control. -- The `pmth_*` secret is returned once at creation. If lost, revoke the key and create a new one. -- Prefer short-lived JWTs (5-minute TTL) on the signing hot path; exchange the stored API key on demand rather than using it directly as a Bearer token for signing. +- Store API keys in a secret manager, not in source code or committed environment files. +- The secret is returned once at creation. If lost, revoke the key and create a new one. +- Prefer short-lived JWTs on the signing hot path; exchange the stored API key on demand. +- Never pass `pmth_cs_*` as `subject_token` — that prefix is only for M2M HTTP Basic auth. - Revoke keys immediately when a user is deactivated or an integration is disconnected. -- Use per-user keys (not the M2M client secret `pmth_cs_…`) for end-user-facing integrations so revocation is scoped to one user. +- Use per-user keys (not the M2M client secret) so revocation is scoped to one user. ## Related guides - [User management](/integration/user-management) — provisioning users before issuing keys -- [Token exchange](/integration/token-exchange) — RFC 8693 signer session exchange +- [Token exchange](/integration/token-exchange) — RFC 8693 device completion and signer session exchange - [Signer routing](/integration/signer-routing) — using the signer session at the DMZ -- [Builder SDK](/integration/sdk) — `exchangeApiKeyForUserAccessToken`, `exchangeApiKeyForSignerSession` +- [Deprecated routes](/integration/deprecated) — removed `auth/api-key/*` and app-level `/keys` paths diff --git a/integration/billing.mdx b/integration/billing.mdx index 01788b0..7293477 100644 --- a/integration/billing.mdx +++ b/integration/billing.mdx @@ -141,7 +141,9 @@ For `free` plans, `overageUnits` and `overageWei` are always `"0"`. Every app has a **Starter** plan created automatically (`isStarterDefault: true`). It is separate from custom billing plans and from the Network Price discovery plan. Starter carries an `includedUsdMicros` allowance (default `5000000` = $5.00) and is automatically synced to OpenMeter. -New end-users are auto-subscribed to the Starter plan when provisioned (via `POST /users`, signer token mint, or signed-ticket ingest) if they have no existing subscription. +New end-users are auto-subscribed to the Starter plan when provisioned (via `POST /users`, signer token mint, or Kafka collector ingest) if they have no existing subscription. + +**Stripe billing is not required for Starter access.** Apps without Stripe Connect linked (or with incomplete Stripe setup) still grant Free Starter access. PymtHouse recovers from Stripe-related setup errors during subscription provisioning rather than failing the user — Connect Stripe via `POST /api/v1/apps/{clientId}/billing/stripe/connect` when you are ready for paid plans and invoicing. Providers can update the Starter allowance via: diff --git a/integration/client-model.mdx b/integration/client-model.mdx index ecd87f3..8fcbaea 100644 --- a/integration/client-model.mdx +++ b/integration/client-model.mdx @@ -29,15 +29,18 @@ OAuth 2.0 draws a hard line between public clients (those that cannot keep a sec Separating them also means that rotating the M2M secret never affects active device sessions, and that a compromised public `client_id` does not expose any server-side credential. -## API keys (`pmth_*`) +## Credential types (do not mix) -In addition to short-lived JWTs, PymtHouse issues **long-lived opaque API keys** prefixed `pmth_*`. These are used for: +| Prefix | Role | RFC usage | +| --- | --- | --- | +| `pmth_` | Per-app-user **API key** (stored / hashed) | `subject_token` on `POST /api/v1/apps/{clientId}/oidc/token` | +| `app_.pmth_` | **Presented** API key (issuance + remote-signer Bearer) | Same as bare `pmth_*`; prefix routes to the app exchange URL | +| `pmth_cs_` | Confidential **M2M client secret** | HTTP Basic with `m2m_…` — never as `subject_token` | +| `app_…` / `m2m_…` | Public / confidential OAuth client ids | Path params and token endpoint `client_id` | -- **M2M client secrets** — The `pmth_cs_…` secret on the confidential client, used for HTTP Basic auth on Builder API calls. -- **Per-user API keys** — `pmth_ak_…` keys created via `POST .../users/{externalUserId}/keys`, exchangeable for short-lived JWTs or signer sessions. -- **App-level API keys** — `pmth_ak_…` keys tied to a subscription, created via `POST .../keys`. +Per-user API keys are created with `POST .../users/{externalUserId}/keys`. Newly issued keys are returned as `app_.pmth_`. App-level keys at `/api/v1/apps/{clientId}/keys` were removed. -API keys are long-lived and must be stored securely server-side. Exchange them for short-lived JWTs on demand rather than using them directly on the signing hot path. See [API keys](/integration/api-keys) for the exchange flows. +API keys are long-lived and must be stored securely server-side. Exchange them for a short-lived **`SignerSession`** via `POST /api/v1/apps/{clientId}/oidc/token` rather than using them as long-lived Bearer tokens on the signing hot path. See [API keys](/integration/api-keys). ## Environment variables @@ -115,4 +118,4 @@ Clients are created and managed through the developer dashboard or API. `npm run - Confirm that `PYMTHOUSE_PUBLIC_CLIENT_ID` is the `app_…` public id wherever it appears in device or browser flows. - Add only the scopes listed in the table above to each client; remove any default placeholder scopes added by tooling. - Rotate M2M secrets through the credentials endpoint; update the secret in your secret manager without touching the public client. -- For per-user API key flows, use `POST .../users/{externalUserId}/keys` — see [API keys](/integration/api-keys). +- For per-user API key flows, use `POST .../users/{externalUserId}/keys`, then exchange at `POST .../oidc/token` — see [API keys](/integration/api-keys). diff --git a/integration/deprecated.mdx b/integration/deprecated.mdx index ca9785b..6536e09 100644 --- a/integration/deprecated.mdx +++ b/integration/deprecated.mdx @@ -1,9 +1,26 @@ --- title: Deprecated routes and migrations -description: Migration guide for removed endpoints. The hosted signer proxy, synchronous signed-ticket ingest, and legacy subscription and credits endpoints have been replaced. +description: Migration guide for removed endpoints in PymtHouse 0.3.x. Use the canonical replacements below. --- -This page documents endpoints that have been **removed** (returning `410 Gone`) or **deprecated** in PymtHouse v0.2.x. Follow the migration steps to update your integration. +This page documents endpoints that have been **removed** or **deprecated**. Follow the migration steps to update your integration to the PymtHouse **0.3.x** contract. + +--- + +## Breaking changes (API cleanup) + +| Removed | Replacement | +| --- | --- | +| `GET /api/v1/auth/validate` | `POST /api/v1/auth/validate` with `{ "key": "pmth_…" }` (`BPP_VALIDATE_V2=1`) | +| `GET` / `POST` / `DELETE /api/v1/subscriptions` | `POST /api/v1/apps/{clientId}/users`, `GET …/users/{externalUserId}/subscription`, `POST …/allowances` | +| `POST /api/v1/apps/{clientId}/usage/signed-tickets` | Kafka `create_signed_ticket` → OpenMeter collector (no HTTP ingest) | +| `GET` / `POST` / `DELETE /api/v1/apps/{clientId}/keys` | Per-user keys: `…/users/{externalUserId}/keys` | +| `…/users/{externalUserId}/credits` | `…/users/{externalUserId}/allowances` | +| Dashboard BFF `POST /api/pymthouse/keys/exchange` (not served by pymthouse) | `POST /api/v1/apps/{clientId}/oidc/token` | +| `POST /api/v1/apps/{clientId}/auth/api-key/signer-session` | `POST /api/v1/apps/{clientId}/oidc/token` (form `subject_token=pmth_*`) | +| `POST /api/v1/apps/{clientId}/auth/api-key/token` | `POST /api/v1/apps/{clientId}/oidc/token` or M2M `…/users/{externalUserId}/token` | + +M2M secret rotation remains at `POST /api/v1/apps/{clientId}/credentials` (provider session). --- @@ -108,30 +125,63 @@ curl -sS \ --- -## Subscription CRUD removed +## Auth validate: GET → POST + +| Route | Status | Replacement | +| --- | --- | --- | +| `GET /api/v1/auth/validate` | Removed | `POST /api/v1/auth/validate` with JSON `{ "key": "pmth_…" }` | + +Requires `BPP_VALIDATE_V2=1`. Inspect the live response schema at `GET /api/v1/openapi.json`. -**Affected routes:** +--- + +## App-level API keys removed | Route | Status | Replacement | | --- | --- | --- | -| `POST /api/v1/subscriptions` | `410 Gone` | Use `POST .../plans` + OpenMeter checkout | -| `DELETE /api/v1/subscriptions` | `410 Gone` | Use `DELETE .../plans?planId=` | -| `GET /api/v1/subscriptions` | Deprecated (returns legacy cache rows) | Use `GET .../plans` or `GET .../users/{id}/subscription` | +| `GET` / `POST` / `DELETE /api/v1/apps/{clientId}/keys` | Removed | `…/users/{externalUserId}/keys` | -Manage subscriptions through the plans CRUD API (dashboard session) or the OpenMeter billing checkout flow. See [Plans](/integration/plans) and [Allowances](/integration/allowances). +Issue and manage keys per user. Presented form is `app_.pmth_`. See [API keys](/integration/api-keys). --- -## Deprecated `credits` alias +## API key exchange routes removed + +| Route | Status | Replacement | +| --- | --- | --- | +| `POST .../auth/api-key/token` | Removed | `POST .../oidc/token` (API key as `subject_token`) or M2M `…/users/{id}/token` | +| `POST .../auth/api-key/signer-session` | Removed | `POST .../oidc/token` (form `subject_token=pmth_*`) | +| Dashboard BFF `POST /api/pymthouse/keys/exchange` | Not served by pymthouse | `POST /api/v1/apps/{clientId}/oidc/token` | + +```bash +curl -sS \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + --data-urlencode "subject_token=pmth_..." \ + --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ + "${BASE_URL}/api/v1/apps/${CLIENT_ID}/oidc/token" +``` -**Affected routes:** +--- + +## Subscription CRUD removed | Route | Status | Replacement | | --- | --- | --- | -| `GET .../users/{id}/credits` | Deprecated alias | `GET .../users/{id}/allowances` | -| `POST .../users/{id}/credits` | Removed / redirects | `POST .../users/{id}/allowances` | +| `POST /api/v1/subscriptions` | `410 Gone` | `POST .../users` (auto-Starter) + OpenMeter checkout / plans | +| `DELETE /api/v1/subscriptions` | `410 Gone` | `DELETE .../plans?planId=` or subscription management via Plans | +| `GET /api/v1/subscriptions` | Deprecated / removed | `GET .../plans` or `GET .../users/{id}/subscription` | + +See [Plans](/integration/plans) and [Allowances](/integration/allowances). + +--- -The `credits` endpoints re-export the `allowances` endpoints. The `POST` route has been removed from PymtHouse. Update all references: +## Deprecated `credits` alias + +| Route | Status | Replacement | +| --- | --- | --- | +| `GET .../users/{id}/credits` | Deprecated alias | `GET .../users/{id}/allowances` | +| `POST .../users/{id}/credits` | Removed | `POST .../users/{id}/allowances` | ```diff - GET /api/v1/apps/{clientId}/users/{externalUserId}/credits @@ -153,37 +203,33 @@ SDK: --- -## App manifest routes deprecated - -**Affected routes:** +## Network capability manifest | Route | Status | | --- | --- | -| `GET /api/v1/apps/{clientId}/manifest` | Deprecated — returns fail-open stub only | -| `PUT /api/v1/apps/{clientId}/manifest` | Deprecated | +| `GET /api/v1/apps/{clientId}/manifest` | Still available — returns a fixed fail-open stub (`capabilities: []`, `manifestVersion: "empty"`) without NaaP resolution | +| `PUT /api/v1/apps/{clientId}/manifest` | Still available — writes Network Price exclusions; response is the fully resolved manifest | -The manifest API was used to configure discoverable pipeline/model combinations (subtractive exclusions). The signing hot path no longer enforces capability restrictions; `GET .../manifest` returns a fail-open stub (`capabilities: []`) rather than a resolved list. Manage capability exclusions through the **Plans UI** instead. +The signing hot path no longer enforces capability restrictions from the manifest. Prefer the **Plans / Network Price** UI for exclusion management. `GET .../manifest` is still linked from `GET /api/v1/apps/{clientId}` (`links.manifest`) for integrators that poll the stub. -**SDK methods deprecated:** +**SDK methods:** -| Method | Replacement | +| Method | Notes | | --- | --- | -| `getAppManifest({ ifNoneMatch? })` | Plans UI | -| `parseAppManifestResponse(raw)` | N/A | -| `computeManifestRevision(parsed)` | N/A | +| `getAppManifest({ ifNoneMatch? })` | Prefer Plans UI / Network Price for capability management | +| `parseAppManifestResponse(raw)` | Legacy helper | +| `computeManifestRevision(parsed)` | Legacy helper | --- ## Deprecated discovery profiles -**Affected routes:** - | Route | Status | | --- | --- | | `GET/POST .../discovery-profiles` | Legacy — still functional, not removed | | `GET/PUT/DELETE .../discovery-profiles/{id}` | Legacy — still functional, not removed | -Discovery profiles are a legacy mechanism for expressing pipeline/model capability sets on plans. They remain functional for backward compatibility but new integrations should use the **Plans UI** for network capability management. +Discovery profiles remain for backward compatibility. New integrations should manage network capabilities via **Network Price** exclusions (`PUT .../manifest` or the Plans UI). --- @@ -194,15 +240,19 @@ Discovery profiles are a legacy mechanism for expressing pipeline/model capabili | `POST /api/signer/generate-live-payment` | `410 Gone` | `createDirectSignerProxyHandler` + DMZ | | `POST /api/signer/sign-orchestrator-info` | `410 Gone` | Direct DMZ | | `POST /api/signer/sign-byoc-job` | `410 Gone` | Direct DMZ | -| `GET /api/signer/discover-orchestrators` | `410 Gone` | Plans UI | -| `POST .../usage/signed-tickets` | `410 Gone` | Kafka async metering (no replacement) | -| `POST /api/v1/subscriptions` | `410 Gone` | Plans CRUD + checkout | +| `GET /api/signer/discover-orchestrators` | `410 Gone` | Plans / Network Price UI | +| `POST .../usage/signed-tickets` | `410 Gone` | Kafka async metering (no HTTP replacement) | +| `GET /api/v1/auth/validate` | Removed | `POST /api/v1/auth/validate` | +| `GET/POST/DELETE .../apps/{clientId}/keys` | Removed | `…/users/{id}/keys` | +| `POST .../auth/api-key/token` | Removed | `POST .../oidc/token` or `…/users/{id}/token` | +| `POST .../auth/api-key/signer-session` | Removed | `POST .../oidc/token` | +| Dashboard BFF keys/exchange | Not on pymthouse | `POST .../oidc/token` | +| `POST /api/v1/subscriptions` | `410 Gone` | Users + plans / checkout | | `DELETE /api/v1/subscriptions` | `410 Gone` | Plans CRUD | | `POST .../users/{id}/credits` | Removed | `POST .../users/{id}/allowances` | | `GET .../users/{id}/credits` | Deprecated alias | `GET .../users/{id}/allowances` | | `getUserCredits()` SDK method | Deprecated | `getUserAllowances()` | | `grantUserCredits()` SDK method | Deprecated | `grantUserAllowance()` | | `ingestSignedTicket()` SDK method | Legacy | No direct replacement (Kafka handles metering) | -| `GET .../manifest` | Deprecated | Plans UI | -| `PUT .../manifest` | Deprecated | Plans UI | -| `getAppManifest()` SDK method | Deprecated | Plans UI | +| `GET .../manifest` | Fail-open stub | Network Price / Plans UI | +| `PUT .../manifest` | Writes exclusions | Network Price / Plans UI | diff --git a/integration/manifest.mdx b/integration/manifest.mdx index 85d1abf..0501e53 100644 --- a/integration/manifest.mdx +++ b/integration/manifest.mdx @@ -1,12 +1,16 @@ --- title: App manifest -description: Read and update your app's network capability manifest — the set of pipeline/model combinations discoverable through PymtHouse. Supports ETag caching and SDK helpers. +description: Network capability manifest for pipeline/model exclusions. GET returns a fail-open stub; PUT writes Network Price exclusions. Not on the active Builder guides nav — see Plans and Deprecated routes. --- -The **app manifest** defines which pipeline/model combinations are discoverable for your app through the PymtHouse network. It is expressed as a **subtractive** allowlist: the full NaaP pipeline catalog minus any `excludedCapabilities` you configure is what integrators and end-users can discover. +The **app manifest** routes expose network capability exclusions for your app (Network Price plan). The signing hot path does **not** enforce the manifest. - **`GET .../manifest` behavior change:** The GET endpoint currently returns a **fail-open stub** (`capabilities: []`, `excludedCapabilities: []`, `manifestVersion: "empty"`) rather than the fully-resolved capability list. `PUT .../manifest` still resolves and returns the full manifest after writing exclusions. The manifest is not enforced on the signing hot path — direct DMZ signing does not consult it. + This page is kept for reference but is not linked in the main navigation. Prefer the **Plans / Network Price** UI for exclusion management. See [Deprecated routes](/integration/deprecated) and [Plans](/integration/plans). + + + + **`GET .../manifest`:** Returns a fixed fail-open stub (`capabilities: []`, `excludedCapabilities: []`, `manifestVersion: "empty"`) without NaaP resolution. **`PUT .../manifest`** still updates Network Price exclusions and returns the fully resolved manifest. App metadata at `GET /api/v1/apps/{clientId}` may include `links.manifest`. ## Endpoints diff --git a/integration/sdk.mdx b/integration/sdk.mdx index 0265952..b7669bd 100644 --- a/integration/sdk.mdx +++ b/integration/sdk.mdx @@ -84,12 +84,15 @@ All REST calls use HTTP Basic auth against `{issuerOrigin}/api/v1/apps/{publicCl | Method | Description | | --- | --- | | `mintUserAccessToken(input)` | `POST .../users/{id}/token` — short-lived JWT. Requires `users:token`. | -| `exchangeForSignerSession({ userJwt, resource? })` | RFC 8693: user JWT → opaque `pmth_*` signer session. | -| `mintUserSignerSessionToken(input)` | Mint user JWT + exchange in one call. | +| `exchangeForSignerSession({ userJwt })` | RFC 8693 via `POST .../apps/{clientId}/oidc/token` — user JWT → `SignerSession`. | +| `mintUserSignerSessionToken(input)` | Mint user JWT + app-scoped exchange in one call. | | `mintSignerSessionForExternalUser(input)` | Upsert user + mint + exchange workflow. | | `createSignerSessionToken({ userJwt? })` | Exchange or fall back to machine token. | -| `exchangeApiKeyForUserAccessToken({ apiKey })` | `POST .../auth/api-key/token` — API key → short-lived JWT. | -| `exchangeApiKeyForSignerSession({ apiKey, facadeUrl? })` | API key → opaque signer session. | +| `exchangeApiKeyForSignerSession({ apiKey })` | API key as `subject_token` on `POST .../oidc/token` → `SignerSession`. | + + + Prefer the HTTP contract in [API keys](/integration/api-keys) and [Token exchange](/integration/token-exchange). Older SDK helpers that call `…/auth/api-key/token`, `…/auth/api-key/signer-session`, or a dashboard `facadeUrl` target routes that were removed in PymtHouse 0.3.x. + ### Device flow @@ -158,7 +161,7 @@ const session = await client.mintUserSignerSessionToken({ externalUserId: "naap-user-123", scope: "sign:job", }); -// session.access_token — opaque pmth_… bearer for the DMZ +// session.access_token — signer JWT for the DMZ // Option 3: upsert user + mint + exchange in one call const session = await client.mintSignerSessionForExternalUser({ diff --git a/integration/signer-routing.mdx b/integration/signer-routing.mdx index 5108e10..92e74c9 100644 --- a/integration/signer-routing.mdx +++ b/integration/signer-routing.mdx @@ -18,7 +18,6 @@ sequenceDiagram participant App as App backend participant PM as PymtHouse Builder API participant DMZ as Remote signer DMZ - participant OP as OIDC issuer App->>PM: GET .../signer/routing PM-->>App: { dmzUrl, webhookUrl, meteringMode } @@ -26,10 +25,10 @@ sequenceDiagram App->>PM: Mint user JWT (Builder API /users/{id}/token) PM-->>App: short-lived JWT (sign:job scope) - App->>OP: RFC 8693 exchange → opaque signer session - OP-->>App: pmth_… signer session token + App->>PM: POST .../apps/{clientId}/oidc/token (RFC 8693) + PM-->>App: SignerSession (access_token = signer JWT) - App->>DMZ: Forward signing request (Bearer pmth_…) + App->>DMZ: Forward signing request (Bearer signer JWT) DMZ->>App: POST /authorize (webhook) App-->>DMZ: { auth_id, externalUserId } DMZ-->>App: Signed ticket @@ -125,26 +124,19 @@ const response = await forwardDirectSignerRequest({ }); ``` -### Device and API key exchange handlers +### Device exchange handler -For CLI device flows and API key integrations, builder-sdk provides purpose-built handlers: +For CLI device flows, builder-sdk provides a purpose-built handler: ```ts -import { - createDeviceExchangeHandler, - createApiKeyExchangeHandler, -} from "@pymthouse/builder-sdk/signer/server"; +import { createDeviceExchangeHandler } from "@pymthouse/builder-sdk/signer/server"; // POST /api/signer/device/exchange — device token → signer JWT const deviceExchange = createDeviceExchangeHandler({ client }); - -// POST /api/signer/api-key/exchange — API key → signer session via facade -const apiKeyExchange = createApiKeyExchangeHandler({ - client, - facadeUrl: process.env.DASHBOARD_ORIGIN!, -}); ``` +For API keys, exchange at `POST /api/v1/apps/{clientId}/oidc/token` with the key as `subject_token` (see [API keys](/integration/api-keys)). Do not use dashboard facade URLs or the removed `auth/api-key/*` routes. + --- ## Identity webhook (`@pymthouse/builder-sdk/signer/webhook`) diff --git a/integration/token-exchange.mdx b/integration/token-exchange.mdx index 27cab9d..beab843 100644 --- a/integration/token-exchange.mdx +++ b/integration/token-exchange.mdx @@ -1,38 +1,26 @@ --- title: Token exchange -description: Use OAuth 2.0 Token Exchange (RFC 8693) to complete device authorization, obtain a remote signer session, or mint a user signer token via the clearinghouse. +description: Use OAuth 2.0 Token Exchange (RFC 8693) for device completion, app-scoped SignerSession issuance, and clearinghouse Option A mint. --- -PymtHouse implements the **OAuth 2.0 Token Exchange** grant (RFC 8693) for three server-side operations: +PymtHouse implements **OAuth 2.0 Token Exchange** (RFC 8693) for three server-side operations. They use **different endpoints**: -1. **Device completion** — a backend binds a pending RFC 8628 device grant to an authenticated user, completing the CLI authentication flow without a second browser redirect. -2. **Remote signer session exchange** — a short-lived access token is exchanged for a long-lived opaque remote signer session token (`pmth_*`) scoped to `sign:job`. -3. **Clearinghouse signer mint (Option A)** — M2M client credentials with `sign:mint_user_token` scope mints a user-scoped signer JWT and allowance data in a single call without a prior Builder API user-token step. +1. **Device completion** — bind a pending RFC 8628 device grant to an authenticated user via `POST {issuer}/token`. +2. **Signer session exchange** — exchange a user JWT **or** per-user API key for a clearinghouse-compatible **`SignerSession`** via `POST /api/v1/apps/{clientId}/oidc/token`. +3. **Clearinghouse signer mint (Option A)** — M2M `client_credentials` with `sign:mint_user_token` on `POST /api/v1/oidc/token` mints a user-scoped signer JWT and allowance data in one call. -Operations 1 and 2 use the same token endpoint (`POST {issuer}/token`) and the same `grant_type`, but with different `resource` values. Operation 3 uses `client_credentials` grant type. - -## Common parameters (operations 1 & 2) - -All RFC 8693 token exchange requests: - -| Parameter | Value | -| --- | --- | -| `grant_type` | `urn:ietf:params:oauth:grant-type:token-exchange` | -| `subject_token_type` | `urn:ietf:params:oauth:token-type:access_token` | -| `subject_token` | A valid access token issued by this PymtHouse issuer | - -Authentication: **M2M HTTP Basic auth** (`Authorization: Basic base64(m2m_id:m2m_secret)`) is required for all token exchange calls. +The global OIDC token endpoint (`POST /api/v1/oidc/token` or `{issuer}/token`) covers standard OAuth grants and device completion. **API-key and user-JWT SignerSession exchange** use the **app-scoped** route, not the global token endpoint. --- ## Device completion (RFC 8693 + RFC 8628) -Use this operation in the **NaaP / Option B** flow: after the user authenticates at your backend, call the token endpoint to bind the pending device grant. The polling CLI receives its access token on the next poll. +Use this in the **NaaP / Option B** flow: after the user authenticates at your backend, call the **global** issuer token endpoint to bind the pending device grant. The polling CLI receives its access token on the next poll. ### Prerequisites - A confidential M2M client (`m2m_…`) with `device:approve` **or** `users:token` scope. -- `device_third_party_initiate_login` enabled on the **public** client. +- `device_third_party_initiate_login` enabled on the **public** client where required. - A user-scoped JWT for the **public** `app_…` client (minted via [User tokens](/integration/user-tokens)). ### Flow @@ -46,7 +34,7 @@ sequenceDiagram Note over RP: User has authenticated at RP's IdP RP->>OP: Mint user JWT (Builder API /users/{id}/token) OP-->>RP: user_jwt (sub = app-user, azp = app_…) - RP->>OP: POST /token — token exchange, resource = urn:pmth:device_code:ABCD-EFGH + RP->>OP: POST {issuer}/token — token exchange, resource = urn:pmth:device_code:ABCD-EFGH OP-->>RP: 200 RFC 8693 response (device grant bound as side-effect) Device->>OP: Poll POST /token (grant_type=device_code) OP-->>Device: access_token ✓ @@ -54,6 +42,8 @@ sequenceDiagram ### Request +HTTP Basic with the M2M client is **required** for device completion. + ```bash ISSUER="${BASE_URL}/api/v1/oidc" M2M_ID="m2m_yourClientId" @@ -71,30 +61,28 @@ curl -sS \ "${ISSUER}/token" ``` -**`resource` format:** `urn:pmth:device_code:` — use the `user_code` (e.g. `ABCD-EFGH`), not the `device_code`. PymtHouse normalizes the code before lookup. +**`resource` format:** `urn:pmth:device_code:` — use the `user_code` (e.g. `ABCD-EFGH`), not the `device_code`. ### Subject token requirements The `subject_token` must be: - A valid JWT issued by **this** PymtHouse issuer (signature verified against `{issuer}/jwks`). -- Issued to the **public** `app_…` client for the same app (`client_id` or `azp` claim = public client id). +- Issued to the **public** `app_…` client for the same app (`client_id` or `azp` = public client id). - Not expired. -The M2M client's `allowed_scopes` must include `device:approve` or `users:token`. - ### Response ```json { - "access_token": "pmth_signer_session_...", + "access_token": "…", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 86400 } ``` -The binding of the device grant is a **side-effect** of this call. The CLI's next poll of the device code token endpoint will return the bound session. +The binding of the device grant is a **side-effect** of this call. The CLI's next poll returns the bound session. SDK helper: @@ -108,79 +96,101 @@ await client.approveDeviceLogin({ --- -## Signer session exchange +## Signer session exchange (app-scoped) -Use this operation to exchange a short-lived user access token for a long-lived opaque remote signer session token (`pmth_*`). - -### Prerequisites +Clearinghouse-compatible **`SignerSession`** issuance: -- **HTTP Basic auth** with the confidential **M2M** client (`m2m_…` and secret). -- The M2M client's `allowed_scopes` must include **`users:token`**. -- The `subject_token` must already contain **`sign:job`** scope. +```http +POST /api/v1/apps/{clientId}/oidc/token +Content-Type: application/x-www-form-urlencoded +``` -### Subject token binding +| Field | Value | +| --- | --- | +| `grant_type` | `urn:ietf:params:oauth:grant-type:token-exchange` | +| `subject_token` | User access JWT **or** per-app-user API key (`pmth_*` / `app_….pmth_*`) | +| `subject_token_type` | `urn:ietf:params:oauth:token-type:access_token` | +| `audience` / `resource` | Optional; when provided must match configured signer audience (issuer URL, `SIGNER_TOKEN_AUDIENCE`, or legacy `livepeer-clearinghouse` / `livepeer-remote-signer`) | -The `subject_token` must be a JWT from this issuer whose `client_id` or `azp` is either: +Optional HTTP Basic with the M2M client. When omitted, the `subject_token` alone authenticates the exchange. -- The **public** `app_…` client for the same developer app as the authenticating M2M client (typical after interactive login or Builder user-token mint), or -- The same **M2M** `client_id` as the request (legacy `client_credentials` access token used as `subject_token`). +Do **not** use the global `{issuer}/token` endpoint for API-key SignerSession exchange. Do **not** call the removed `POST .../auth/api-key/signer-session` route. -### Request +### Example (API key as `subject_token`) ```bash -ISSUER="${BASE_URL}/api/v1/oidc" -M2M_ID="m2m_yourClientId" -M2M_SECRET="pmth_cs_yourSecret" -ACCESS_TOKEN="eyJ..." # user access token with sign:job scope (azp = public app_…) +CLIENT_ID="app_yourClientId" +API_KEY="pmth_..." curl -sS \ - -u "${M2M_ID}:${M2M_SECRET}" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ - --data-urlencode "subject_token=${ACCESS_TOKEN}" \ + --data-urlencode "subject_token=${API_KEY}" \ --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ - --data-urlencode "scope=sign:job" \ - "${ISSUER}/token" + "${BASE_URL}/api/v1/apps/${CLIENT_ID}/oidc/token" ``` -Omit `resource`, or set `resource` to the issuer URL (`{issuer}`) if your client always sends a resource indicator (RFC 8707). +### Example (user JWT after device flow or M2M user-token mint) -### Response +```bash +USER_JWT="eyJ..." + +curl -sS \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + --data-urlencode "subject_token=${USER_JWT}" \ + --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ + "${BASE_URL}/api/v1/apps/${CLIENT_ID}/oidc/token" +``` + +### Response (`SignerSession`) ```json { - "access_token": "pmth_signer_session_longtoken...", - "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + "access_token": "eyJ...", "token_type": "Bearer", - "expires_in": 86400 + "expires_in": 300, + "scope": "sign:job", + "signer_url": "https://dmz.example.com", + "discovery_url": "https://discovery.livepeer.cloud", + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + "correlation_id": "…", + "balanceUsdMicros": "4200000", + "lifetimeGrantedUsdMicros": "5000000" } ``` -SDK helpers: +| Field | Description | +| --- | --- | +| `access_token` | Short-lived signer JWT for the remote DMZ. | +| `signer_url` | Optional remote signer DMZ URL. | +| `discovery_url` | Optional Livepeer network discovery URL (not OIDC metadata). | +| `issued_token_type` | RFC 8693 issued token type when present. | +| `correlation_id` | Optional request correlation id. | +| `balanceUsdMicros` | Current OpenMeter entitlement balance (USD micros). | +| `lifetimeGrantedUsdMicros` | Lifetime granted allowance (USD micros). | + +SDK helpers (paths may vary by SDK version — prefer the HTTP contract above): ```ts -// Exchange a user JWT for a signer session const session = await client.exchangeForSignerSession({ userJwt: accessToken }); -// Mint user JWT + exchange in one call const session = await client.mintUserSignerSessionToken({ externalUserId: "naap-user-123", scope: "sign:job", }); -// Full workflow: upsert user + mint + exchange const session = await client.mintSignerSessionForExternalUser({ externalUserId: "naap-user-123", email: "user@example.com", }); -// session.accessToken is opaque pmth_… ``` --- ## Clearinghouse signer mint (Option A) -M2M clients with `sign:mint_user_token` scope (automatically added when the public client has `sign:job`) can mint a user-scoped signer JWT and receive allowance balance information in a single `client_credentials` call. This avoids the separate Builder API user-token step. +M2M clients with `sign:mint_user_token` (auto-added when the public client has `sign:job`) can mint a user-scoped signer JWT and receive allowance balance in a single `client_credentials` call on the **global** OIDC token endpoint. ### Request @@ -196,7 +206,6 @@ curl -sS \ --data-urlencode "client_secret=${M2M_SECRET}" \ --data-urlencode "scope=sign:mint_user_token" \ --data-urlencode "external_user_id=naap-user-123" \ - --data-urlencode "audience=livepeer-remote-signer" \ "${ISSUER}/token" ``` @@ -215,12 +224,14 @@ curl -sS \ | Field | Description | | --- | --- | -| `access_token` | User-scoped JWT with `aud=livepeer-remote-signer`. Use as a Bearer token at the remote signer DMZ. | +| `access_token` | Short-lived signer JWT. Use as a Bearer token at the remote signer DMZ. | | `balanceUsdMicros` | Current OpenMeter entitlement balance for the user in USD micros. | | `lifetimeGrantedUsdMicros` | Total lifetime granted allowance for the user. | +For a follow-on RFC 8693 exchange after minting a **user** JWT (not Option A), use `POST /api/v1/apps/{clientId}/oidc/token`. + - `sign:mint_user_token` is automatically granted to M2M clients when the public sibling client has `sign:job` in its `allowed_scopes`. No manual scope configuration is required. + `sign:mint_user_token` is automatically granted to M2M clients when the public sibling client has `sign:job` in its `allowed_scopes`. --- @@ -229,26 +240,24 @@ curl -sS \ | Status | Condition | | --- | --- | -| `400 invalid_grant` | `subject_token` is expired, invalid signature, or wrong issuer. | -| `400 invalid_request` | Missing required parameter, or `resource` value not recognized. | -| `401 Unauthorized` | M2M / client credentials invalid. | -| `403 Forbidden` | M2M client lacks the required scope (`device:approve`, `users:token`, or `sign:mint_user_token`), or `subject_token` client mismatch. | +| `400 invalid_grant` | `subject_token` is expired, invalid signature, wrong issuer, or invalid API key. | +| `400 invalid_request` | Missing required parameter, or `resource` / `audience` not recognized. | +| `401 Unauthorized` | M2M credentials invalid (when Basic is required or supplied). | +| `403 Forbidden` | M2M client lacks the required scope, or `subject_token` client mismatch. | --- ## Key design decisions -1. **Single token endpoint for all exchange types.** Routing device completion, signer session exchange, and signer JWT mint through `POST {issuer}/token` keeps the public surface minimal and consistent with RFC standards. -2. **`resource` as the dispatch discriminator.** `urn:pmth:device_code:` signals device completion; absence or the issuer URL routes to signer session exchange. -3. **Binding is a side-effect, not the primary response.** The CLI polls the standard device code endpoint rather than a proprietary callback, keeping device polling logic independent of the Option B backend. -4. **Option A (`sign:mint_user_token`)** minimizes round trips for clearinghouse integrators — one call returns both the signer JWT and the user's current balance. +1. **App-scoped route for SignerSession.** Clearinghouse-compatible exchange (JWT or API key) uses `POST /api/v1/apps/{clientId}/oidc/token` so `{clientId}` is explicit and Basic auth can be optional when the API key authenticates the call. +2. **Global issuer token endpoint for device completion and Option A.** Device binding and `sign:mint_user_token` stay on `{issuer}/token` / `/api/v1/oidc/token`. +3. **Binding is a side-effect for device completion.** The CLI polls the standard device code endpoint rather than a proprietary callback. +4. **Option A minimizes round trips** for clearinghouse integrators — one call returns the signer JWT and the user's current balance. ## Implementation tasks -- For device completion: mint the user JWT via the Builder API **before** calling the token exchange. -- Validate that your backend stores the `user_code` from the device code response and passes it verbatim to the `resource` parameter. -- For signer session exchange, verify the `subject_token` contains `sign:job` before calling. -- For Option A: ensure the M2M client has `sign:mint_user_token` in `allowed_scopes` (automatically derived from public client's `sign:job`). -- Check `balanceUsdMicros` from the Option A response to gate access before forwarding to the DMZ. -- Do not retry a device completion exchange with the same `user_code` after success; the grant has already been bound. +- For device completion: mint the user JWT via the Builder API **before** calling `{issuer}/token`. +- For SignerSession: call `POST /api/v1/apps/{clientId}/oidc/token` (not the global token endpoint, and not the removed `auth/api-key/*` routes). +- Never pass `pmth_cs_*` as `subject_token`. +- For Option A: rely on auto-derived `sign:mint_user_token` when the public client has `sign:job`. - After obtaining a signer session or signer JWT, forward it to the remote signer DMZ — see [Signer routing](/integration/signer-routing). diff --git a/integration/user-management.mdx b/integration/user-management.mdx index da846f2..bd6f249 100644 --- a/integration/user-management.mdx +++ b/integration/user-management.mdx @@ -153,7 +153,7 @@ curl -sS -X DELETE \ ## Per-user API keys -Long-lived opaque API keys (`pmth_*`) can be issued per-user and exchanged for short-lived JWTs or signer sessions without repeating device login. +Long-lived opaque API keys (`pmth_`, presented as `app_.pmth_`) can be issued per-user and exchanged for a **`SignerSession`** without repeating device login. ```http GET /api/v1/apps/{clientId}/users/{externalUserId}/keys @@ -164,43 +164,30 @@ Authorization: Basic base64(m2m_id:m2m_secret) **GET** returns the list of active API keys for the user. -**POST** creates a new key. Response includes the full `pmth_*` secret (shown once only): +**POST** creates a new key. Response includes the full secret (shown once only): ```json { "keyId": "key-uuid", - "apiKey": "pmth_ak_...", + "apiKey": "app_yourClientId.pmth_a1b2c3...", "createdAt": "2026-04-01T00:00:00.000Z" } ``` **DELETE** with `?keyId=` revokes the key immediately. -Exchange a per-user API key for a short-lived JWT: +Exchange a per-user API key for a `SignerSession` at the app-scoped OIDC token endpoint: ```http -POST /api/v1/apps/{clientId}/auth/api-key/token -Authorization: Bearer pmth_ak_... -Content-Type: application/json - -{ "scope": "sign:job" } -``` +POST /api/v1/apps/{clientId}/oidc/token +Content-Type: application/x-www-form-urlencoded -See [API keys](/integration/api-keys) for the full exchange flow. - ---- - -## App-level API keys - -App-level API keys (tied to a subscription) are managed separately: - -```http -GET /api/v1/apps/{clientId}/keys -POST /api/v1/apps/{clientId}/keys -DELETE /api/v1/apps/{clientId}/keys +grant_type=urn:ietf:params:oauth:grant-type:token-exchange& +subject_token=pmth_...& +subject_token_type=urn:ietf:params:oauth:token-type:access_token ``` -Auth: provider dashboard session. These keys are suitable for integrations where a single credential covers the entire app rather than individual users. +App-level keys at `/api/v1/apps/{clientId}/keys` and `POST .../auth/api-key/*` were removed. See [API keys](/integration/api-keys) and [Deprecated routes](/integration/deprecated). --- diff --git a/integration/user-tokens.mdx b/integration/user-tokens.mdx index 86402e2..18b5fc3 100644 --- a/integration/user-tokens.mdx +++ b/integration/user-tokens.mdx @@ -82,11 +82,12 @@ The issued JWT contains: | `iss` | PymtHouse issuer URL | Verify against discovery. | | `sub` | `app_users.id` (PymtHouse app-user row id) | Not the same as `externalUserId`. | | `client_id` / `azp` | Public `app_…` client id | Used for tenant matching in RFC 8693 exchanges. | +| `external_user_id` | Your system's user identifier | The `externalUserId` as stored during provisioning. Included on programmatic user JWTs for downstream attribution without a separate lookup. | | `scope` | Granted scopes | Subset of the public client's `allowed_scopes`. | | `exp` | Expiry timestamp | Tokens are short-lived by design. | - `sub` is the PymtHouse internal app-user id, **not** your `externalUserId`. If you need to correlate the JWT back to your user, use the `client_id`/`azp` + your own session context rather than parsing `sub`. + `sub` is the PymtHouse internal app-user id, **not** your `externalUserId`. Prefer the `external_user_id` claim for correlating the JWT back to your user, rather than parsing `sub`. ## Passing the token to downstream services