> ## Documentation Index
> Fetch the complete documentation index at: https://docs.truagents.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> OAuth 2.0 client_credentials flow with rotating refresh tokens. Use this guide to wire your service to any TruAgents public API.

<Warning>
  **Work in Progress — for partner review.** The flow described below is the
  design TruAgents is implementing; endpoint URLs are final. Self-service
  provisioning is available in **Beta** — with organization admin access you
  can create and manage `client_id` / `client_secret` pairs under
  **Settings → API Keys**. Otherwise, a TruAgents administrator can provision
  credentials for you.
</Warning>

## Summary

TruAgents public APIs are authenticated with **OAuth 2.0 `client_credentials`** ([RFC 6749 §4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)). A partner application exchanges a long-lived `client_id` + `client_secret` for a short-lived access token, then sends the access token on every API request.

The authorization server is operated by TruAgents — there is no external identity provider to register with. **All requests to the token endpoint and the API must use HTTPS.** Never send credentials over unencrypted connections.

| Property               | Value                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------ |
| Flow                   | `client_credentials` (service-to-service)                                            |
| Token format           | Opaque bearer string                                                                 |
| Access-token TTL       | 15 minutes                                                                           |
| Refresh-token TTL      | 30 days, single-use (rotated on every refresh)                                       |
| Client-secret rotation | Manual, administrator-driven                                                         |
| Token endpoint         | `POST /oauth/token`                                                                  |
| Authentication method  | `client_secret_basic` (HTTP Basic) — see [Future auth methods](#future-auth-methods) |

## Machine-readable specification

The OAuth 2.0 token endpoint described on this page — `POST /oauth/token`, with `client_credentials` and `refresh_token` grants — is included in TruAgents' single OpenAPI 3.0.3 specification, which also covers the [Unsubscribe API](/integrations/unsubscribe-api). Generate client SDKs from it, or import it into Postman / Insomnia for interactive exploration.

<Card title="OpenAPI specification" icon="file-code" href="/openapi/truagents.json">
  Download `truagents.json` — covers `POST /oauth/token` and all six `/api/v1/unsubscribe/{email,sms,phone}` operations.
</Card>

<Note>
  **Building in Python?** The first-party [`truagents` SDK](/sdks/python) handles token acquisition, refresh rotation, and family-revoke recovery automatically. Run `pip install truagents` to skip hand-rolling the flow below.
</Note>

## Who this is for

Engineers integrating an external service with a TruAgents public API (today: [Unsubscribe API](/integrations/unsubscribe-api)). The credential is issued for a **set of organizations** in TruAgents — one or more — with one organization designated as the **default**. Per-request targeting within the authorized set, default-when-omitted behavior, and any per-API authorization rules are documented on the API-specific integration page.

## Endpoints

| Environment | Token endpoint                              | API base                               |
| ----------- | ------------------------------------------- | -------------------------------------- |
| Production  | `https://api.truagents.com/oauth/token`     | `https://api.truagents.com/api/v1`     |
| Development | `https://api.dev-truagents.com/oauth/token` | `https://api.dev-truagents.com/api/v1` |

Each environment has independent credentials — a `client_id` issued in development does not authenticate in production.

## Credentials

Each integration is authenticated by a `client_id` + `client_secret` pair. The full `client_secret` is shown **once** at creation and never again — copy it into your secret store immediately. A short prefix (`tru_cs_…`) remains visible afterwards for identification in the admin UI.

To view and manage your API keys, go to **Settings → API Keys** (Beta):

<img src="https://mintcdn.com/truagents/r7sFZHuCZim6LCk0/images/truagents-settings-api-keys.png?fit=max&auto=format&n=r7sFZHuCZim6LCk0&q=85&s=1fe9ab0ff9e76dd1e32d59806da085f9" alt="Settings → API Keys page (Beta) showing columns Label, Client ID, Secret prefix, Active, Last used, Created, with a Create API key button" width="1073" height="434" data-path="images/truagents-settings-api-keys.png" />

| Field           | Description                                                                                                       |
| --------------- | ----------------------------------------------------------------------------------------------------------------- |
| `client_id`     | Opaque identifier, not a secret. Safe to log and include in audit trails.                                         |
| `client_secret` | Sensitive bearer credential, prefixed `tru_cs_`. Store it in a secret manager; never commit it to source control. |

Rotation is an administrator action today: a new key is provisioned, you switch your store, and the previous key is deactivated. Deactivated keys reject all `/oauth/token` requests immediately, and all access and refresh tokens issued from them are revoked at the same moment.

## Step 1 — Request an access token

Exchange your `client_id` + `client_secret` for an access token using the `client_credentials` grant. Send the credentials in the HTTP `Authorization` header:

```bash theme={null}
curl -X POST "https://api.truagents.com/oauth/token" \
  -H "Authorization: Basic $(echo -n '<client_id>:<client_secret>' | base64)" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials"
```

<Note>
  If your HTTP client cannot set the `Authorization` header, credentials may be sent as body parameters ([RFC 6749 §2.3.1](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1)) — this method is less safe because the `client_secret` is more likely to appear in server access logs, reverse-proxy logs, and exception traces. Prefer the `Authorization` header form.

  ```bash theme={null}
  curl -X POST "https://api.truagents.com/oauth/token" \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=client_credentials" \
    -d "client_id=<client_id>" \
    -d "client_secret=<client_secret>"
  ```
</Note>

A successful response (token values shortened for readability — actual tokens are longer, opaque strings):

```json theme={null}
{
  "access_token": "tru_at_<access_token>",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "tru_rt_<refresh_token>",
  "refresh_expires_in": 2592000
}
```

| Field                | Meaning                                                                                         |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| `access_token`       | Send this on every API request in the `Authorization: Bearer <token>` header.                   |
| `token_type`         | Always `Bearer`.                                                                                |
| `expires_in`         | Seconds until the access token expires (900 = 15 min).                                          |
| `refresh_token`      | Long-lived credential used to obtain a new access token without re-sending the `client_secret`. |
| `refresh_expires_in` | Seconds until the refresh token expires (2592000 = 30 days).                                    |

Store the `access_token` in memory — losing it on restart just costs one extra `/oauth/token` call. Store the `refresh_token` durably alongside your `client_secret`: it must survive process restarts and must be updated atomically every time you refresh, because the previous value is invalidated by rotation (see [Refresh-token rotation](#refresh-token-rotation)).

## Step 2 — Call the API

Send the access token in the `Authorization` header on every request to the API base:

```bash theme={null}
curl -X GET "https://api.truagents.com/api/v1/unsubscribe/email" \
  -H "Authorization: Bearer tru_at_<access_token>"
```

When the token expires or is revoked, the API returns `401 Unauthorized` with a `WWW-Authenticate` header per [RFC 6750 §3](https://datatracker.ietf.org/doc/html/rfc6750#section-3):

```http theme={null}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="truagents", error="invalid_token", error_description="The access token expired"
```

Treat any `401` as a signal to refresh — see [Step 3](#step-3-refresh-the-access-token).

## Step 3 — Refresh the access token

Use the `refresh_token` grant to obtain a new access token without re-sending the `client_secret`:

```bash theme={null}
curl -X POST "https://api.truagents.com/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=tru_rt_<refresh_token>"
```

The response is the same shape as Step 1 — **a new `access_token` and a new `refresh_token`**:

```json theme={null}
{
  "access_token": "tru_at_<new>",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "tru_rt_<new>",
  "refresh_expires_in": 2592000
}
```

### Refresh-token rotation

Every successful refresh issues a **new** refresh token and **invalidates the previous one**. You must store the latest value returned. This is a [breach-detection mechanic](https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.2.3): if a previously-used refresh token is presented again, TruAgents assumes it was stolen and revokes every active access and refresh token for that `client_id`. The integration must re-authenticate via `client_credentials` (Step 1).

<Note>
  **Concurrency.** Refresh tokens are single-use. If your first refresh request
  succeeded but you didn't receive the response, **do not reuse the refresh
  token** — retry the API call with `client_credentials` (Step 1) to start a new
  session. Reusing a rotated refresh token, whether by a retry after a lost
  response or by a second worker holding the same stored value, is treated as
  evidence of theft: TruAgents revokes every active access and refresh token for
  the `client_id` and you must re-authenticate. If your integration runs
  multiple workers, serialize refresh calls per `client_id` (for example, with a
  mutex or a leader-elected refresher) so only one worker ever presents the
  current refresh token.
</Note>

### When the refresh token is gone

The refresh token can be unusable for three reasons:

* It has expired (30 days since issuance).
* It has been rotated (you already used it for a refresh) — any reuse revokes the family.
* Your administrator deactivated the `client_id` or rotated the `client_secret`.

In all three cases, `POST /oauth/token` with `grant_type=refresh_token` returns `400 invalid_grant`. Fall back to Step 1 (`client_credentials`) to start a new session.

## Renewal pattern

Use the **reactive** pattern — send the request with the access token you hold, refresh on `401`. The TruAgents authorization server is the authoritative clock, which avoids the drift bugs a proactive expiry check introduces.

```text theme={null}
function call(request):
    response = send(request, access_token)
    if response.status != 401:
        return response

    if have_refresh_token:
        try:
            new_tokens = POST /oauth/token
                grant_type=refresh_token
                refresh_token=current_refresh_token
            access_token  = new_tokens.access_token
            refresh_token = new_tokens.refresh_token   # rotated
            return send(request, access_token)
        catch invalid_grant:
            pass  # fall through

    # No refresh token, or it failed — re-auth from credentials
    new_tokens = POST /oauth/token
        grant_type=client_credentials
        Authorization: Basic <client_id:client_secret>
    access_token  = new_tokens.access_token
    refresh_token = new_tokens.refresh_token
    return send(request, access_token)
```

Proactive renewal (checking `expires_in` against a local clock) is also acceptable — keep the `401` retry path as a fallback for clock drift and mid-cycle revocation.

## Error responses

`/oauth/token` returns standard OAuth 2.0 error envelopes per [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2):

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "The refresh token has expired"
}
```

| `error`                  | When                                                                                             |
| ------------------------ | ------------------------------------------------------------------------------------------------ |
| `invalid_request`        | Malformed request — missing `grant_type`, wrong content type, etc.                               |
| `invalid_client`         | Authentication failed — wrong `client_id`, wrong `client_secret`, or the credential is inactive. |
| `invalid_grant`          | The supplied refresh token is expired, already rotated, or revoked.                              |
| `unsupported_grant_type` | `grant_type` is not `client_credentials` or `refresh_token`.                                     |

API endpoints return `401` with `WWW-Authenticate: Bearer error="invalid_token"` when the access token is missing, expired, or revoked. See the integration page (e.g. [Unsubscribe API](/integrations/unsubscribe-api)) for per-resource error codes.

## Rate limits

The token endpoint and API endpoints have independent rate limits applied per `client_id`. Exceeding a limit returns `429 Too Many Requests` with a `Retry-After` header.

| Endpoint group                  | Default ceiling                        |
| ------------------------------- | -------------------------------------- |
| `POST /oauth/token`             | 10 requests per minute per `client_id` |
| `/api/v1/**` resource endpoints | 60 requests per minute per `client_id` |

Limits are scoped to the `client_id` as a whole — they do **not** split per organization when one credential is authorized across several organizations. If your integration fans across many organizations and the default ceiling is too tight, contact your TruAgents administrator to request a bump.

## Future auth methods

The token endpoint is structured to accept additional transport-layer protections alongside `client_secret_basic`. This is roadmap-only — **not** available at v1:

* **Optional mTLS transport binding** — TruAgents may pin a client certificate to the `client_id` as an additional transport-layer identity check, layered on top of `client_secret_basic`. Not a replacement for the shared secret.

## What this page does not cover yet

* A discovery document at `/.well-known/oauth-authorization-server` ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)).
* Token introspection ([RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662)) — clients do not need it for the current public APIs.

## Related pages

* [Integrations overview](/integrations/overview)
* [Unsubscribe API](/integrations/unsubscribe-api)
* [Errors and status codes](/developers/errors-and-status-codes)
* [Rate limits and retries](/developers/rate-limits-and-retries)
