Skip to content

OAuth 2.0 Machine-to-Machine API

Arbitex supports the OAuth 2.0 client_credentials grant flow for machine-to-machine (M2M) API access. Use M2M tokens to authenticate service accounts, CI/CD pipelines, and backend integrations without user interaction.


M2M authentication is appropriate when:

  • A backend service or CI/CD pipeline makes requests to the Arbitex API without a human user logged in.
  • You want time-limited, automatically rotated credentials instead of long-lived API keys.
  • You need fine-grained scope control over what the service account can access.

For interactive user authentication and browser-based API key management, use API key management instead.

For managing the client registrations that back M2M tokens, see OAuth client administration.


POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded

This endpoint is public — no existing user JWT is required. The client authenticates using its own credentials.

You can supply credentials in either of two ways. HTTP Basic auth takes precedence if both are present.

Option 1 — HTTP Basic auth (recommended)

POST /api/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials

Option 2 — Request body

grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
ParameterRequiredDescription
grant_typeYesMust be client_credentials
client_idConditionalClient UUID. Required if not using Basic auth.
client_secretConditionalClient secret in plaintext. Required if not using Basic auth.
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "api:read audit:read"
}
FieldDescription
access_tokenSigned JWT bearer token
token_typeAlways bearer (lowercase)
expires_inToken lifetime in seconds
scopeSpace-separated scopes granted to this token

The issued JWT carries M2M-specific claims:

ClaimDescription
subClient UUID (same as client_id)
client_idClient UUID
tenant_idOrg/tenant UUID (null for platform-wide clients)
scopeSpace-separated scope string
token_typeAlways "m2m" — distinguishes M2M tokens from user JWTs
rate_limit_tierRate limit tier assigned to this client (standard, premium, or unlimited)
iatUnix timestamp — token issued at
expUnix timestamp — token expiry

Scopes control which API endpoints the M2M token can access. Assign scopes when creating or updating a client in the admin console.

ScopeAccess
api:readRead access to core API endpoints
api:writeWrite access to core API endpoints
admin:readRead access to admin endpoints
admin:writeWrite access to admin endpoints
audit:readAccess to audit log search and export endpoints
dlp:readRead access to DLP rule and result endpoints

Scopes are additive — a token with api:read audit:read can access both API and audit endpoints.

Scope enforcement only applies to M2M tokens (token_type: "m2m"). User JWTs and API-key-authenticated requests use RBAC and are not subject to OAuth scope checks.

Use caseRecommended scopes
Read audit logs for SIEM exportaudit:read
Submit chat completions from a backend serviceapi:write
Inspect DLP rule configurationdlp:read
Read org configurationadmin:read
Automated policy managementadmin:write

  • Lifetime: Tokens are valid for the expires_in period from issue time. The default is 3600 seconds (1 hour); the maximum is 86400 seconds (24 hours). Lifetime is configured per-client in the admin console.
  • No refresh tokens: The client_credentials grant does not issue refresh tokens. Request a new token when the current one expires.
  • Token refresh timing: A common pattern is to refresh when less than 10% of the token lifetime remains (e.g., with a 3600-second token, refresh at 3240 seconds).
  • Secret rotation: Client secrets can be rotated by deleting and recreating the client registration in the admin console. All tokens issued with the old secret are immediately invalidated.
  • Revocation: Individual tokens cannot be revoked before expiry. To revoke access immediately, delete the client record.

M2M tokens are subject to rate limits based on the rate_limit_tier assigned to the OAuth client. Rate limit headers are returned on every response:

HeaderDescription
X-RateLimit-LimitMaximum requests per minute
X-RateLimit-RemainingRemaining requests in the current window
X-RateLimit-ResetUnix timestamp when the window resets

On rate limit exceeded:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
TierDescription
standardDefault tier — suitable for most service accounts
premiumHigher request limits for high-throughput workloads
unlimitedNo rate limiting applied

See Rate limiting for per-tier request limits and retry guidance.


All errors follow RFC 6749 format:

{
"error": "invalid_client",
"error_description": "Invalid client credentials"
}
HTTP statuserror valueCause
400unsupported_grant_typegrant_type is missing or not client_credentials
401invalid_clientCredentials missing, malformed, not found, or client is disabled

When credentials are rejected, the response includes WWW-Authenticate: Basic realm="oauth".


Terminal window
# Get a token
TOKEN=$(curl -s -X POST https://api.arbitex.ai/api/oauth/token \
-d "grant_type=client_credentials" \
-d "client_id=${ARBITEX_CLIENT_ID}" \
-d "client_secret=${ARBITEX_CLIENT_SECRET}" \
| jq -r .access_token)
# Use the token to search audit logs
curl -H "Authorization: Bearer ${TOKEN}" \
"https://api.arbitex.ai/api/admin/audit-logs/?limit=50"
Terminal window
CREDS=$(echo -n "${ARBITEX_CLIENT_ID}:${ARBITEX_CLIENT_SECRET}" | base64)
TOKEN=$(curl -s -X POST https://api.arbitex.ai/api/oauth/token \
-H "Authorization: Basic ${CREDS}" \
-d "grant_type=client_credentials" \
| jq -r .access_token)
import os
import time
import requests
class ArbitexM2MClient:
"""M2M client with automatic token refresh."""
TOKEN_URL = "https://api.arbitex.ai/api/oauth/token"
def __init__(self, client_id: str, client_secret: str) -> None:
self._client_id = client_id
self._client_secret = client_secret
self._token: str | None = None
self._expires_at: float = 0.0
def get_token(self) -> str:
# Refresh when less than 10% of lifetime remains
if self._token is None or time.time() >= self._expires_at:
self._refresh()
return self._token # type: ignore[return-value]
def _refresh(self) -> None:
resp = requests.post(
self.TOKEN_URL,
data={"grant_type": "client_credentials"},
auth=(self._client_id, self._client_secret),
)
resp.raise_for_status()
data = resp.json()
self._token = data["access_token"]
self._expires_at = time.time() + data["expires_in"] * 0.9
def get(self, path: str, **kwargs) -> requests.Response:
return requests.get(
f"https://api.arbitex.ai{path}",
headers={"Authorization": f"Bearer {self.get_token()}"},
**kwargs,
)
client = ArbitexM2MClient(
client_id=os.environ["ARBITEX_CLIENT_ID"],
client_secret=os.environ["ARBITEX_CLIENT_SECRET"],
)
# Search audit logs — client auto-refreshes token as needed
resp = client.get("/api/admin/audit-logs/", params={"action": "dlp_block", "limit": 100})
events = resp.json()["items"]
name: Arbitex Daily Audit Export
on:
schedule:
- cron: '0 6 * * *' # Daily at 06:00 UTC
jobs:
export-audit:
runs-on: ubuntu-latest
steps:
- name: Get M2M token
id: auth
run: |
TOKEN=$(curl -s -X POST https://api.arbitex.ai/api/oauth/token \
-d "grant_type=client_credentials" \
-d "client_id=${{ secrets.ARBITEX_CLIENT_ID }}" \
-d "client_secret=${{ secrets.ARBITEX_CLIENT_SECRET }}" \
| jq -r .access_token)
echo "token=${TOKEN}" >> "$GITHUB_OUTPUT"
- name: Export yesterday's DLP events
run: |
YESTERDAY=$(date -u -d yesterday +%Y-%m-%dT00:00:00Z)
TODAY=$(date -u +%Y-%m-%dT00:00:00Z)
curl -s \
-H "Authorization: Bearer ${{ steps.auth.outputs.token }}" \
"https://api.arbitex.ai/api/admin/audit-logs/?action=dlp_block&created_after=${YESTERDAY}&created_before=${TODAY}&limit=500" \
> dlp_events.json
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: dlp-events
path: dlp_events.json

  • Store credentials in secrets management. Never hardcode client_id or client_secret in source code or CI/CD configuration files. Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, GitHub Secrets).
  • Use the minimum required scope. Request only the scopes your service needs. A reporting service needs audit:read, not admin:write.
  • Rotate client secrets regularly. Treat M2M client secrets with the same rotation cadence as production API keys.
  • Check expires_in on every token response. Per-client token lifetimes can differ. Always compute the expiry time from the actual response rather than a hardcoded constant.