Skip to main content
Machine access is the authentication pattern for server-to-server calls: your backend presents its M2M client credentials directly to PymtHouse without user involvement. This follows the OAuth 2.0 client credentials grant (RFC 6749 §4.4) and the HTTP Basic authentication scheme (RFC 7617) for Builder API calls.

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).
Do not use this pattern for interactive user login flows. For those, see Interactive login or Device flow.

Prerequisites

  • A confidential M2M client (m2m_… id and pmth_cs_… secret). See Client model.
  • The M2M client has been granted the scopes required by the endpoint you are calling.
export BASE_URL="https://your-pymthouse.example"
export M2M_ID="m2m_yourClientId"
export M2M_SECRET="pmth_cs_yourSecret"

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 under token_endpoint. For convenience it is stable at {issuer}/token.
MACHINE_TOKEN=$(curl -sS \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=${M2M_ID}" \
  -d "client_secret=${M2M_SECRET}" \
  -d "scope=users:write users:token" \
  "${BASE_URL}/api/v1/oidc/token" | jq -r '.access_token')
Scope selection. Request only the scopes your call sequence requires. The full list of available M2M scopes is in Client model — M2M scopes. Token lifetime. Machine tokens are short-lived. Cache and reuse the token for the duration of a request batch, then discard it. Do not persist machine tokens across application restarts — just acquire a new one.

2. Call the Builder API with Bearer auth

curl -sS \
  -H "Authorization: Bearer ${MACHINE_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{ "externalUserId": "user-123", "email": "alice@example.com", "status": "active" }' \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/users"

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.
curl -sS \
  -u "${M2M_ID}:${M2M_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{ "externalUserId": "user-123", "email": "alice@example.com", "status": "active" }' \
  "${BASE_URL}/api/v1/apps/${PUBLIC_CLIENT_ID}/users"
The tenant boundary is enforced identically in both authentication modes: the 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 trips2 (token + API)1
Best forBatching multiple API callsSingle, isolated calls
Credential exposureToken (short-lived) per API callRaw credentials per API call
StandardRFC 6749 §4.4RFC 7617
For high-throughput backends making many calls in sequence, acquire one token and reuse it for the batch. For low-frequency automations and operational scripts, Basic auth is simpler.

Error responses

StatusCondition
400 Bad RequestMalformed token request body, unknown grant_type, or unsupported scope.
401 UnauthorizedInvalid client_id, wrong client_secret, or expired Bearer token.
403 ForbiddenValid credentials, but the client does not have the required scope for this endpoint.
404 Not FoundValid 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:
  1. Update the secret in your backend’s secret manager.
  2. Redeploy or restart the service.
  3. Do not rotate the public client secret — public clients must remain secretless.

Key design decisions

  1. 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.
  2. Tenant boundary on path, not query parameter. Enforcing clientId as a URL path segment rather than a query parameter makes the tenant scope visible and cache-key-safe. The route handler resolves the OAuth client_id to an internal record before any query, keeping the public API free of internal IDs.
  3. 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_ID and M2M_SECRET in 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 a auth or user/password field.
  • Test that Basic auth calls to a different app’s clientId return 404, not 403 — this verifies that the tenant boundary is enforced, not just that the credentials are valid.