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

# Workload Token Exchange

> Exchange a CI system's OIDC id_token for a short-lived phantom token — RFC 8693 token exchange on the AI Gateway data plane, with no stored secret in the pipeline.

### Workload token exchange

```http theme={"dark"}
POST https://{tenant-slug}.knoxcall.com/v1/oauth/token
```

Exchange a workload's OIDC **id\_token** — a GitHub Actions token, a GCP or AWS
workload identity token, an Azure managed-identity token — for a short-lived
KnoxCall phantom token, using [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)
token exchange. Nothing long-lived is stored in the pipeline.

<Note>
  **This is not the OAuth 2.1 token endpoint.** `POST https://api.knoxcall.com/oauth/token`
  mints management-API access tokens for `/v1/*`. This endpoint lives on your
  **tenant's data-plane subdomain** under `/v1/oauth/token`, and mints **phantom
  tokens** for `/v1/ai/<agent>` and `/v1/mcp/<slug>`. The two are separate endpoints
  with separate credentials.
</Note>

The endpoint is **unauthenticated by design** — the subject token *is* the
credential, and verifying it is what the endpoint is being asked to do.

**Prerequisite: an OIDC binding**

An exchange only succeeds against a binding you have configured (the agent's
Federation section, or the admin API). A binding pins:

* the trusted **issuer**, whose JWKS is fetched over HTTPS from its published
  discovery document;
* the required **attribute conditions** — for example a specific repository, ref
  or environment;
* the **capability scope** and TTL of the token that gets minted.

A binding with **zero attribute conditions is refused**. A binding that matched any
token from an issuer would hand a KnoxCall credential to every repository on
GitHub.

**Request**

`application/x-www-form-urlencoded` (JSON is also accepted).

```http theme={"dark"}
POST /v1/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<oidc_id_token>
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
```

| Field                | Required | Notes                                                                                                                                                                                                                                               |
| -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grant_type`         | yes      | Must be `urn:ietf:params:oauth:grant-type:token-exchange`.                                                                                                                                                                                          |
| `subject_token`      | yes      | The workload's OIDC id\_token (a compact JWS).                                                                                                                                                                                                      |
| `subject_token_type` | yes      | Must be `urn:ietf:params:oauth:token-type:id_token`.                                                                                                                                                                                                |
| `audience`           | no       | Defaults to KnoxCall's own audience; any other value is refused.                                                                                                                                                                                    |
| `resource`           | no       | [RFC 8707](https://datatracker.ietf.org/doc/html/rfc8707) resource indicator — an MCP resource URI on this host, e.g. `https://acme.knoxcall.com/v1/mcp/weather`. A single string only; an array or object is refused rather than silently dropped. |

**Response**

```json theme={"dark"}
{
  "access_token": "kp_live_a_...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "{\"models\":[\"claude-sonnet-*\"]}"
}
```

`expires_in` is the binding's `access_token_ttl_seconds`, default **900**. `scope`
is a JSON serialisation for visibility only — the enforceable copy is bound into
the token's capability HMAC and cannot be edited after minting.

**What `resource` changes**

| Called with   | Token kind | Where it works                      |
| ------------- | ---------- | ----------------------------------- |
| no `resource` | `agent`    | `/v1/ai/<agent>`                    |
| a `resource`  | `tool`     | **only** the named `/v1/mcp/<slug>` |

A resource-bound token is confined to exactly that MCP server: the canonical
resource URI is bound *into* the capability scope, so the HMAC covers it. The MCP
plane requires a `tool`-kind token, and the AI plane refuses a token with no agent
— so the kind narrows to match what you asked for, and never widens.

**Single use**

A given `subject_token` can be exchanged **once**. A replay returns
`invalid_grant`, which is what stops a leaked CI log from being worth anything.

**Errors**

RFC 6749-shaped: `{ "error": "...", "error_description": "..." }`.

| `error`                  | Cause                                                                                                                                                                        |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unsupported_grant_type` | `grant_type` is not the token-exchange URN.                                                                                                                                  |
| `invalid_request`        | Missing or malformed `subject_token`, wrong `subject_token_type`, or a subject token that is not a JWS triple.                                                               |
| `invalid_target`         | Unsupported `audience`, or a `resource` that is not a valid MCP resource URI on this host.                                                                                   |
| `invalid_grant`          | Signature verification failed, the token is expired or not yet valid, no enabled binding matches its issuer and attributes, or the subject token has already been exchanged. |
| `server_error`           | Issuer JWKS unreachable, or the tenant master key is unavailable.                                                                                                            |

**Rate limits**

30 requests/minute per (client IP, issuer, subject) and 60/minute per client IP.
The client IP is deliberately part of the subject bucket: `iss` and `sub` come from
an unverified JWT and are public for the primary use case, so keying on the claims
alone would let anyone who knows your repository name lock out your real CI runs.
See [Limits](/ai-gateway/limits#request-rate).

**Example — GitHub Actions**

```yaml theme={"dark"}
permissions:
  id-token: write
steps:
  - name: Exchange for a KnoxCall phantom token
    run: |
      OIDC=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
        "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=knoxcall" | jq -r .value)

      KC=$(curl -s -X POST https://acme.knoxcall.com/v1/oauth/token \
        -d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
        -d subject_token="$OIDC" \
        -d subject_token_type=urn:ietf:params:oauth:token-type:id_token \
        | jq -r .access_token)

      curl https://acme.knoxcall.com/v1/ai/support-bot/v1/messages \
        -H "Authorization: Bearer $KC" \
        -H "content-type: application/json" \
        -d '{"model":"claude-sonnet-5","max_tokens":64,"messages":[{"role":"user","content":"hi"}]}'
```

See [Tokens & DPoP](/ai-gateway/tokens-and-dpop#oidc-workload-federation-ci-without-static-secrets)
for the conceptual overview.
