The two-client model
Each developer app that needs both end-user and server-to-server access has two OIDC clients registered in PymtHouse:
The two clients are siblings:
developer_apps.oidc_client_id → public row; developer_apps.m2m_oidc_client_id → M2M row.
Why two clients instead of one?
OAuth 2.0 draws a hard line between public clients (those that cannot keep a secret, such as native apps and CLIs) and confidential clients (servers that can). A single client cannot satisfy both security requirements:- Device flow polling happens in a CLI or SDK — no place to store a secret.
- Builder API calls must be authenticated to enforce the tenant boundary — a secret is required.
client_id does not expose any server-side credential.
API keys (pmth_*)
In addition to short-lived JWTs, PymtHouse issues long-lived opaque API keys prefixed pmth_*. These are used for:
- 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 viaPOST .../users/{externalUserId}/keys, exchangeable for short-lived JWTs or signer sessions. - App-level API keys —
pmth_ak_…keys tied to a subscription, created viaPOST .../keys.
Environment variables
Follow this naming convention when configuring your integration. Mistakes here are a common source of400 invalid_scope and 401 errors.
The
@pymthouse/builder-sdk/env subpath reads these exact variable names. See Builder SDK.OAuth scopes
Scopes control what each client can request and what claims appear in issued tokens.Public client scopes (allowed_scopes)
These scopes are configured on the public app_… client. They govern:
- End-user token claims — which claims appear in tokens issued to the user.
- Programmatic user JWT requests — scopes requested when calling the user-token mint endpoint are validated against this list, not the M2M list.
- Billing pattern — see below.
M2M client scopes (allowed_scopes)
These scopes gate server-side calls from the confidential m2m_… client.
Grant only the minimum scopes the server-side flow requires.
Billing pattern
The presence ofusers:token in the public client’s allowed_scopes determines the billing mode for the app:
This is derived from
src/lib/allowed-scopes.ts → billingPatternFromAllowedScopesString. Add users:token to the public client only if your use case requires per-user billing data from the Usage API.
Client registration
Clients are created and managed through the developer dashboard or API.npm run oidc:seed initialises signing keys only — it does not create application clients. Ask the platform administrator if you need a client pair provisioned in a shared environment.
Key design decisions
client_idas the URL tenant identifier. All Builder API paths use/api/v1/apps/{clientId}/…whereclientIdis the publicapp_…value. This avoids exposing internal database IDs and keeps the URL stable across secret rotations.- Secrets on M2M only. Placing the secret exclusively on the M2M client allows the public client to be freely embedded in CLI binaries, native apps, and JavaScript SDKs without creating a secret-exposure risk.
- Scope policy on the public client governs JWT claims. Validating the requested scope for user-JWT minting against the public client means users cannot receive capabilities that exceed what the app registered for.
- Billing pattern inferred from scope. Using the presence of
users:tokeninallowed_scopesrather than a separate flag means the billing mode is a direct consequence of the integration pattern rather than an independent setting. sign:mint_user_tokenauto-derived. Auto-adding this scope to M2M clients when the public client hassign:jobprevents a configuration footgun where the public scope and M2M clearinghouse capability drift out of sync.
Implementation tasks
- Register your app and both clients (public + M2M) before testing any endpoint.
- Store
PYMTHOUSE_M2M_CLIENT_IDandPYMTHOUSE_M2M_CLIENT_SECRETin your backend secret manager; never expose them to the browser, mobile app, or CLI. - Confirm that
PYMTHOUSE_PUBLIC_CLIENT_IDis theapp_…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.