When to use machine access
Use this pattern when your backend needs to:- Provision or update users via the Builder API.
- Mint user-scoped JWTs for end-users already known to your system.
- Read aggregated usage data from the Usage API.
- Complete a device authorization on behalf of a user (RFC 8693 token exchange).
Prerequisites
- A confidential M2M client (
m2m_…id andpmth_cs_…secret). See Client model. - The M2M client has been granted the scopes required by the endpoint you are calling.
Option A: Client credentials grant
Exchange your credentials for a short-lived Bearer token, then use that token for subsequent API calls. This is the preferred pattern when making multiple API calls in the same request lifecycle, because it decouples token acquisition from the API call.1. Obtain a machine token
The token endpoint is published in OIDC discovery undertoken_endpoint. For convenience it is stable at {issuer}/token.
2. Call the Builder API with Bearer auth
Option B: HTTP Basic auth
For single calls where acquiring a separate token adds unnecessary latency, pass the M2M credentials directly using HTTP Basic auth (RFC 7617). PymtHouse accepts Basic auth on all Builder API endpoints.clientId in the URL path must match the authenticated confidential client’s associated app. A valid credential from a different app’s M2M client returns 404.
Choosing between Bearer and Basic
| Client credentials (Bearer) | HTTP Basic | |
|---|---|---|
| Round trips | 2 (token + API) | 1 |
| Best for | Batching multiple API calls | Single, isolated calls |
| Credential exposure | Token (short-lived) per API call | Raw credentials per API call |
| Standard | RFC 6749 §4.4 | RFC 7617 |
Error responses
| Status | Condition |
|---|---|
400 Bad Request | Malformed token request body, unknown grant_type, or unsupported scope. |
401 Unauthorized | Invalid client_id, wrong client_secret, or expired Bearer token. |
403 Forbidden | Valid credentials, but the client does not have the required scope for this endpoint. |
404 Not Found | Valid credentials, but the M2M client does not belong to the app in the URL path. |
Rotating client secrets
Rotate M2M secrets through the app credentials endpoint in the developer dashboard or admin API. After rotation:- Update the secret in your backend’s secret manager.
- Redeploy or restart the service.
- Do not rotate the public client secret — public clients must remain secretless.
Key design decisions
- Basic auth is supported alongside Bearer. Confidential server-to-server clients are common in automation tooling where adding a token exchange step adds operational friction. Supporting both modes simplifies bootstrapping and scripting without compromising security, since the credential type (M2M secret) carries the same privilege either way.
- Tenant boundary on path, not query parameter. Enforcing
clientIdas a URL path segment rather than a query parameter makes the tenant scope visible and cache-key-safe. The route handler resolves the OAuthclient_idto an internal record before any query, keeping the public API free of internal IDs. - Short-lived machine tokens, not long-lived API keys. Using the standard client credentials flow means PymtHouse does not need a separate API-key issuance system. Short lifetimes limit the blast radius of a compromised token without requiring explicit revocation infrastructure.
Implementation tasks
- Store
M2M_IDandM2M_SECRETin your backend secret manager (e.g., HashiCorp Vault, AWS Secrets Manager, Vercel environment variables). Do not commit them to source control. - Implement a simple in-process token cache: acquire a machine token once per request batch, reuse it, and let it expire naturally rather than building explicit refresh logic.
- For HTTP Basic auth calls, ensure your HTTP client encodes the credentials correctly (
base64(client_id + ":" + client_secret)). Most libraries handle this via aauthoruser/passwordfield. - Test that Basic auth calls to a different app’s
clientIdreturn404, not403— this verifies that the tenant boundary is enforced, not just that the credentials are valid.