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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
21 changes: 19 additions & 2 deletions 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 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,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.

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

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

Expand Down
4 changes: 2 additions & 2 deletions index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Card>
<Card title="API keys" icon="key" href="/integration/api-keys">
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.
</Card>
</CardGroup>

Expand Down Expand Up @@ -77,7 +77,7 @@ Choose the authentication pattern that matches your integration:
## Migration

<Card title="Deprecated routes" icon="triangle-exclamation" href="/integration/deprecated">
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.
</Card>

## Discovery
Expand Down
144 changes: 72 additions & 72 deletions integration/api-keys.mdx
Original file line number Diff line number Diff line change
@@ -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_<hex>` | Per-app-user **API key** (stored / hashed form) | `subject_token` on `POST /api/v1/apps/{clientId}/oidc/token` |
| `app_<clientId>.pmth_<hex>` | **Presented** API key (issuance + remote-signer Bearer) | Same as bare `pmth_*`; the `app_*` prefix routes the key to the correct app |
| `pmth_cs_<hex>` | 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_<clientId>.pmth_<hex>` (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

Expand All @@ -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"
Expand All @@ -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"
}
```
Expand All @@ -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

Expand All @@ -67,109 +72,104 @@ 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
{
"access_token": "eyJ...",
"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
4 changes: 3 additions & 1 deletion integration/billing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
17 changes: 10 additions & 7 deletions integration/client-model.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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_<hex>` | Per-app-user **API key** (stored / hashed) | `subject_token` on `POST /api/v1/apps/{clientId}/oidc/token` |
| `app_<clientId>.pmth_<hex>` | **Presented** API key (issuance + remote-signer Bearer) | Same as bare `pmth_*`; prefix routes to the app exchange URL |
| `pmth_cs_<hex>` | 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_<clientId>.pmth_<hex>`. 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

Expand Down Expand Up @@ -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).
Loading