> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.emtelligent.com/platform/oauth-clients/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.emtelligent.com/_mcp/server. # OAuth client credentials If your security policy requires OAuth 2.0, your systems can call the APIs with an access token instead of an [API key](/platform/api-keys). Your system holds a client ID and secret, exchanges them for an access token that lasts one hour, and sends that token on each call. This is the client credentials grant of [RFC 6749 §4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). Your own identity provider is not involved: emtelligent issues the credentials. ## Creating a client Anyone in your organization who can create API keys can create an OAuth client. Members see and manage the clients they created; owners and admins see and manage all of them. You choose: * **A label**, such as `claims pipeline`. * **Scopes**: the APIs the client may call. They are the same four scopes an API key has, with the same rule: one for each API your own code calls. * **How the client authenticates** to the token endpoint: | Method | How the client proves itself | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `client_secret_basic` (the default) | The client ID and secret in an HTTP Basic `Authorization` header | | `client_secret_post` | The client ID and secret as form fields in the request body | | `private_key_jwt` | A JWT assertion signed with your own private key. You provide an `https` URL where your public keys are published as a JWKS. There is no secret | The client authenticates only by the method it was created with. A `client_secret_post` client that sends a Basic header is refused. The platform then shows the **client ID** (`oac_…`), the **client secret** (`cs-emt-…`) and the **token URL**. The secret is shown once and emtelligent stores only a hash of it, so store it in your secret manager straight away. ## Getting an access token Set the three values the platform showed you in your environment rather than putting them in code: ```bash export EMT_TOKEN_URL='...' export EMT_CLIENT_ID='oac_...' export EMT_CLIENT_SECRET='cs-emt-...' ``` Then exchange them for a token: ```bash curl -s -u "$EMT_CLIENT_ID:$EMT_CLIENT_SECRET" \ -d grant_type=client_credentials \ -d scope=ocr:process \ "$EMT_TOKEN_URL" ``` ```json { "access_token": "eyJhbGciOiJSUzI1NiIs…", "token_type": "bearer", "expires_in": 3599, "scope": "ocr:process" } ``` `scope` is optional. Without it, the token carries every scope the client was given. Asking for a scope the client was not given returns `invalid_scope`. For `client_secret_post`, send the credentials as form fields instead: `-d client_id=$EMT_CLIENT_ID -d client_secret=$EMT_CLIENT_SECRET`. ## Calling an API with the token Send the access token wherever the API's quickstart sends an API key. The token lasts one hour, and one token serves every call in that hour, so request a new one shortly before it expires rather than for every call. This example keeps one token and replaces it a minute before it expires, then submits a document to the OCR API: ```python import os import time import requests TOKEN_URL = os.environ["EMT_TOKEN_URL"] CLIENT_ID = os.environ["EMT_CLIENT_ID"] CLIENT_SECRET = os.environ["EMT_CLIENT_SECRET"] class AccessToken: """One access token, replaced a minute before it expires.""" def __init__(self): self._token = None self._expires_at = 0.0 def get(self): if self._token is None or time.time() > self._expires_at - 60: response = requests.post( TOKEN_URL, auth=(CLIENT_ID, CLIENT_SECRET), data={"grant_type": "client_credentials", "scope": "ocr:process"}, timeout=10, ) response.raise_for_status() body = response.json() self._token = body["access_token"] self._expires_at = time.time() + body["expires_in"] return self._token token = AccessToken() with open("report.pdf", "rb") as fp: response = requests.post( "https://ocr.emtelligent.com/jobs", data=fp, headers={ "Authorization": f"Bearer {token.get()}", "X-Filename": "report.pdf", }, timeout=120, ) response.raise_for_status() job_id = response.json()["job_id"] ``` Libraries that implement the client credentials grant, such as `requests-oauthlib` and `authlib`, work as well. Point them at the token URL. ## Rotating and revoking * **Rotate the secret** when your policy calls for it. The old secret stops working at the token endpoint immediately. Tokens it already obtained keep working until they expire, which is at most an hour later. * **Revoke the client** if the secret may have leaked. Its tokens are refused within a minute, and it can obtain no new ones. When a member is removed, the OAuth clients they created can be revoked at the same time, as their API keys can. See [Accounts and sign-in](/platform/accounts). ## Usage and billing Work done with a client's tokens is billed to your organization in the same way as work done with an API key. Usage by credential shows it against the client ID. See [Billing and usage](/platform/billing). > Calling the APIs with short-lived OAuth 2.0 access tokens instead of an API key