> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.emtelligent.com/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).