Skip to main content

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.
Keeping them separate also means rotating the M2M secret never disrupts active device sessions.

The public client (app_…)

Use the public client wherever the user or device needs to see a client_id:
  • The client_id parameter in device authorization requests
  • Verification URLs shown to users (verification_uri_complete)
  • The client_id / azp claim in issued user JWTs
  • The {clientId} URL path segment in all Builder API calls
Never add a secret to the public client. Device flow polling requires that the public client has no secret — adding one would break every CLI and SDK that polls the device code endpoint.

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)
Never expose the M2M client id or secret in browser JavaScript, CLI binaries, or mobile apps.

OAuth scopes

Public client scopes (allowed_scopes)

Configured on the app_… client. They control two things:
  1. What claims appear in user JWTs — requested scopes are validated against this list
  2. Billing mode — presence of users:token switches 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

The users: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_ID starts with app_
  • Confirm PYMTHOUSE_M2M_CLIENT_ID starts with m2m_
  • 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:job in allowed_scopes
  • M2M client has users:write and users:token in allowed_scopes
  • If per-user billing: public client also has users:token

Rotate M2M secrets

Rotate via POST /api/v1/apps/{clientId}/credentials (provider session) or the credentials page in the dashboard. After rotation:
  1. Update the secret in your backend secret manager.
  2. Redeploy or restart your service.
  3. Do not touch the public client — it has no secret to rotate.