> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Agent access tokens for external harnesses

> Let an agent that runs outside Runtype get short-lived, audience-bound tokens for your APIs and MCP servers.

An agent that runs on its own harness, such as an in-house agent loop on a schedule, often needs to call an API or MCP server that requires a named principal. Runtype issues that agent a short-lived OAuth access token. The token names the agent as its subject and is valid only for one resource that you allow. No person takes part in the request, and the agent never borrows a user's API key.

This guide shows how to allow a resource, give the agent a credential, request a token with `curl`, and verify the token in your API.

> **Note**
>
> Agent access tokens are enabled per organization. If the token endpoint returns
> `unauthorized_client` with the description "Agent tokens are not available for this
> organization", contact Runtype support to enable them.

## Before you begin

Complete the following before the agent requests a token:

* Register the harness as an external agent. See [Bring your own agent](/developer-guides/guides/bring-your-own-agent).
* Only an organization admin can create or revoke the agent's credentials and change its policy. With an API key, the key needs the `AGENTS:WRITE` permission and must belong to an organization admin. Reading them needs `AGENTS:READ`.
* Find the canonical resource URI of each API the agent calls. The canonical form uses `https`, a lowercase host, no default port, no query, no fragment, and no trailing slash, for example `https://mcp.example.com/mcp`.

## How agent tokens work

The agent authenticates to the token endpoint as an OAuth client and names the resource it wants to call:

1. You list the resource in the agent's audience policy, with the scopes a token may carry.
2. You give the agent a credential: a registered public key (recommended) or a client secret.
3. The agent sends a `client_credentials` request to `https://api.runtype.com/v1/identity/token` with the `resource` parameter.
4. Runtype checks the credential and the policy, records the issuance in your organization's audit log, and returns an access token that lives at most 300 seconds.
5. The agent sends the token to your API as a bearer token. Your API verifies it with Runtype's public keys.

The agent's client ID is its agent ID. A credential revocation or a policy change applies to the next token request.

## Allow a resource in the audience policy

The audience policy lists every resource the agent may get tokens for. Replace the whole policy with one request:

**`Replace the audience policy`**

```bash title="Replace the audience policy"
curl -X PUT "https://api.runtype.com/v1/agents/YOUR_AGENT_ID/token-audience-policy" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entries": [
      {
        "resource": "https://mcp.example.com/mcp",
        "allowedGrants": ["principal"],
        "allowedScopes": ["orders:read"],
        "ttlCeilingSeconds": 180
      }
    ]
  }'
```

Replace the following:

* `YOUR_AGENT_ID`: the ID of the external agent, which starts with `agent_`.
* `YOUR_API_KEY`: a Runtype API key, owned by an organization admin, with the `AGENTS:WRITE` permission.

The fields of each entry are:

| Field               | Required | Description                                                                                                 |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `resource`          | Yes      | The resource URI. Runtype stores the canonical form and refuses a URI with a query, fragment, or user info. |
| `allowedGrants`     | Yes      | Include `principal` so the agent can request its own tokens for this resource.                              |
| `allowedScopes`     | No       | The scopes a token for this resource may carry. Leave it out to issue tokens without a `scope` claim.       |
| `ttlCeilingSeconds` | No       | A lifetime from 120 to 300 seconds. Tokens for this resource never live longer.                             |

Each change increases the policy's `policyRevision` and is recorded with you as the actor. To avoid overwriting a teammate's change, send `expectedPolicyRevision`; the request fails with `409` if the policy changed after you read it.

## Get a token with a client secret

A client secret is the shortest path for `curl`, low-code tools, and scripts. The secret starts with `rt_agent_sk_` so secret scanners can flag a leak.

1. Create a secret:

   **`Create a client secret`**

   ```bash title="Create a client secret"
   curl -X POST "https://api.runtype.com/v1/agents/YOUR_AGENT_ID/credentials/secrets" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"lifetimeDays": 90}'
   ```

   The response contains `clientId` and `clientSecret`. Store the secret in the harness's secret manager. Runtype shows it only once and stores only its hash. A secret expires after at most 365 days.

2. Request a token with HTTP Basic authentication:

   **`Request a token with a client secret`**

   ```bash title="Request a token with a client secret"
   curl -X POST "https://api.runtype.com/v1/identity/token" \
     -u "YOUR_AGENT_ID:YOUR_CLIENT_SECRET" \
     -d grant_type=client_credentials \
     -d resource=https://mcp.example.com/mcp \
     -d scope=orders:read
   ```

   Replace `YOUR_CLIENT_SECRET` with the `rt_agent_sk_` secret. To send the credentials in the body instead, pass `client_id` and `client_secret` as form fields.

The response is a standard OAuth token response:

**`Token response`**

```json title="Token response"
{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIs...",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 180,
  "scope": "orders:read"
}
```

An agent can hold two active secrets. To rotate without downtime, create a second secret, switch the harness to it, and then revoke the first one.

## Get a token with private\_key\_jwt

With `private_key_jwt`, the private key stays with the harness and no reusable secret crosses the network. Runtype accepts EC (`ES256`, `ES384`), RSA (`RS256` to `RS512`, `PS256` to `PS512`, 2048 bits or more), and Ed25519 (`EdDSA`) keys.

1. Register the public key. Send only the public JWK; Runtype refuses symmetric keys and any JWK with private members:

   **`Register a public key`**

   ```bash title="Register a public key"
   curl -X POST "https://api.runtype.com/v1/agents/YOUR_AGENT_ID/credentials/keys" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "jwk": {
         "kty": "EC",
         "crv": "P-256",
         "x": "YOUR_KEY_X",
         "y": "YOUR_KEY_Y",
         "kid": "harness-2026-10",
         "alg": "ES256"
       }
     }'
   ```

   Replace `YOUR_KEY_X` and `YOUR_KEY_Y` with the public coordinates of your key. If you leave out `kid`, Runtype uses the key's RFC 7638 thumbprint.

2. Sign a client assertion for each token request. The following Node.js example uses the `jose` library:

   **`Sign a client assertion`**

   ```typescript title="Sign a client assertion"
   import { SignJWT, importJWK } from 'jose'

   const agentId = 'YOUR_AGENT_ID'
   const privateKey = await importJWK(JSON.parse(process.env.AGENT_PRIVATE_JWK!), 'ES256')
   const now = Math.floor(Date.now() / 1000)

   const clientAssertion = await new SignJWT({ jti: crypto.randomUUID() })
     .setProtectedHeader({ alg: 'ES256', kid: 'harness-2026-10', typ: 'client-authentication+jwt' })
     .setIssuer(agentId)
     .setSubject(agentId)
     .setAudience('https://api.runtype.com/orgs/YOUR_ORGANIZATION_ID/agents')
     .setIssuedAt(now)
     .setExpirationTime(now + 60)
     .sign(privateKey)
   ```

   Replace `YOUR_ORGANIZATION_ID` with the ID of the Runtype organization that owns the agent.

3. Request a token with the assertion:

   **`Request a token with private_key_jwt`**

   ```bash title="Request a token with private_key_jwt"
   curl -X POST "https://api.runtype.com/v1/identity/token" \
     -d grant_type=client_credentials \
     -d resource=https://mcp.example.com/mcp \
     -d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
     -d client_assertion=YOUR_CLIENT_ASSERTION
   ```

Runtype refuses an assertion unless all of the following are true:

* `iss` and `sub` both equal the agent ID.
* `aud` is a single string equal to the organization's agents issuer, `https://api.runtype.com/orgs/YOUR_ORGANIZATION_ID/agents`, or its organization issuer, `https://api.runtype.com/orgs/YOUR_ORGANIZATION_ID`. An array is refused.
* The header `alg` matches the registered key, and `kid`, when present, names it.
* `jti` is present and has not been used before. Each assertion works once.
* `exp` minus `iat` is at most 300 seconds, and the assertion has not expired.
* The `typ` header, when present, is `client-authentication+jwt` or `JWT`.

An organization admin can turn on the `requireAgentPrivateKeyJwt` organization setting. The token endpoint then refuses client secrets for every agent in the organization with `invalid_client`.

## Manage credentials

Use the following endpoints to see and retire an agent's credentials:

* `GET /v1/agents/YOUR_AGENT_ID/credentials` lists every key and secret with its `status`: `active`, `expired`, or `revoked`. Secrets appear only as their last four characters.
* `POST /v1/agents/YOUR_AGENT_ID/credentials/CREDENTIAL_ID/revoke` revokes one credential. The next token request with it fails. Tokens issued before the revocation stay valid until they expire, at most 300 seconds later.

Registering and revoking credentials and changing the policy are recorded in your organization's audit log. Each issued token also writes an audit record with the agent, credential, resource, scopes, lifetime, token ID, and policy revision. The record never contains the token.

## Verify an agent token in your API

Agent tokens are RFC 9068 access tokens signed with the same keys as Runtype identity assertions, published at `https://api.runtype.com/.well-known/jwks.json`. They use a separate issuer, so an API that trusts only `https://api.runtype.com/orgs/YOUR_ORGANIZATION_ID` rejects them without any change.

To accept agent tokens, check the following on every request:

1. The signature verifies with the JWKS key whose `kid` matches the token header, and `alg` is `ES256`.
2. The `typ` header is `at+jwt`.
3. `iss` is exactly `https://api.runtype.com/orgs/YOUR_ORGANIZATION_ID/agents`. Reject every other issuer.
4. `aud` is your API's canonical resource URI.
5. `exp` has not passed, allowing a small clock tolerance.
6. `scope` contains the scope the operation needs, if your API uses scopes.

The following Node.js example uses the `jose` library:

**`Verify an agent access token`**

```typescript title="Verify an agent access token"
import { createRemoteJWKSet, jwtVerify } from 'jose'

const runtypeKeys = createRemoteJWKSet(new URL('https://api.runtype.com/.well-known/jwks.json'))

export async function verifyAgentToken(authorization: string) {
  const token = authorization.replace(/^Bearer /, '')
  const { payload } = await jwtVerify(token, runtypeKeys, {
    issuer: 'https://api.runtype.com/orgs/YOUR_ORGANIZATION_ID/agents',
    audience: 'https://mcp.example.com/mcp',
    algorithms: ['ES256'],
    typ: 'at+jwt',
    clockTolerance: 30,
  })
  const scopes = typeof payload.scope === 'string' ? payload.scope.split(' ') : []
  if (!scopes.includes('orders:read')) throw new Error('Missing scope orders:read')
  return { agentId: payload.client_id as string, subject: payload.sub }
}
```

The token's `sub` is `agent:` followed by the agent ID, and `client_id` is the agent ID. The `runtype` claim holds the organization ID, `principal`, `trigger` (always `external` for this grant), `grant` (always `agent`), and the `policyRevision` the token was issued under. The policy decides which resources a token can reach; your API still decides which operations the agent may perform.

OAuth libraries and MCP clients can discover the issuer from its metadata at `https://api.runtype.com/.well-known/oauth-authorization-server/orgs/YOUR_ORGANIZATION_ID/agents`.

## Token endpoint errors

The token endpoint returns OAuth errors as JSON with `error` and `error_description`. The following table lists the codes:

| `error`                   | Status     | Cause                                                                                                                      |
| ------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`         | 400        | A missing or repeated parameter, more than one authentication method, or an `audience` or `actor_token` parameter.         |
| `invalid_client`          | 401        | The credential is wrong, revoked, or expired, the assertion breaks a rule, or the organization requires `private_key_jwt`. |
| `unauthorized_client`     | 400        | The agent is not an external agent, or agent tokens are not enabled for the organization.                                  |
| `unsupported_grant_type`  | 400        | A grant other than `client_credentials`.                                                                                   |
| `invalid_target`          | 400        | The resource is malformed or is not in the audience policy for the `principal` grant.                                      |
| `invalid_scope`           | 400        | A requested scope is not allowed for the resource.                                                                         |
| `temporarily_unavailable` | 429 or 503 | Too many requests, or issuance is unavailable. Retry after the `Retry-After` header.                                       |
| `server_error`            | 500        | Runtype could not record the issuance, so it issued no token.                                                              |

## Next steps

Use the following pages to continue:

* [Bring your own agent](/developer-guides/guides/bring-your-own-agent): register the harness as an external agent.
* [End-user identity and credentials](/developer-guides/guides/end-user-identity): send a verified end user's identity to your API.
* [Authentication](/developer-guides/getting-started/authentication): create the API key that manages agent credentials.