Skip to navigation

Agent access tokens for external harnesses

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.

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.
  • 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
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:

FieldRequiredDescription
resourceYesThe resource URI. Runtype stores the canonical form and refuses a URI with a query, fragment, or user info.
allowedGrantsYesInclude principal so the agent can request its own tokens for this resource.
allowedScopesNoThe scopes a token for this resource may carry. Leave it out to issue tokens without a scope claim.
ttlCeilingSecondsNoA 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
    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
    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
{
"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
    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
    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
    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
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:

errorStatusCause
invalid_request400A missing or repeated parameter, more than one authentication method, or an audience or actor_token parameter.
invalid_client401The credential is wrong, revoked, or expired, the assertion breaks a rule, or the organization requires private_key_jwt.
unauthorized_client400The agent is not an external agent, or agent tokens are not enabled for the organization.
unsupported_grant_type400A grant other than client_credentials.
invalid_target400The resource is malformed or is not in the audience policy for the principal grant.
invalid_scope400A requested scope is not allowed for the resource.
temporarily_unavailable429 or 503Too many requests, or issuance is unavailable. Retry after the Retry-After header.
server_error500Runtype could not record the issuance, so it issued no token.

Next steps

Use the following pages to continue: