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.
When to use M2M authentication
Section titled “When to use M2M authentication”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.
Getting a token
Section titled “Getting a token”Endpoint
Section titled “Endpoint”POST /api/oauth/tokenContent-Type: application/x-www-form-urlencodedThis endpoint is public — no existing user JWT is required. The client authenticates using its own credentials.
Credential methods
Section titled “Credential methods”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/tokenAuthorization: Basic base64(client_id:client_secret)Content-Type: application/x-www-form-urlencoded
grant_type=client_credentialsOption 2 — Request body
grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRETRequest parameters
Section titled “Request parameters”| 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. |
Response
Section titled “Response”{ "access_token": "eyJhbGciOiJIUzI1NiJ9...", "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 |
Token payload
Section titled “Token payload”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
Section titled “Scopes”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.
Least-privilege guidance
Section titled “Least-privilege guidance”| 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 |
Token lifecycle
Section titled “Token lifecycle”- Lifetime: Tokens are valid for the
expires_inperiod 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_credentialsgrant 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.
Rate limits
Section titled “Rate limits”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 RequestsRetry-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.
Error responses
Section titled “Error responses”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".
Integration examples
Section titled “Integration examples”curl — form body
Section titled “curl — form body”# Get a tokenTOKEN=$(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 logscurl -H "Authorization: Bearer ${TOKEN}" \ "https://api.arbitex.ai/api/admin/audit-logs/?limit=50"curl — Basic auth
Section titled “curl — Basic auth”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)Python (requests)
Section titled “Python (requests)”import osimport timeimport 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 neededresp = client.get("/api/admin/audit-logs/", params={"action": "dlp_block", "limit": 100})events = resp.json()["items"]GitHub Actions
Section titled “GitHub Actions”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.jsonSecurity considerations
Section titled “Security considerations”- Store credentials in secrets management. Never hardcode
client_idorclient_secretin 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, notadmin:write. - Rotate client secrets regularly. Treat M2M client secrets with the same rotation cadence as production API keys.
- Check
expires_inon every token response. Per-client token lifetimes can differ. Always compute the expiry time from the actual response rather than a hardcoded constant.
See also
Section titled “See also”- OAuth client administration — create, enable/disable, and rotate OAuth client credentials
- API key management — user-facing API key creation and management
- Rate limiting — rate limit tiers and 429 handling
- Audit event API — search and filter audit log entries