TL;DR
Every app has two OIDC clients. You use different ones in different places:
The single most common integration mistake: putting
m2m_… where app_… belongs (or vice versa). Keep the env variable names unambiguous:
Why two clients?
OAuth 2.0 distinguishes public clients (those that cannot keep secrets — CLIs, native apps, browsers) from confidential clients (servers that can). A single client cannot satisfy both:- Device flow polling happens on the user’s device — no place to safely store a secret.
- Builder API calls must prove your backend’s identity — a secret is required.
The public client (app_…)
Use the public client wherever the user or device needs to see a client_id:
- The
client_idparameter in device authorization requests - Verification URLs shown to users (
verification_uri_complete) - The
client_id/azpclaim in issued user JWTs - The
{clientId}URL path segment in all Builder API calls
The M2M client (m2m_…)
Use the M2M client exclusively in your server-side backend:
- HTTP Basic auth:
Authorization: Basic base64(m2m_id:m2m_secret) - Client credentials grant to get a machine token
- RFC 8693 token exchange (device completion, signer session)
OAuth scopes
Public client scopes (allowed_scopes)
Configured on the app_… client. They control two things:
- What claims appear in user JWTs — requested scopes are validated against this list
- Billing mode — presence of
users:tokenswitches the app to per-user billing
M2M client scopes (allowed_scopes)
Gate what your backend can do:
Request only the scopes your backend needs. Excess scopes increase risk without benefit.
Billing mode
Theusers:token scope on the public client determines how usage is attributed:
Add
users:token only if your use case requires per-user billing data. It cannot be removed without changing billing attribution behavior.
API keys (pmth_*)
Long-lived opaque keys issued by PymtHouse:
Per-user API keys are created via
POST .../users/{externalUserId}/keys and must be stored securely server-side. Exchange them for short-lived JWTs on demand rather than using them directly in the signing hot path.
Three-sibling pattern (advanced)
Apps that also need browser-based SSO (e.g., a Kong Dev Portal login) register a third client:
The
web_… client registers redirect URIs and supports authorization_code. It is not for Builder API or device flows. See Interactive login.
Checklist before writing code
- Confirm
PYMTHOUSE_PUBLIC_CLIENT_IDstarts withapp_ - Confirm
PYMTHOUSE_M2M_CLIENT_IDstarts withm2m_ - Both credentials belong to the same registered developer app
- M2M secret is stored in your backend secret manager, not in source code
- Public client has
sign:jobinallowed_scopes - M2M client has
users:writeandusers:tokeninallowed_scopes - If per-user billing: public client also has
users:token
Rotate M2M secrets
Rotate viaPOST /api/v1/apps/{clientId}/credentials (provider session) or the credentials page in the dashboard. After rotation:
- Update the secret in your backend secret manager.
- Redeploy or restart your service.
- Do not touch the public client — it has no secret to rotate.