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

# Agent Accounts

> Learn about Runlayer Agent Accounts, how to create them, and how to authenticate your AI applications with the platform.

<Warning>
  **Beta Feature** - Agent Accounts is currently in beta and requires access approval.
</Warning>

Agent Accounts in Runlayer represent AI applications or services that interact with MCP servers on behalf of users or autonomously. They are the primary way to programmatically connect your AI applications to the Runlayer platform.

## What is an Agent Account?

An Agent Account is a registered client application that can:

* **Authenticate programmatically** using OAuth 2.0 client credentials
* **Call MCP tools** through the Runlayer proxy
* **Act on behalf of users** when delegated permissions
* **Operate autonomously** with its own policies

Each agent account receives a unique **Client ID** and **Client Secret** upon creation, which are used to authenticate API requests.

## Token Types

Agent accounts can obtain two types of access tokens depending on their use case:

### M2M Token (Machine-to-Machine)

Use M2M tokens when your agent account operates **autonomously** without a specific user context.

* Agent account operates independently
* Only agent account-level policies are enforced
* Ideal for background jobs, scheduled tasks, or autonomous AI agents

### OBO Token (On-Behalf-Of)

Use OBO tokens when your agent account acts **on behalf of a specific user**.

* Agent account operates in the context of a delegating user
* Intersection of agent account and user policies are enforced
* Requires an active delegation from the user to the agent account
* Ideal for user-facing AI assistants or copilots

## How OBO Works

The complete On-Behalf-Of flow, end to end: a one-time delegation from the user, a two-step token flow (mint an agent token, then exchange it for an OBO token per RFC 8693), and a proxied MCP call that enforces both parties' policies and resolves OAuth credentials through session grants.

```mermaid theme={null}
sequenceDiagram
    autonumber
    actor User as User (delegator)
    participant App as Your Application
    participant RL as Runlayer
    participant MCP as MCP Server (upstream)

    Note over User,RL: One-time setup (Runlayer UI)
    User->>RL: Connect to the agent account
    Note over User,RL: Creates the delegation, plus session grants<br/>for OAuth-protected servers

    Note over App,RL: Step 1 — mint an agent token
    App->>RL: POST /api/v1/oauth/token<br/>grant_type=client_credentials<br/>client_id + client_secret
    RL-->>RL: Validate client credentials
    RL-->>App: Agent JWT (1h — cache and reuse across users)

    Note over App,RL: Step 2 — exchange for an OBO token (RFC 8693)
    App->>RL: POST /api/v1/oauth/token<br/>grant_type=token-exchange<br/>actor_token=agent JWT<br/>subject_token=user UUID / email / WorkOS JWT
    RL-->>RL: Verify agent JWT (actor)
    RL-->>RL: Resolve user from subject_token
    RL-->>RL: Check active delegation (user to agent)
    alt Denied: no delegation, user not found or inactive
        RL-->>App: 400 invalid_grant<br/>+ X-Runlayer-Connect-URL header
        App->>User: Forward the connect URL
        User->>RL: Sign in and click Connect<br/>(creates delegation + session grants)
        App->>RL: Retry token exchange
    end
    RL-->>App: OBO JWT (sub=user, act.sub=agent, 1h)

    Note over App,MCP: Step 3 — call MCP tools on behalf of the user
    App->>RL: POST /api/v1/proxy/{server_id}/mcp<br/>Authorization: Bearer OBO JWT
    RL-->>RL: Verify OBO JWT
    RL-->>RL: Enforce policy intersection<br/>(agent + user + server policies)
    RL-->>RL: Resolve OAuth credentials via session grant<br/>(personal grant, else shared grant, else 401)
    RL->>MCP: Forward tool call (grantor's credentials)
    MCP-->>RL: Tool result
    RL-->>App: Tool result (audit-logged with agent + user identity)
```

Each concept in the diagram is covered below: [authentication](#authentication), [delegations](#delegations), [session grants](#session-grants), and [policies](#policies). For copy-paste code, see the [Agent Account Authentication Recipes](/cookbook-agent-accounts).

## Authentication

Agent accounts authenticate using OAuth 2.0: the Client Credentials grant for M2M tokens, plus an RFC 8693 token exchange for OBO tokens. The token endpoint returns a JWT that you include in the `Authorization` header of all API requests.

### Getting an M2M Token

```bash theme={null}
# Get M2M token (agent-only, no user context)

TOKEN_RESPONSE=$(curl -s -X POST 'https://your-runlayer-instance.com/api/v1/oauth/token' \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id=your-client-id" \
  --data-urlencode "client_secret=your-client-secret")

ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')

echo "M2M Token: ${ACCESS_TOKEN:0:50}..."
# Token valid for 1 hour
```

### Getting an OBO Token

To get an OBO token, you need one of:

1. A **user UUID** from an active delegation
2. A **user email** from an active delegation
3. A **WorkOS user access token** (RFC 8693 compliant)

Getting an OBO token is a two-step flow: mint an agent token with `client_credentials` (the same call as the M2M example above), then exchange it for an OBO token via RFC 8693 token exchange. The agent token is cacheable and can be reused across many user exchanges, and the `client_secret` never travels on the per-user exchange call.

```bash theme={null}
# Step 1: mint agent token (same call as M2M)
AGENT_RESPONSE=$(curl -s -X POST 'https://your-runlayer-instance.com/api/v1/oauth/token' \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id=your-client-id" \
  --data-urlencode "client_secret=your-client-secret")
AGENT_TOKEN=$(echo "$AGENT_RESPONSE" | jq -r '.access_token')
```

For step 2, pick the `subject_token` form that matches what your system stores:

<CodeGroup>
  ```bash User UUID theme={null}
  USER_ID="user-uuid-from-delegation"

  TOKEN_RESPONSE=$(curl -s -X POST 'https://your-runlayer-instance.com/api/v1/oauth/token' \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    --data-urlencode "actor_token=$AGENT_TOKEN" \
    --data-urlencode "actor_token_type=urn:ietf:params:oauth:token-type:access_token" \
    --data-urlencode "subject_token=$USER_ID" \
    --data-urlencode "subject_token_type=urn:runlayer:token-type:user-id")

  OBO_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
  ```

  ```bash User email theme={null}
  USER_EMAIL="user@example.com"

  TOKEN_RESPONSE=$(curl -s -X POST 'https://your-runlayer-instance.com/api/v1/oauth/token' \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    --data-urlencode "actor_token=$AGENT_TOKEN" \
    --data-urlencode "actor_token_type=urn:ietf:params:oauth:token-type:access_token" \
    --data-urlencode "subject_token=$USER_EMAIL" \
    --data-urlencode "subject_token_type=urn:runlayer:token-type:user-email")

  OBO_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
  ```

  ```bash WorkOS access token theme={null}
  USER_ACCESS_TOKEN="user-workos-access-token"

  TOKEN_RESPONSE=$(curl -s -X POST 'https://your-runlayer-instance.com/api/v1/oauth/token' \
    -H "Content-Type: application/x-www-form-urlencoded" \
    --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
    --data-urlencode "actor_token=$AGENT_TOKEN" \
    --data-urlencode "actor_token_type=urn:ietf:params:oauth:token-type:access_token" \
    --data-urlencode "subject_token=$USER_ACCESS_TOKEN" \
    --data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token")

  OBO_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
  ```
</CodeGroup>

The agent now acts with the intersection of agent and user policies.

<Warning>
  **Migrating from the `client_credentials` OBO shortcut:** earlier versions
  documented passing `subject_token` / `subject_token_type` directly on the
  `client_credentials` grant. That shape is still accepted for backward
  compatibility but deprecated — switch to the two-step flow: mint an agent
  token with `client_credentials`, then exchange it with
  `grant_type=urn:ietf:params:oauth:grant-type:token-exchange` (agent token in
  `actor_token`, user identity in `subject_token`, per RFC 8693 §2.1).
</Warning>

### Scoping a token to specific servers (optional)

Pass one or more RFC 8707 `resource` parameters (the MCP server URL) on the
`client_credentials` or token-exchange call to scope the minted token to those
servers. The server records the targets in the token's `aud` claim:

```bash theme={null}
# Same client_credentials call as above, plus one `resource` per target server.
# Example: scope the token to two servers, 3f6a2b1e-… and b81f0c2d-….
TOKEN_RESPONSE=$(curl -s -X POST 'https://your-runlayer-instance.com/api/v1/oauth/token' \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "client_id=your-client-id" \
  --data-urlencode "client_secret=your-client-secret" \
  --data-urlencode "resource=https://your-runlayer-instance.com/api/v1/proxy/3f6a2b1e-8c4d-4e5f-9a7b-2c1d0e9f8a7b/mcp" \
  --data-urlencode "resource=https://your-runlayer-instance.com/api/v1/proxy/b81f0c2d-5a6e-4f3b-8d9c-7e6f5a4b3c2d/mcp")
```

The same parameter works on the token-exchange call.

<Note>
  The `aud` claim is narrowed to the servers the principal can access.
  Whether the proxy *enforces* it is controlled per agent account by the
  **Enforce token resource scoping** setting — see
  [Resource Scoping Enforcement](#resource-scoping-enforcement). Omitting
  `resource` mints a token with no `aud`, accepted everywhere the principal's
  policies allow (unchanged behavior).
</Note>

## Calling MCP Tools

Once authenticated, you can call MCP tools through the Runlayer proxy using your access token. The proxy exposes a standard **MCP Streamable HTTP** transport endpoint at `/api/v1/proxy/{server_id}/mcp`.

```bash theme={null}
# Call MCP tool through the Runlayer proxy
# Use $ACCESS_TOKEN (M2M) or $OBO_TOKEN

curl -X POST "https://your-runlayer-instance.com/api/v1/proxy/your-server-id/mcp" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "get_context",
      "arguments": {
        "query": "project structure"
      }
    },
    "id": 1
  }'
```

<Tip>
  The proxy endpoint uses the [MCP Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http). You can also use any MCP SDK client (such as the Vercel AI SDK, OpenAI Agents SDK, or Google ADK) pointed at the `https://your-runlayer-instance.com/api/v1/proxy/{server_id}/mcp` URL.
</Tip>

## Resource Scoping Enforcement

[Scoping a token](#scoping-a-token-to-specific-servers-optional) records the target servers in the token's `aud` claim. Whether the proxy **enforces** that scope is a per–agent-account setting, **Enforce token resource scoping**, in **Settings → Agent Accounts → (account) → Settings**.

When enforcement is **on**, the proxy rejects an `aud`-bearing token used against a server outside its `aud` with `403 invalid_target`. This confines a leaked or over-shared token to exactly the servers it was scoped to, below the account's usual policy ceiling.

* **Off by default — opt in per account.** Enforcement is disabled until an admin turns it on for a specific agent account, so no account is forced to scope its tokens by an upgrade.
* **When on, `resource` is required.** The token endpoint rejects a token request that doesn't name at least one server (`invalid_target`), so every token this account issues is scoped. (Per RFC 8707 §2, an authorization server may require resource indicators.)
* **Scope a token to every server it will call.** A token scoped to server A cannot call server B, even if the account's policies allow B.
* **Aggregate endpoints reject scoped tokens.** The agent-account aggregate MCP, plugin, and skill endpoints fan out beyond a single named server, so a scoped (`aud`-bearing) token is rejected there.

For example, with enforcement on and a token scoped to server `3f6a2b1e-…` (see [the scoping example above](#scoping-a-token-to-specific-servers-optional)):

```bash theme={null}
# In the token's aud — allowed (normal policy checks still apply)
curl -X POST "https://your-runlayer-instance.com/api/v1/proxy/3f6a2b1e-8c4d-4e5f-9a7b-2c1d0e9f8a7b/mcp" \
  -H "Authorization: Bearer $ACCESS_TOKEN" ...

# Any other server — rejected with 403
curl -X POST "https://your-runlayer-instance.com/api/v1/proxy/b81f0c2d-5a6e-4f3b-8d9c-7e6f5a4b3c2d/mcp" \
  -H "Authorization: Bearer $ACCESS_TOKEN" ...
```

```json 403 response theme={null}
{ "detail": "invalid_target: token audience does not include this resource" }
```

A token request that omits `resource` fails at the token endpoint instead:

```json 400 response theme={null}
{
  "error": "invalid_target",
  "error_description": "invalid_target: this agent account requires one or more `resource` indicators naming the server(s) the token may access"
}
```

<Warning>
  Turning enforcement on takes effect immediately, including for tokens already issued: any `aud`-bearing token used outside its `aud` starts getting rejected, and new token requests must include `resource`. Confirm your clients pass every server they call before enabling.
</Warning>

## Delegations

Delegations allow users to grant agent accounts permission to act on their behalf. When a user creates a delegation to an agent account, the agent account can request OBO tokens for that user.

### Key Concepts

* **Delegator**: The user granting permission
* **Delegatee**: The agent account receiving permission
* **Expiration**: Delegations can have optional expiration times
* **Revocation**: Users can revoke delegations at any time

### Delegation Flow

1. User navigates to the agent account in the Runlayer UI
2. User creates a delegation to the agent account
3. Agent account exchanges its agent token for an OBO token scoped to the user
4. Agent account calls MCP tools with the user's permissions applied

Delegations control **who** the agent account can act as. For OAuth-protected servers, the agent account also needs a **session grant** to determine **whose credentials** to use (see below).

### Recovering from a denied OBO call

When an agent's OBO token exchange is denied because the end-user has no active delegation (or any of the related recoverable failure reasons), the `400 invalid_grant` response carries an `X-Runlayer-Connect-URL` header pointing at the agent account's recovery page:

```
X-Runlayer-Connect-URL: https://your-runlayer-instance.com/agent-accounts/<agent-account-id>
```

Recommended flow:

1. The agent forwards the URL from the header to the end-user it is acting on behalf of.
2. The user opens the URL and signs in to Runlayer.
3. The user clicks **Connect** on the agent account page. This creates the delegation and any required session grants for OAuth-protected servers in a single click.
4. The agent retries the OBO token exchange — it now succeeds.

The header is only set when the failure is recoverable by the end-user. Agent-side failures (disabled agent account, missing/invalid client credentials, bad agent JWT) deliberately omit the header so callers cannot probe for the existence of agent accounts they do not control.

<Note>
  The body of the 400 response is unchanged across all denial reasons — the recovery hint is delivered exclusively through the response header. Agents that don't yet inspect the header continue to work; they just fall back to surfacing the raw `detail` message to the user.
</Note>

## Session Grants

Session grants control how an agent account authenticates to OAuth-protected MCP servers. A session grant shares a user's OAuth credentials for a specific server with an agent account, independent of delegations.

### Personal vs Shared

| Type         | Who can use it                                                                | Limit                                    |
| ------------ | ----------------------------------------------------------------------------- | ---------------------------------------- |
| **Personal** | Only the grantor's own OBO calls use these credentials                        | One per (grantor, agent account, server) |
| **Shared**   | Any user's OBO calls on this agent account can fall back to these credentials | One per (agent account, server)          |

<Warning>
  Switching a connection between **Dynamic** (personal grants) and **Owner** (a shared grant) can be disruptive: calls that relied on the previous grant can start failing, and users without a personal grant may need to grant the agent access to the server again.
</Warning>

### Credential Resolution

When an agent account makes an OBO call to an OAuth-protected server, Runlayer resolves credentials in this order:

1. **Caller's personal grant** — if the OBO caller has their own session grant for this server, their OAuth credentials are used.
2. **Shared grant fallback** — if no personal grant exists, a shared session grant (from any grantor) is used.
3. **Error** — if neither exists, the call fails with a `401` error.

### Lifecycle

Session grants are created when:

* A user connects to an OAuth connector attached to an agent
* An admin creates one through the Agent Accounts API

Revoking a delegation through the Runlayer UI does not revoke session grants, and revoking a session grant does not revoke delegations. However, **disconnecting** from an agent (removing your connection) revokes both your delegation and your session grants for that agent. Additionally, when an admin **deactivates a user**, the user can no longer obtain new OBO tokens, effectively rendering their session grants unusable — though the grants themselves are not explicitly deleted from the system.

<Note>
  If the grantor's OAuth session expires, calls relying on that session grant will fail until the grantor re-authorizes the server.
</Note>

### Example: Slack Agent with Multiple Users

Suppose you have an agent account called "Support Bot" that needs to call a Slack MCP server on behalf of users.

1. **Alice** connects to the agent and authorizes Slack. This creates a **personal** session grant for Alice.
2. An admin promotes Alice's grant to **shared**, so it can serve as a fallback for other users.
3. **Bob** connects to the agent but does not authorize Slack — he has no personal grant.
4. When Support Bot makes an OBO call to Slack **as Alice**, Runlayer uses Alice's personal grant (her own OAuth credentials).
5. When Support Bot makes an OBO call to Slack **as Bob**, Runlayer falls back to Alice's shared grant (since Bob has no personal grant).
6. If Alice later authorizes a second user **Carol**, and Carol creates her own personal grant, Carol's OBO calls use her own credentials — Alice's shared grant is not used.

## Policies

Both agent accounts and users can have policies that control what actions they can perform. When an agent account uses an OBO token, the effective permissions are the **intersection** of:

* The agent account's policies
* The user's policies
* Any server-level policies

This ensures that an agent account acting on behalf of a user can never exceed either party's permissions.

## Best Practices

<AccordionGroup>
  <Accordion title="Secure your client secret">
    Store your agent account's client secret securely (e.g., environment variables, secrets manager). Never commit it to version control or expose it in client-side code.
  </Accordion>

  <Accordion title="Use OBO tokens when appropriate">
    If your agent account is acting on behalf of a specific user, always use OBO tokens rather than M2M tokens. This ensures proper audit trails and policy enforcement.
  </Accordion>

  <Accordion title="Handle token expiration">
    Access tokens are valid for 1 hour. Implement token refresh logic in your application to request new tokens before expiration.
  </Accordion>

  <Accordion title="Apply least privilege">
    Configure your agent account's policies to only allow the minimum permissions required for its function. Avoid granting broad access.
  </Accordion>
</AccordionGroup>

## Managing Agent Accounts

Agent accounts are managed through the Runlayer UI:

1. Navigate to **Settings → Agent Accounts**
2. Create new agent accounts with the **Add Agent Account** button (admin-only)
3. Configure agent account settings — including [**Enforce token resource scoping**](#resource-scoping-enforcement) — policies, and delegations
4. View agent account activity in the audit logs

<Note>
  All workspace members can view agent accounts, create delegations, and create session grants. Only administrators can create, edit, or delete agent accounts and rotate credentials.
</Note>

## Credential Rotation

Agent account credentials should be rotated periodically or after security events to maintain security.

### When to Rotate

* **Scheduled rotation**: Rotate credentials every 90 days as a best practice
* **Security incident**: Immediately rotate if credentials may have been exposed
* **Personnel changes**: Rotate when team members with access leave the organization
* **Suspicious activity**: Rotate if you detect unusual API usage patterns

### How to Rotate

1. Navigate to **Settings → Agent Accounts**
2. Select the agent account to rotate
3. Click **Rotate Credentials**
4. Confirm the rotation
5. Save the new client secret immediately (shown only once)
6. Update your application with the new credentials

<Note>
  Agent accounts linked to an <a href="/platform-agents">Agent</a> cannot be rotated from the Agent Accounts page. The UI will direct you to the agent's page instead, where you can rotate the credentials for the linked account.
</Note>

### Impact of Rotation

When you rotate credentials:

* **Old client secret is immediately invalidated** - The agent account cannot authenticate with the old secret to obtain new tokens
* **Existing tokens remain valid until expiry** - Previously issued JWTs (both M2M and OBO) continue to work for up to 1 hour since they are stateless tokens
* **Delegations remain intact** - No need to recreate delegations
* **Policies are unchanged** - Access rules continue to apply

<Warning>
  After rotating credentials, existing access tokens remain valid for up to 1 hour. If you need to fully block access immediately, consider disabling the agent account temporarily in addition to rotating credentials. Update your application with the new client secret promptly, as no new tokens can be obtained with the old secret.
</Warning>

## Error Handling

When authenticating or making API calls, you may encounter these common errors:

### Authentication Errors

| Error Code | Error                    | Cause                                         | Remediation                                                                                             |
| ---------- | ------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| 400        | `unsupported_grant_type` | Wrong grant type used                         | Use `client_credentials` (M2M / agent token) or `urn:ietf:params:oauth:grant-type:token-exchange` (OBO) |
| 401        | `invalid_client`         | Invalid client ID or secret                   | Verify credentials are correct and not rotated                                                          |
| 400        | `invalid_grant`          | Invalid or expired token                      | Re-authenticate to get a new token                                                                      |
| 400        | `invalid_grant`          | User deactivated, not found, or no delegation | Verify the user exists, is active, and has delegated to the agent                                       |

### OBO Token Exchange Errors

| Error Code | Error             | Cause                                                                                                | Remediation                                                                                                                                                                       |
| ---------- | ----------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400        | `invalid_request` | Unsupported `subject_token_type`                                                                     | Use `urn:runlayer:token-type:user-id`, `urn:runlayer:token-type:user-email`, or `urn:ietf:params:oauth:token-type:access_token`                                                   |
| 400        | `invalid_request` | Missing `subject_token` / `subject_token_type` on token exchange                                     | Pass user identity as `subject_token` with a supported `subject_token_type`                                                                                                       |
| 400        | `invalid_request` | Agent JWT placed in `subject_token` (tokens swapped)                                                 | Agent token goes in `actor_token`, user identity in `subject_token` (RFC 8693 §2.1)                                                                                               |
| 400        | `invalid_request` | Wrong `actor_token_type`                                                                             | Must be `urn:ietf:params:oauth:token-type:access_token`                                                                                                                           |
| 400        | `invalid_grant`   | User not found, inactive, no delegation, or malformed `subject_token` value (uniform denial message) | Send the user to the `X-Runlayer-Connect-URL` recovery page (see [Recovering from a denied OBO call](#recovering-from-a-denied-obo-call)); double-check the `subject_token` value |
| 400        | `invalid_grant`   | Agent account disabled                                                                               | Contact admin to re-enable agent account                                                                                                                                          |

### Proxy Call Errors

| Error Code | Error                                                                    | Cause                                                     | Remediation                                                |
| ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------- | ---------------------------------------------------------- |
| 401        | `No credentials available for this server. A session grant is required.` | No session grant exists for this agent account and server | Create a session grant by granting access on the connector |
| 401        | `OAuth authorization required. Please authorize this server first.`      | Grantor's OAuth session is missing or expired             | Grantor needs to re-authorize the MCP server               |
| 403        | `Access denied ...` (or a tool error `Access denied by policy: ...`)     | PBAC policy blocked the request or tool call              | Review agent account and user policies                     |

### Example Error Responses

Agent account authentication and token exchange errors follow RFC 6749 §5.2: a JSON object with `error` and `error_description` fields (a legacy `detail` field is also emitted for backward compatibility):

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "invalid_grant: subject token exchange denied",
  "detail": "invalid_grant: subject token exchange denied"
}
```

When the denial can be recovered by the end-user (missing delegation, user not found / inactive, malformed actor token), the same response also carries an `X-Runlayer-Connect-URL` header:

```http theme={null}
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Runlayer-Connect-URL: https://your-runlayer-instance.com/agent-accounts/<agent-account-id>

{"error":"invalid_grant","error_description":"invalid_grant: subject token exchange denied","detail":"invalid_grant: subject token exchange denied"}
```

<Tip>
  Check the `error_description` field for specific guidance on how to resolve the error. When `X-Runlayer-Connect-URL` is present, forward that URL to the end-user — clicking through and signing in lets them grant the missing delegation in one step. See [Recovering from a denied OBO call](#recovering-from-a-denied-obo-call).
</Tip>
