M2M Clients
OAuth client_credentials for machine-to-machine auth.
M2M (machine-to-machine) clients implement the OAuth 2.0 client_credentials grant. They are designed for backend services and CI pipelines that need to call the Rensei API without a human user session. Unlike rsk_live_* API keys, M2M clients exchange credentials for short-lived JWTs, which limits the blast radius of a leaked credential.
How it works
Tokens are signed HS256 JWTs. The default lifetime is 3600 seconds (1 hour), configurable via M2M_TOKEN_LIFETIME (seconds). Issued tokens are not stored - validation is pure JWT signature checking, making the system stateless and horizontally scalable.
Token claims
Every M2M JWT carries the following claims:
{
"sub": "m2m_abc123...",
"org_id": "org_xxxxxxxxxxx",
"workspace_id": "ws_yyyyyyyyyyy",
"scopes": ["workers:read", "sessions:read"],
"iss": "rensei-platform",
"aud": "rensei-api",
"iat": 1748870400,
"exp": 1748874000
}| Claim | Description |
|---|---|
sub | The client_id (format: m2m_<uuid>) |
org_id | The organization the client belongs to |
workspace_id | The workspace the client belongs to |
scopes | Declared scopes from the client definition |
iss | Always rensei-platform |
aud | Always rensei-api |
Creating a client
M2M clients are managed via the API only - there is no settings UI for them (/settings/m2m-clients is a legacy alias that redirects to the API-keys page).
Valid scopes are workers:read, workers:write, sessions:read, sessions:write, agents:read, agents:write, and *.
curl -X POST https://app.rensei.ai/api/admin/m2m-clients \
-H "Authorization: Bearer rsk_live_<org-wide-key>" \
-H "Content-Type: application/json" \
-d '{
"name": "deploy-pipeline",
"scopes": ["workers:read", "sessions:read"]
}'Response (201):
{
"client": {
"clientId": "m2m_3f9e2a1b4c5d6e7f8a9b0c1d2e3f4a5b",
"clientSecret": "m2ms_6e7f8a9b0c1d...",
"name": "deploy-pipeline",
"orgId": "org_xxxxxxxxxxx",
"scopes": ["workers:read", "sessions:read"],
"createdAt": 1748870400000
}
}Store the clientSecret immediately - it is not retrievable after this response.
Obtaining a token
curl -X POST https://app.rensei.ai/api/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "m2m_3f9e2a1b4c5d6e7f8a9b0c1d2e3f4a5b",
"client_secret": "m2ms_6e7f8a9b0c1d..."
}'Response (200):
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600
}Error responses:
| Error code | Meaning |
|---|---|
invalid_client | Unknown client_id or wrong client_secret |
invalid_token | Token failed signature validation or has expired |
Using the token
Pass the access token as a Bearer header:
curl https://app.rensei.ai/api/org/projects \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."Tokens expire after expires_in seconds. Your service should request a new token before the current one expires. A common pattern is to cache the token and refresh when the remaining lifetime drops below 5 minutes.
TypeScript example
let cachedToken: { value: string; expiresAt: number } | null = null
async function getM2MToken(): Promise<string> {
const now = Math.floor(Date.now() / 1000)
if (cachedToken && cachedToken.expiresAt > now + 300) {
return cachedToken.value
}
const res = await fetch('https://app.rensei.ai/api/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.RENSEI_CLIENT_ID,
client_secret: process.env.RENSEI_CLIENT_SECRET,
}),
})
const data = await res.json()
cachedToken = {
value: data.access_token,
expiresAt: now + data.expires_in,
}
return cachedToken.value
}Listing clients
curl https://app.rensei.ai/api/admin/m2m-clients \
-H "Authorization: Bearer rsk_live_<org-wide-key>"The list response omits clientSecretHash - it returns only metadata (clientId, name, scopes, createdAt, lastUsedAt).
Revoking a client
curl -X DELETE "https://app.rensei.ai/api/admin/m2m-clients?client_id=<clientId>" \
-H "Authorization: Bearer rsk_live_<org-wide-key>"Revocation is immediate. Existing tokens issued to the client continue to pass signature validation until they expire (up to 1 hour). If you need immediate revocation, rotate M2M_JWT_SECRET - this invalidates all outstanding M2M tokens across all clients.
Audit trail
M2M client events are recorded in the audit trail:
| Event | Trigger |
|---|---|
m2m.client_created | New client created |
m2m.token_issued | Token issued (logged per issuance) |
m2m.client_revoked | Client revoked |
Environment variables
| Variable | Required | Description |
|---|---|---|
M2M_JWT_SECRET | Yes | HS256 signing secret. Generate with openssl rand -hex 32. Rotating this immediately invalidates all outstanding tokens. |
M2M_TOKEN_LIFETIME | No (default: 3600) | Token lifetime in seconds. |
M2M_JWT_SECRET is a shared secret. Treat it with the same sensitivity as a root credential. Store it in your secrets manager and rotate it on a schedule.
Related pages
- API Keys - static
rsk_live_*tokens for daemon workers and CI - Members & RBAC - user-level access control
- Audit Trail - token issuance is logged for compliance