From c506409320046a1496e516a1156e92231d528863 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 26 Jun 2026 05:41:52 +0000 Subject: [PATCH] docs: refresh docs based on pymthouse/pymthouse changes (v0.2.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api-reference/introduction.mdx: document new OpenAPI endpoints GET /api/v1/openapi.json and GET /api/v1/docs (Scalar UI) - integration/api-keys.mdx: add canonical signer-session endpoint POST .../auth/api-key/signer-session with SignerSession envelope; fix GET /api/v1/auth/validate → POST; add pmth_cs_* type warning; remove deprecated app-level keys section - integration/deprecated.mdx: add three new removal entries - GET /api/v1/auth/validate → POST with JSON body - GET/POST/DELETE /api/v1/apps/{clientId}/keys → per-user keys - POST /api/pymthouse/keys/exchange BFF → auth/api-key/signer-session Update summary table with all new removals - integration/user-tokens.mdx: add external_user_id JWT claim (included on programmatic user JWTs per PR #168) - integration/billing.mdx: document Stripe billing graceful fallback Apps without Stripe billing connected still get Free Starter access (per PR #170) - integration/token-exchange.mdx: document full SignerSession envelope fields returned from signer session exchange (balanceUsdMicros, lifetimeGrantedUsdMicros, signer_url, issued_token_type, correlation_id) Co-authored-by: John | Elite Encoder --- api-reference/introduction.mdx | 18 ++++++- integration/api-keys.mdx | 88 +++++++++++++++++++++++------- integration/billing.mdx | 2 + integration/deprecated.mdx | 97 +++++++++++++++++++++++++++++++++- integration/token-exchange.mdx | 25 +++++++-- integration/user-tokens.mdx | 3 +- 6 files changed, 207 insertions(+), 26 deletions(-) diff --git a/api-reference/introduction.mdx b/api-reference/introduction.mdx index 89ebb0f..f193b5e 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 now 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,6 +12,21 @@ 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. | + +The OpenAPI document covers all 90+ Builder API operations, including deprecation markers for removed routes. OIDC issuer metadata remains at `{issuer}/.well-known/openid-configuration` (a virtual pointer to `POST /api/v1/oidc/token` appears in the OpenAPI document for cross-reference). + + + `GET /api/v1/openapi.json` is regenerated from route metadata at build time. After adding route handlers, 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: @@ -21,6 +36,7 @@ All endpoint guides currently live in the **Builder guides** tab: | 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 | ## Next steps diff --git a/integration/api-keys.mdx b/integration/api-keys.mdx index 8d389df..88ed546 100644 --- a/integration/api-keys.mdx +++ b/integration/api-keys.mdx @@ -11,7 +11,10 @@ Two types of API keys exist: | --- | --- | --- | --- | | **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` | + + + Do not mix credential types. Presenting a `pmth_cs_*` M2M client secret to `POST .../auth/api-key/token` or `POST .../auth/api-key/signer-session` returns **`400 invalid_request`** (not `401`). The API-key exchange routes accept only per-user `pmth_*` API keys as Bearer credentials — M2M secrets must be used with HTTP Basic auth for Builder API routes. + ## Per-user API keys @@ -67,18 +70,6 @@ Revocation is immediate. Any exchange request using the revoked key returns `401 --- -## App-level API keys - -App-level keys are tied to a subscription and suitable for app-wide machine access without a specific user context. - -```http -GET /api/v1/apps/{clientId}/keys -POST /api/v1/apps/{clientId}/keys -DELETE /api/v1/apps/{clientId}/keys -``` - -Auth: provider dashboard session. Returns the same `pmth_ak_…` format. - --- ## Exchange API key → short-lived JWT @@ -131,32 +122,91 @@ const tokens = await client.exchangeApiKeyForUserAccessToken({ --- -## Exchange API key → signer session +## Exchange API key → signer session (single call) + +Exchange a `pmth_*` API key directly for a signer session in one HTTP call using the canonical endpoint. This replaces the two-step pattern (API key → JWT → RFC 8693 exchange) and the deprecated dashboard BFF facade: + +```http +POST /api/v1/apps/{clientId}/auth/api-key/signer-session +Authorization: Bearer pmth_ak_abc123... +Content-Type: application/json +``` + +Optional request body: -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: +```json +{ "scope": "sign:job" } +``` + +Response — **`SignerSession` envelope:** + +```json +{ + "access_token": "pmth_signer_session_...", + "token_type": "Bearer", + "expires_in": 86400, + "scope": "sign:job", + "balanceUsdMicros": "4200000", + "lifetimeGrantedUsdMicros": "5000000", + "signer_url": "https://dmz.example.com", + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token" +} +``` + +| Field | Description | +| --- | --- | +| `access_token` | Opaque signer session bearer for the remote DMZ. | +| `balanceUsdMicros` | Current OpenMeter entitlement balance for the user in USD micros. | +| `lifetimeGrantedUsdMicros` | Total lifetime granted allowance for the user. | +| `signer_url` | Direct DMZ URL for this app (optional; present when configured). | +| `issued_token_type` | RFC 8693 token type indicator. | +| `correlation_id` | Opaque tracing id (optional). | + +```bash +API_KEY="pmth_ak_abc123..." + +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/signer-session" +``` + +SDK: ```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.balanceUsdMicros — user's current entitlement balance +// session.signer_url — DMZ URL when available ``` + + Integrator and dashboard facades that previously forwarded requests to `POST /api/pymthouse/keys/exchange` (a BFF route outside PymtHouse) should migrate to call `POST .../auth/api-key/signer-session` on the PymtHouse issuer directly. Pass the `SignerSession` response unchanged to the caller. + + --- ## Validate an API key (subscription-backed) -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: +To validate a Bearer `pmth_*` key and check the associated plan and capabilities, POST to the validation endpoint. This is used by integrations that need to gate access before processing a request: ```http -GET /api/v1/auth/validate -Authorization: Bearer pmth_ak_abc123... +POST /api/v1/auth/validate +Content-Type: application/json + +{ "key": "pmth_ak_abc123..." } ``` Returns `{ valid: true, planId: "...", capabilities: [...] }` on success. + + The legacy `GET /api/v1/auth/validate` with a `Bearer` header has been removed. Use `POST /api/v1/auth/validate` with the key in the JSON body (`BPP_VALIDATE_V2=1`). See [Deprecated routes](/integration/deprecated) for the migration. + + --- ## Security guidance diff --git a/integration/billing.mdx b/integration/billing.mdx index 01788b0..65e7d86 100644 --- a/integration/billing.mdx +++ b/integration/billing.mdx @@ -143,6 +143,8 @@ Every app has a **Starter** plan created automatically (`isStarterDefault: true` 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. +**Stripe billing not required for Starter access.** If your app does not yet have Stripe billing fully connected (no Stripe Connect account linked, or Stripe setup is incomplete), end-users still receive Free Starter access. PymtHouse detects Stripe-related setup errors and recovers gracefully rather than failing subscription provisioning — users are granted the Starter allowance even when the Stripe billing configuration is pending. Connect Stripe via `POST /api/v1/apps/{clientId}/billing/stripe/connect` when you are ready to enable paid plans and Stripe invoicing. + Providers can update the Starter allowance via: ```http diff --git a/integration/deprecated.mdx b/integration/deprecated.mdx index ca9785b..874c512 100644 --- a/integration/deprecated.mdx +++ b/integration/deprecated.mdx @@ -1,9 +1,99 @@ --- 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. The hosted signer proxy, synchronous signed-ticket ingest, legacy subscription, credits, and auth/validate (GET) endpoints have been replaced. --- -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** (returning `410 Gone` or `400 invalid_request`) or **deprecated** in PymtHouse v0.2.x. Follow the migration steps to update your integration. + +--- + +## `GET /api/v1/auth/validate` removed + +**Affected route:** + +| Route | Status | Replacement | +| --- | --- | --- | +| `GET /api/v1/auth/validate` | Removed | `POST /api/v1/auth/validate` with `{ "key": "pmth_…" }` | + +### Why it was removed + +The `GET` form passed the API key in the `Authorization: Bearer` header, which made key validation semantically identical to a Bearer-authenticated request — with no request body for the key being validated. The `POST` form provides an explicit validation surface compatible with the BPP v2 validate shape. + +### Migration + +Replace `GET /api/v1/auth/validate` with a `POST` and move the key into the JSON body: + +```diff +- GET /api/v1/auth/validate +- Authorization: Bearer pmth_… + ++ POST /api/v1/auth/validate ++ Content-Type: application/json ++ ++ { "key": "pmth_…" } +``` + +The response shape (`{ valid, planId, capabilities }`) is unchanged. Enable `BPP_VALIDATE_V2=1` on your deployment to activate the `POST` route. + +--- + +## App-level keys removed (`/api/v1/apps/{clientId}/keys`) + +**Affected routes:** + +| Route | Status | Replacement | +| --- | --- | --- | +| `GET /api/v1/apps/{clientId}/keys` | Removed | `GET .../users/{externalUserId}/keys` | +| `POST /api/v1/apps/{clientId}/keys` | Removed | `POST .../users/{externalUserId}/keys` | +| `DELETE /api/v1/apps/{clientId}/keys` | Removed | `DELETE .../users/{externalUserId}/keys?keyId=` | + +### Why it was removed + +App-level keys were tied to subscriptions rather than to individual users, making revocation coarse-grained and attribution impossible. Per-user keys (`/users/{externalUserId}/keys`) carry user identity through to the DMZ webhook and support fine-grained revocation. + +### Migration + +Issue and manage API keys through the per-user endpoints: + +```diff +- POST /api/v1/apps/{clientId}/keys ++ POST /api/v1/apps/{clientId}/users/{externalUserId}/keys + +- GET /api/v1/apps/{clientId}/keys ++ GET /api/v1/apps/{clientId}/users/{externalUserId}/keys + +- DELETE /api/v1/apps/{clientId}/keys ++ DELETE /api/v1/apps/{clientId}/users/{externalUserId}/keys?keyId= +``` + +See [API keys](/integration/api-keys) for the full per-user key lifecycle. + +--- + +## Dashboard BFF key exchange removed (`POST /api/pymthouse/keys/exchange`) + +**Affected route:** + +| Route | Status | Replacement | +| --- | --- | --- | +| `POST /api/pymthouse/keys/exchange` | Removed (external BFF route) | `POST /api/v1/apps/{clientId}/auth/api-key/signer-session` | + +### Why it was removed + +The BFF facade was an application-layer relay that proxied API key → signer session exchange through the dashboard backend. Routing this through a third-party relay added latency and a dependency on the dashboard deployment for a security-critical token exchange. + +### Migration + +Call the canonical PymtHouse endpoint directly: + +```diff +- POST /api/pymthouse/keys/exchange (external dashboard BFF) + ++ POST /api/v1/apps/{clientId}/auth/api-key/signer-session ++ Authorization: Bearer pmth_… +``` + +The response is the canonical **`SignerSession`** envelope: `access_token`, `token_type`, `expires_in`, `scope`, `balanceUsdMicros`, `lifetimeGrantedUsdMicros`, `signer_url` (optional), `issued_token_type`, `correlation_id` (optional). Pass this response shape unchanged to callers. See [API keys — Exchange API key → signer session](/integration/api-keys#exchange-api-key--signer-session-single-call). --- @@ -191,6 +281,9 @@ Discovery profiles are a legacy mechanism for expressing pipeline/model capabili | Route | Status | Replacement | | --- | --- | --- | +| `GET /api/v1/auth/validate` | Removed | `POST /api/v1/auth/validate` with `{ "key": "pmth_…" }` | +| `GET/POST/DELETE /api/v1/apps/{clientId}/keys` | Removed | `…/users/{externalUserId}/keys` (per-user) | +| `POST /api/pymthouse/keys/exchange` (BFF) | Removed | `POST .../auth/api-key/signer-session` on issuer | | `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 | diff --git a/integration/token-exchange.mdx b/integration/token-exchange.mdx index 27cab9d..d9d45f3 100644 --- a/integration/token-exchange.mdx +++ b/integration/token-exchange.mdx @@ -145,17 +145,35 @@ curl -sS \ Omit `resource`, or set `resource` to the issuer URL (`{issuer}`) if your client always sends a resource indicator (RFC 8707). -### Response +### Response — `SignerSession` envelope ```json { "access_token": "pmth_signer_session_longtoken...", - "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", - "expires_in": 86400 + "expires_in": 86400, + "scope": "sign:job", + "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", + "balanceUsdMicros": "4200000", + "lifetimeGrantedUsdMicros": "5000000", + "signer_url": "https://dmz.example.com", + "correlation_id": "req_abc123" } ``` +| Field | Description | +| --- | --- | +| `access_token` | Opaque signer session bearer for the remote DMZ. | +| `balanceUsdMicros` | Current OpenMeter entitlement balance for the user in USD micros (optional). | +| `lifetimeGrantedUsdMicros` | Total lifetime granted allowance for the user (optional). | +| `signer_url` | Direct DMZ URL for this app (optional; present when configured). | +| `issued_token_type` | RFC 8693 token type indicator. | +| `correlation_id` | Opaque tracing id (optional). | + + + Integrator facades should pass the `SignerSession` response shape through unchanged. For the single-call API key → signer session path (no prior user JWT needed), use `POST .../auth/api-key/signer-session` instead. See [API keys](/integration/api-keys#exchange-api-key--signer-session-single-call). + + SDK helpers: ```ts @@ -174,6 +192,7 @@ const session = await client.mintSignerSessionForExternalUser({ email: "user@example.com", }); // session.accessToken is opaque pmth_… +// session.signerUrl — DMZ URL when available ``` --- diff --git a/integration/user-tokens.mdx b/integration/user-tokens.mdx index 86402e2..a3d3197 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 requiring 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