> Markdown version of https://authpi.com/docs/guides/mcp-token-exchange/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# Let your MCP server call your API for a user

Use your AuthPI issuer to exchange a user's MCP access token for a delegated token that your own API can verify.

Your MCP server receives an access token intended for the MCP resource. To call a different API on that user's behalf, exchange it at your AuthPI issuer for a new token intended for that API. The user remains the subject; your MCP backend is recorded as the service acting for them.

This guide uses **your issuer, your MCP server, and your API**. AuthPI is a multi-tenant identity provider: each issuer defines its own identity and authorization boundary. An organization belongs within an issuer; it is not another issuer. Hosting two issuers on AuthPI does not make their tokens or organization memberships interchangeable.

Looking to connect to AuthPI's hosted MCP instead? See [Use AuthPI MCP](/docs/guides/authpi-mcp/).

## The participants

| Participant | Role in this example |
|---|---|
| Human user | Signs in through your issuer and consents to `documents:read`. |
| MCP consumer | The application connecting to your MCP, using its own OAuth client. It can be a registered public client or an issuer-enabled CIMD client. |
| Your MCP backend | Receives the user's MCP token and authenticates the exchange using a separate, confidential service client. Only this backend holds that client's secret. |
| Your API | Accepts the resulting delegated token for `https://api.example.com` and authorizes access to the selected organization's documents. |

The flow is: user consent → MCP-audience access token → authenticated exchange → API-audience delegated token → your API. The consumer's OAuth client and the backend's exchange client are different clients, registered or resolved within the same issuer.

## Prerequisites

- An AuthPI issuer and its owning account. Use that issuer throughout this flow.
- Your MCP resource at `https://mcp.example.com/mcp` and target API at `https://api.example.com`. Replace these example resource identifiers consistently with your own exact URIs.
- A human user with active membership in an active organization under your issuer, with membership authority for `documents:read`.
- A configured MCP authorization flow. Follow the [MCP resource profile](/docs/reference/oidc/#mcp-resource-profile) with your resource URI and both `openid` and `documents:read` in the issuer's permitted resource scopes. Registered consumer clients also need that resource in `allowed_resources` and `documents:read` in `settings.scopes`. CIMD consumers follow the issuer's CIMD policy, including an organization policy that permits the selected membership.
- An operator [API key](/docs/guides/api-keys/) with `issuers.clients:manage` on the owning account, used only to provision the exchange client. This authority also permits creating the client.
- `curl` and `jq` for the commands below. They call the HTTP endpoints directly.

The user must request and consent to `documents:read` in the original MCP authorization flow, alongside `openid`. Membership authority alone does not supply OAuth consent. The resulting access token must have the MCP resource as its only audience and include the selected organization in its membership claims. If it already has an `org_id`, the exchange must use that same organization. See the [authorization flow](/docs/reference/oidc/#supported-grant-types) and [organization selection](/docs/reference/oidc/#selected-organizations).

## Register the backend's exchange client

Save this complete client-creation body as `exchange-client-request.json`:

```json
{
  "name": "Documents MCP exchange",
  "type": "internal",
  "confidential": true,
  "settings": {
    "protocol": "oidc",
    "scopes": ["documents:read"],
    "openid": {
      "application_type": "m2m",
      "response_types": [],
      "grant_types": ["urn:ietf:params:oauth:grant-type:token-exchange"],
      "redirect_uris": [],
      "token_exchange": {
        "subject_resources": [
          { "resource": "https://mcp.example.com/mcp", "scopes": ["documents:read"] }
        ],
        "target_resources": [
          { "resource": "https://api.example.com", "scopes": ["documents:read"] }
        ],
        "max_access_token_age": 300
      }
    }
  }
}
```

`internal` describes a client owned by you. It does not grant privileges: an `external` client follows the same exchange rules. The explicit `token_exchange` policy grants the capability. This dedicated client uses the exchange grant exclusively; it is not the browser client and does not use `client_credentials`.

Set `AUTHPI_ACCOUNT_ID`, `AUTHPI_ISSUER_ID`, `AUTHPI_KEY_ID`, and `AUTHPI_KEY_SECRET` to your provisioning credentials and identifiers, then create the client once:

```bash
umask 077
curl --fail-with-body --silent --show-error \
  --user "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  --header 'Content-Type: application/json' \
  --data-binary @exchange-client-request.json \
  "https://api.authpi.com/v1/accounts/$AUTHPI_ACCOUNT_ID/issuers/$AUTHPI_ISSUER_ID/clients" \
  > exchange-client.json

EXCHANGE_CLIENT_ID=$(jq -er '.data.id' exchange-client.json)
EXCHANGE_CLIENT_SECRET=$(jq -er '.data.secret' exchange-client.json)
```

Creation returns HTTP `201`. The secret is returned only at creation; store it in your backend's secret manager. The operator API key is for AuthPI configuration, while the exchange client secret authenticates runtime exchanges. Neither belongs in the MCP consumer or browser.

## Exchange a user's MCP token

In your MCP backend, set `MCP_ACCESS_TOKEN` to the verified access token from the incoming MCP request and `ORG_ID` to the organization the user selected. In a multi-issuer backend, select the issuer and exchange credentials from trusted application configuration, not from an unverified token claim.

```bash
TOKEN_RESPONSE=$(curl --fail-with-body --silent --show-error \
  --user "$EXCHANGE_CLIENT_ID:$EXCHANGE_CLIENT_SECRET" \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
  --data-urlencode "subject_token=$MCP_ACCESS_TOKEN" \
  --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
  --data-urlencode 'resource=https://api.example.com' \
  --data-urlencode 'scope=documents:read' \
  --data-urlencode "org=$ORG_ID" \
  "https://idp.authpi.com/$AUTHPI_ISSUER_ID/token")

API_ACCESS_TOKEN=$(printf '%s' "$TOKEN_RESPONSE" | jq -er '.access_token')
```

The response is a delegated access token, not another login or a refresh token. Each requested permission must pass the [scope and membership checks](/docs/reference/oidc/#token-exchange-grant-rfc-8693-constrained-profile). A disallowed scope rejects the entire exchange; the server does not silently return a smaller grant.

## Validate and use the token in your API

Start with [Validate tokens in your API](/docs/guides/validate-tokens/): verify the signature, access-token type, exact trusted issuer, audience, and expiry before reading authorization claims. Configure the audience as your API's resource URI, not the MCP URI.

For a route that requires this delegation, also enforce:

- Exactly your API's single audience, a human `sub` with `dat.type: "identity"`, and a single actor object whose `act.sub` equals `client_id` and identifies a service your API permits.
- `org_id` equals the organization requested by the API route, with exactly that organization in the token's `organizations` array. Scope every data query to that organization under the trusted issuer.
- The route's required permission is in the top-level `scope`; the membership scopes match that delegated grant. Do not substitute broader roles or permissions from another token.

Record the human `sub` and service `act.sub` separately in your audit trail. The [token claims reference](/docs/reference/token-claims/) describes their meanings.

For example, if your API exposes `GET /organizations/{org_id}/documents` with those checks, call it from the MCP backend:

```bash
curl --fail-with-body --silent --show-error \
  --header "Authorization: Bearer $API_ACCESS_TOKEN" \
  "https://api.example.com/organizations/$ORG_ID/documents"
```

This is an example route **you implement**, not an AuthPI endpoint. Verify that it returns only the selected organization's documents and records both identities. Then confirm the same route rejects the original MCP token, an access token from another issuer, and a request for another organization. A read-only delegated token must not authorize writes.

## Handle failures and lifecycle changes

Use the [error reference](/docs/reference/errors/) to distinguish bad client authentication, disallowed exchange policy, rejected subject or membership, and missing scope authority. Changing the service policy cannot add a permission missing from the user's original consent; obtain a new consented MCP grant when that permission is needed.

User and membership status are checked when exchanging. Already-issued delegated tokens remain bounded by their expiry; a later change is not a promise of immediate invalidation at an API using local JWT validation. Follow the [lifetime contract](/docs/reference/oidc/#token-exchange-grant-rfc-8693-constrained-profile), and obtain a new delegated token through a fresh exchange when needed. Never forward the original MCP token to the target API or return the backend's exchange secret to the consumer.

## Next steps

- [RFC 8693 reference](/docs/reference/oidc/#token-exchange-grant-rfc-8693-constrained-profile) — exact request, policy, and token constraints.
- [Use AuthPI MCP](/docs/guides/authpi-mcp/) — the separate first-party integration for AuthPI itself.