# Client credentials for services

> Get an access token for a service, job or daemon that calls an API without a user, and reuse it until it is about to expire.

Source: https://oltinid.com/docs/guides/client-credentials/ · Section: Guides · All OneiD documentation: https://oltinid.com/llms.txt

When a program calls an API on its own behalf, with no user involved, it uses the client credentials grant. The program authenticates to OneiD with its client ID and secret and receives an access token. This guide shows the request, the response, what the token contains and how to cache it.

## Before you start

Ask your OneiD administrator for a client with:

- client type `confidential`
- the client credentials grant
- the API scopes the service needs, for example `orders.read`

You receive a client ID and a client secret. Public clients cannot use client credentials.

> **Warning:** The client secret is a password for your service. Keep it in a secret store or an environment variable supplied at run time. Never commit it to source control or write it to logs.

## Request a token

Send a POST to the token endpoint with `grant_type=client_credentials` and the scopes you need.

### With client_secret_basic (recommended)

The client ID and secret go in the `Authorization` header, joined by a colon and Base64-encoded.

```http
POST /connect/token HTTP/1.1
Host: YOUR_ONEID
Authorization: Basic WU9VUl9DTElFTlRfSUQ6WU9VUl9DTElFTlRfU0VDUkVU
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=orders.read
```

```bash
curl -s https://YOUR_ONEID/connect/token \
  -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope=orders.read
```

### With client_secret_post

The client ID and secret go in the form body. OneiD accepts this, but prefer `client_secret_basic`, because a body is more likely to be logged by tools along the way.

```http
POST /connect/token HTTP/1.1
Host: YOUR_ONEID
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=orders.read&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
```

```bash
curl -s https://YOUR_ONEID/connect/token \
  -d grant_type=client_credentials \
  -d scope=orders.read \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_CLIENT_SECRET
```

To request several scopes, separate them with spaces: `scope=orders.read orders.write` (URL-encoded as `orders.read%20orders.write`). A scope that the client is not allowed returns `invalid_scope`.

> **Not supported:** OneiD authenticates clients with a client secret only. `private_key_jwt` and mTLS client authentication cannot be used.

## The response

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVYQU1QTEVfS0lEIn0.EXAMPLE_ACCESS_TOKEN_PAYLOAD.EXAMPLE_SIGNATURE",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "orders.read"
}
```

There is no ID token, because no user signed in, and no refresh token. When the access token is about to expire, request a new one with the same call.

`expires_in` is in seconds. Access tokens last 1 hour by default; your administrator can change this for the client.

> **Checkpoint:** The curl command prints JSON with an `access_token` and `"token_type": "Bearer"`. If you see an error instead, check the client ID and secret, then ask your administrator to confirm that the client is enabled, is `confidential` and has the client credentials grant and the scope you requested.

## What the token contains

The access token is a JWT signed with RS256. A decoded payload looks like this:

```json
{
  "iss": "https://YOUR_ONEID/",
  "sub": "YOUR_CLIENT_ID",
  "client_id": "YOUR_CLIENT_ID",
  "name": "Orders sync job",
  "scope": "orders.read",
  "iat": 1767225600,
  "exp": 1767229200
}
```

- **`sub`** is the client ID. There is no user.
- **`name`** is the client's display name, as registered by the administrator.
- **`scope`** lists the granted scopes, separated by spaces.
- There is no `aud` claim. An API checks the issuer, signature, expiry and scope instead.

An API can tell a service token from a user token by comparing `sub` with `client_id`, or by the scopes it requires. See [Protect an API](https://oltinid.com/docs/guides/protect-an-api/).

## Call the API

```http
GET /orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkVYQU1QTEVfS0lEIn0.EXAMPLE_ACCESS_TOKEN_PAYLOAD.EXAMPLE_SIGNATURE
```

## Cache the token

Request a token once and reuse it for every call until it is close to expiry. Requesting a new token for each API call slows your service down and can hit OneiD's rate limits.

A simple pattern:

1. On the first call, request a token and remember it with its expiry time: now plus `expires_in`.
2. Before each API call, reuse the cached token if it has more than a minute left.
3. Otherwise request a new one. If several threads need a token at the same moment, let one request it and the others wait for the result.
4. If the API answers 401, discard the cached token, request a new one and retry once.

```js
let cached = null;

async function getToken() {
  if (cached && cached.expiresAt - Date.now() > 60_000) return cached.token;

  const res = await fetch('https://YOUR_ONEID/connect/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: 'Basic ' + Buffer.from(`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`).toString('base64'),
    },
    body: new URLSearchParams({ grant_type: 'client_credentials', scope: 'orders.read' }),
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status}`);

  const body = await res.json();
  cached = { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
  return cached.token;
}
```

Most OAuth client libraries, and many HTTP client frameworks, cache client credentials tokens for you. Check that yours does before writing your own.

## Handle rate limiting

The token endpoint is rate limited. When a service sends too many requests, OneiD answers HTTP 429:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{"error":"slow_down","error_description":"Too many requests. Try again in 12 seconds."}
```

Wait for the number of seconds in `Retry-After` before trying again. Do not retry in a tight loop. Caching tokens as shown above keeps a service well clear of the limits. See [Rate limits](https://oltinid.com/docs/reference/rate-limits/).

## When the secret changes or expires

If the client secret has expired, OneiD returns `invalid_client` with the description "The client secret has expired." If your administrator disables the secret or the client, existing tokens are revoked and new requests fail. Read the secret from configuration at start-up so you can replace it without a code change.

## Learn more

- [Protect an API](https://oltinid.com/docs/guides/protect-an-api/)
- [curl quickstart](https://oltinid.com/docs/quickstarts/curl/)
- [Rate limits](https://oltinid.com/docs/reference/rate-limits/)
