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

# Token exchange reference

> Request and response fields, validation rules and errors for the workload identity token exchange endpoint.

The token exchange endpoint trades a subject token from your identity provider for a short-lived Reducto access token. It implements [OAuth 2.0 Token Exchange (RFC 8693)](https://datatracker.ietf.org/doc/html/rfc8693). For concepts and setup, see [Workload identity federation](https://docs.reducto.ai/security/workload-identity-federation.md).

The Reducto Python and Node.js SDKs call this endpoint for you. Call it directly when you use another language or want to manage the access token yourself.

## Endpoint

```text theme={null}
POST https://platform.reducto.ai/oauth/token
```

Use the host of your region: `platform.reducto.ai` (US), `eu.platform.reducto.ai` (EU) or `au.platform.reducto.ai` (AU). The endpoint accepts `application/json` or `application/x-www-form-urlencoded` bodies. It needs no `Authorization` header.

```python Python theme={null}
import os

import httpx

response = httpx.post(
    "https://platform.reducto.ai/oauth/token",
    json={
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
        "subject_token": os.environ["SUBJECT_TOKEN"],
        "identity_provider_id": os.environ["REDUCTO_IDENTITY_PROVIDER_ID"],
    },
)
response.raise_for_status()
token = response.json()
print(token["expires_at"])
```

```javascript Node.js theme={null}
const response = await fetch("https://platform.reducto.ai/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
    subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
    subject_token: process.env.SUBJECT_TOKEN,
    identity_provider_id: process.env.REDUCTO_IDENTITY_PROVIDER_ID,
  }),
});
if (!response.ok) {
  throw new Error(`Token exchange failed: ${await response.text()}`);
}
const token = await response.json();
console.log(token.expires_at);
```

```bash cURL theme={null}
curl -X POST "https://platform.reducto.ai/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
  --data-urlencode "subject_token=$SUBJECT_TOKEN" \
  --data-urlencode "identity_provider_id=$REDUCTO_IDENTITY_PROVIDER_ID"
```

## Request fields

| Field | Required | Description |
| - | - | - |
| `grant_type` | Yes | Must be `urn:ietf:params:oauth:grant-type:token-exchange`. |
| `subject_token_type` | Yes | `urn:ietf:params:oauth:token-type:jwt` or `urn:ietf:params:oauth:token-type:id_token`. Reducto verifies both types the same way. Use `id_token` for OIDC ID tokens, such as Google metadata server tokens, and `jwt` for everything else. |
| `subject_token` | Yes | The OIDC JWT or SPIFFE JWT-SVID from your identity provider. |
| `identity_provider_id` | Yes | The ID of the Reducto identity provider, `wip_` followed by 24 characters. |

Reducto ignores `scope`, `resource`, `audience` and `requested_token_type` if you send them. Access is decided by the matching mapping only.

## Response

A successful exchange returns HTTP 200:

```json theme={null}
{
  "access_token": "rdat_eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJvcmdfaWQiOiJvcmdfN...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "expires_at": 1793329845
}
```

| Field | Type | Description |
| - | - | - |
| `access_token` | String | The Reducto access token. Send it as `Authorization: Bearer <access_token>`. Treat it as opaque: the `rdat_` prefix is stable, the rest can change. |
| `issued_token_type` | String | Always `urn:ietf:params:oauth:token-type:access_token`. |
| `token_type` | String | Always `Bearer`. |
| `expires_in` | Integer | Lifetime in seconds, counted from issue. Time spent in transit uses part of it. |
| `expires_at` | Integer | Expiry as a Unix timestamp in seconds (UTC). |

The response has no refresh token. To renew, exchange a fresh subject token before `expires_at`.

## Access token lifetime

The access token expires at the earliest of:

* The matching mapping's `token_lifetime_seconds` (60 to 3600, default 3600).
* 3600 seconds after issue.
* The subject token's `exp`.

A subject token that expires in 5 minutes gives an access token that expires in 5 minutes or less.

## Subject token validation

Reducto verifies the subject token before it evaluates mappings. The token must:

* Be a compact JWT of at most 16 KiB.
* Have a `kid` header and an `alg` header of `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, `ES512` or `EdDSA`. Reducto rejects `none` and the `HS*` family.
* Have a valid signature from a key in the provider's key source, matched by `kid`.
* Have an `iss` claim equal to the provider issuer. A trailing slash on either side is ignored. Everything else, including scheme and path, must match.
* Have an `aud` claim that equals, or is a list that contains, the provider audience.
* Have `sub`, `exp` and `iat` claims.
* Be within its validity window. Reducto allows 30 seconds of clock skew on `exp` and `iat`.
* Have been issued less than 24 hours ago (`iat`), even if `exp` is later.

Reducto does not reject a subject token that it has seen before. You can exchange the same token more than once while it is valid, for example when a token file has not rotated yet.

After verification, Reducto runs the provider's attribute transformations, then evaluates every enabled mapping of the provider. Exactly one mapping must match. See [Mapping resolution](https://docs.reducto.ai/security/workload-identity-federation.md#mapping-resolution).

## Errors

A failed exchange returns HTTP 400, or HTTP 503 for `temporarily_unavailable`, with an [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) error body:

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "The subject token claims are invalid or expired."
}
```

| `error` | Cause |
| - | - |
| `invalid_request` | A required field is missing, the body is not valid JSON or form data, the body is larger than 64 KiB, or `subject_token_type` is not one of the two supported values. |
| `unsupported_grant_type` | `grant_type` is not `urn:ietf:params:oauth:grant-type:token-exchange`. |
| `invalid_grant` | The provider, the subject token or the mapping failed. `error_description` names the stage. |
| `temporarily_unavailable` | HTTP 503. Token exchange is not available in this region right now, or Reducto could not reach your provider's discovery document or JWKS (a timeout, a connection failure or an HTTP 5xx from your provider). Retry with backoff. |

`invalid_grant` covers these stages:

| Stage | `error_description` | Typical causes |
| - | - | - |
| Malformed | `The subject token is not a valid JWT.` | The token is not a JWT, is larger than 16 KiB, has no `kid`, or uses an unsupported `alg`. |
| Provider | `The identity provider is unknown or disabled.` | The provider ID is unknown in this region, or the provider is disabled. |
| Keys | `The signing key for the subject token could not be resolved.` | Your provider's discovery document or JWKS returned an error other than HTTP 5xx (for example HTTP 404) or is not valid, or no key matches the token `kid`. |
| Signature | `The subject token signature is invalid.` | The token was not signed by the key with its `kid`. |
| Claims | `The subject token claims are invalid or expired.` | `iss` or `aud` does not match, a required claim is missing, or the token is expired, not yet valid, or older than 24 hours. |
| Transform | `An attribute transformation failed for the subject token.` | A transformation claim path does not exist in the token, or its value is not a string, an integer or a boolean. |
| Mapping | `The subject token does not match exactly one enabled mapping.` | No enabled mapping matches, or more than one matches. |

`error_description` does not include your issuer, audience or mapping values. Admins can see the full reason for each failure in Studio under the exchange history. Match on the `error` field in code; the wording of `error_description` can change.

## Rate limits

Reducto limits `POST /oauth/token` per client IP address at the API edge. The default limit is 300 requests in 5 minutes. A request over the limit returns HTTP 429 from the edge. The body of this response does not use the `{error, error_description}` format, so check the status code.

* Cache the access token and use it until shortly before `expires_at`. Do not exchange a token for each API request.
* Share one client, or one cached token, between the threads and workers of a process. The SDKs do this for each client.
* Workloads that share one outbound IP address, such as pods behind a NAT gateway, share the limit.
* After a 429, retry with exponential backoff.

The limit applies only to the token endpoint. A 429 from `POST /oauth/token` means that you are over this limit. It is not one of the infrastructure 429s described in [Rate limits](https://docs.reducto.ai/reference/rate-limits.md).

## Authorization behavior

An access token authenticates as your organization, the same way an API key does. It works on every endpoint that accepts an API key, with the same rate limits, billing checks and organization settings. Usage from an access token is attributed to the mapping that issued it.

An access token works only in the region that issued it.

Reducto checks the provider and mapping behind every access token. If you disable or delete either one, its access tokens fail with HTTP 401 within 60 seconds.

Requests made with an expired or revoked access token return HTTP 401. The SDKs handle this by exchanging once more and retrying the request.

## Limitations

* Only JWT subject tokens are supported: OIDC JWTs and SPIFFE JWT-SVIDs. X.509 certificates, SPIFFE X.509-SVIDs and AWS SigV4 credentials are not supported.
* A mapping grants full organization access. Mappings cannot narrow access to some endpoints.
* Identity providers and mappings are managed in Studio. There is no public management API yet.
* Each organization can have 50 identity providers, and each provider 50 mappings.
