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
Parameter Required Description
grant_type Yes Must be client_credentials
client_id Conditional Client UUID. Required if not using Basic auth.
client_secret Conditional Client secret in plaintext. Required if not using Basic auth.
{
"access_token": "eyJhbGciOiJSUzI1NiJ9...",
"token_type": "bearer",
"expires_in": 3600,
"scope": "api:read audit:read"
}
Field Description
access_token Signed JWT bearer token
token_type Always bearer (lowercase)
expires_in Token lifetime in seconds
scope Space-separated scopes granted to this token

The issued JWT carries M2M-specific claims:

Claim Description
sub Client UUID (same as client_id)
client_id Client UUID
tenant_id Org/tenant UUID (null for platform-wide clients)
scope Space-separated scope string
token_type Always "m2m" — distinguishes M2M tokens from user JWTs
rate_limit_tier Rate limit tier assigned to this client (standard, premium, or unlimited)
iat Unix timestamp — token issued at
exp Unix 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.

Scope Access
api:read Read access to core API endpoints
api:write Write access to core API endpoints
admin:read Read access to admin endpoints
admin:write Write access to admin endpoints
audit:read Access to audit log search and export endpoints
dlp:read Read 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 case Recommended scopes
Read audit logs for SIEM export audit:read
Submit chat completions from a backend service api:write
Inspect DLP rule configuration dlp:read
Read org configuration admin:read
Automated policy management admin: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: Use the dedicated POST /api/v1/admin/oauth-clients/{client_id}/rotate-secret endpoint. This generates a new secret and preserves the previous secret in a grace window (default 3600 seconds). During the grace period, both the old and new secrets are accepted, enabling zero-downtime rotation. After the grace period expires, only the new secret is accepted. The new plaintext secret is returned once in the rotation response and is not retrievable again.
  • 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:

Header Description
X-RateLimit-Limit Maximum requests per minute
X-RateLimit-Remaining Remaining requests in the current window
X-RateLimit-Reset Unix timestamp when the window resets

On rate limit exceeded:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Tier Description
standard Default tier — suitable for most service accounts
premium Higher request limits for high-throughput workloads
unlimited No 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 status error value Cause
400 unsupported_grant_type grant_type is missing or not client_credentials
401 invalid_client Credentials 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/v1/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/v1/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/v1/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.