Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion api-reference/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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).

<Note>
`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.
</Note>

## Where to find endpoint documentation

All endpoint guides currently live in the **Builder guides** tab:
Expand All @@ -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

Expand Down
88 changes: 69 additions & 19 deletions integration/api-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

<Warning>
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.
</Warning>

## Per-user API keys

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```

<Note>
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.
</Note>

---

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

<Note>
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.
</Note>

---

## Security guidance
Expand Down
2 changes: 2 additions & 0 deletions integration/billing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 95 additions & 2 deletions integration/deprecated.mdx
Original file line number Diff line number Diff line change
@@ -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=<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).

---

Expand Down Expand Up @@ -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 |
Expand Down
25 changes: 22 additions & 3 deletions integration/token-exchange.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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). |

<Note>
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).
</Note>

SDK helpers:

```ts
Expand All @@ -174,6 +192,7 @@ const session = await client.mintSignerSessionForExternalUser({
email: "user@example.com",
});
// session.accessToken is opaque pmth_…
// session.signerUrl — DMZ URL when available
```

---
Expand Down
3 changes: 2 additions & 1 deletion integration/user-tokens.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

<Warning>
`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`.
</Warning>

## Passing the token to downstream services
Expand Down