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

# End-user identity and credentials

> Recognize end users in Runtype and let them connect their third-party accounts.

When your backend calls the Runtype API with your API key, the request is authenticated as your Runtype account. Use end-user identity to associate each request with one of your end users. An agent can then scope memory, records, and secrets to that end user and use their connected permissions.

This guide explains two capabilities: verify an end user's identity with Identity Exchange, and register a third-party account for that end user.

> **Note**
>
> Use end-user identity for a multi-tenant product when each end user needs isolated data or a
> connected account. If every execution runs for your organization, use your API key without
> end-user identity.

## Before you begin

Complete the following before you send identity proofs:

* If you use a scoped API key, give it the `INTEGRATIONS:WRITE` or `INTEGRATIONS:*` permission. You need it to create or change identity integrations and to connect accounts. Listing and getting integrations needs `INTEGRATIONS:READ`.

## How end-user identity works

The following table maps what your product needs to the section that covers it:

| You need to                                          | Use                                                                           | Section                                                                                |
| ---------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Prove who the end user is                            | An identity proof from your identity provider or from Runtype-hosted identity | [Prove an end user's identity](#prove-an-end-users-identity)                           |
| Isolate memory, records, and secrets per end user    | The `end-user-isolated` tenancy strategy on the agent                         | [Scope an agent to the verified end user](#scope-an-agent-to-the-verified-end-user)    |
| Let the agent act with the end user's GitHub account | A connected account stored under the end user's identity                      | [Connect an end user's third-party account](#connect-an-end-users-third-party-account) |
| Separate memory only, from your own backend          | A plain end-user ID in the request body                                       | [Scope memory without a proof](#scope-memory-without-a-proof)                          |
| Sign identity assertions from your own runtime       | A signer in `@runtypelabs/runtime` with your own key and issuer               | [Self-host the runtime](#self-host-the-runtime)                                        |

## Prove an end user's identity

Identity Exchange and Runtype-hosted identity produce the same kind of verified end-user identity. Choose the method that matches your authentication setup.

| Identity method         | Use it when                                                                                                         | What you provide                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Identity Exchange       | You already have an identity provider, such as Clerk, WorkOS, Auth0, Okta, or your own OpenID Connect (OIDC) issuer | An integration that identifies your issuer and a token for each request |
| Runtype-hosted identity | You want Runtype to handle end-user sign-in                                                                         | A request to enable Runtype-hosted identity for your account            |

### Use Identity Exchange with your identity provider

Identity Exchange verifies a token from your identity provider. Register the provider once, then send a short-lived token with each request. Runtype checks the token against the provider's public keys and maps the verified subject to an end user.

The following request registers an integration with your issuer details:

**`cURL`**

```bash title="cURL"
curl https://api.runtype.com/v1/identity-integrations \
  -H "Authorization: Bearer rt_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Example Organization OIDC",
    "provider": "oidc",
    "descriptor": {
      "kind": "oidc-jwt",
      "issuer": "https://example.com",
      "jwksUri": "https://example.com/.well-known/jwks.json",
      "audience": "example-runtype-client",
      "allowedAlgorithms": ["RS256"],
      "claimMap": {
        "subject": "sub",
        "email": "email"
      }
    }
  }'
```

Replace `rt_live_YOUR_API_KEY` with your live Runtype API key. Replace the `example.com` URLs with your issuer and JSON Web Key Set (JWKS) endpoint. This example uses `oidc` as the provider value. The other provider values are `clerk`, `workos`, `auth0`, `okta`, `better-auth`, and `custom`. The response includes the integration's `id`.

The `oidc-jwt` descriptor accepts the following fields:

| Field               | Required | Notes                                                                                                                                                                                                                                                                                           |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind`              | Yes      | Set to `oidc-jwt` for a verifiable JSON Web Token (JWT).                                                                                                                                                                                                                                        |
| `issuer`            | Yes      | Use an `https` URL. Runtype selects the integration by the token's verified issuer.                                                                                                                                                                                                             |
| `jwksUri`           | Yes      | Use an `https` endpoint that serves the issuer's public keys.                                                                                                                                                                                                                                   |
| `audience`          | Yes      | A string or an array of strings. Name a specific relying party. Wildcards are rejected.                                                                                                                                                                                                         |
| `allowedAlgorithms` | Yes      | Use asymmetric algorithms such as `RS256` or `ES256`. Symmetric algorithms are rejected.                                                                                                                                                                                                        |
| `claimMap`          | Yes      | Map identity fields to token claims with dot notation. `subject` is required. `tenant`, `email`, `roles`, and `permissions` are optional.                                                                                                                                                       |
| `authorizedParties` | No       | An allowlist of `azp` claim values. Runtype rejects a token whose `azp` isn't in the list.                                                                                                                                                                                                      |
| `claimPolicy`       | No       | Provider-specific checks: `rejectImpersonation` rejects a token with an `act` claim, `rejectPendingOrg` rejects `sts: pending`, `rejectAudienceEqualsAzp` rejects a token whose `aud` equals its `azp`, and `requireIssuerPathSegment` requires the issuer URL path to contain the given value. |
| `clockSkewSec`      | No       | The clock skew that Runtype allows when it checks token times, from 0 to 120 seconds. The default is 30.                                                                                                                                                                                        |
| `maxAssurance`      | No       | Lowers the assurance that the integration can grant to `asserted`. It can't raise it.                                                                                                                                                                                                           |

Only an `oidc-jwt` descriptor produces a verified end-user identity. The `session` and `custom` descriptor kinds are capped at asserted assurance. Their proofs don't meet the default assurance of the `end-user-isolated` tenancy strategy, and they can't connect a third-party account.

> **Warning**
>
> Create only one active integration for each issuer in your account. The Runtype API rejects a
> second active integration for the same issuer with `409`, because issuer-based selection would be
> ambiguous.

### Manage an integration

You manage identity integrations through the API. The following endpoints act on an existing integration:

* `GET /v1/identity-integrations` lists your integrations.
* `GET /v1/identity-integrations/{id}` gets one integration.
* `PATCH /v1/identity-integrations/{id}` renames it, replaces its descriptor, or sets its `status` to `active` or `disabled`.
* `DELETE /v1/identity-integrations/{id}` deletes it. End users that it already identified keep their end-user IDs.

To replace an integration for the same issuer, disable the old one first. The following request disables an integration:

**`cURL`**

```bash title="cURL"
curl -X PATCH https://api.runtype.com/v1/identity-integrations/YOUR_INTEGRATION_ID \
  -H "Authorization: Bearer rt_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "disabled" }'
```

Replace `YOUR_INTEGRATION_ID` with the integration's `id`. For the full request and response schemas, see [Update an identity integration](/api-reference/runtype-api/identity-integrations/update-an-identity-integration).

### Use Runtype-hosted identity

Use Runtype-hosted identity when you don't run an identity provider. Runtype hosts the sign-in page, brands it to your product, and issues an end-user token. Present that token as `identityProof` in the same way as a token from your identity provider. You don't register an integration. Contact Runtype to enable this method for your account.

## Pass a proof on a request

After you configure an identity integration, attach the end user's proof to a dispatch or chat request as `identityProof`. Set `provider` to the integration's provider value and `token` to the proof. For a Runtype-hosted token, use `runtype-hosted` as the provider value. If `provider` doesn't match the integration that the token's issuer selects, Runtype rejects the request.

The following request dispatches a message to a saved agent with an end-user proof:

**`cURL`**

```bash title="cURL"
curl https://api.runtype.com/v1/dispatch \
  -H "Authorization: Bearer rt_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": {
      "agentId": "agent_YOUR_AGENT_ID"
    },
    "messages": [
      {
        "role": "user",
        "content": "Summarize my open pull requests"
      }
    ],
    "identityProof": {
      "provider": "oidc",
      "token": "YOUR_IDENTITY_PROOF"
    }
  }'
```

Replace `rt_live_YOUR_API_KEY` with your live Runtype API key, `agent_YOUR_AGENT_ID` with the saved agent ID, and `YOUR_IDENTITY_PROOF` with a token from your identity provider.

The TypeScript SDK accepts the same `identityProof` field on dispatch. The Python SDK and the `dispatch` tool of the Runtype MCP server accept it as `identity_proof`.

The token must meet the following requirements:

* It includes an `iat` (issued at) claim, and it was issued in the last 10 minutes. Runtype rejects an older token, so don't reuse a long-lived access token.
* It's no longer than 8,192 characters.

`identityProof` isn't supported with `Prefer: respond-async`, which returns `400` with `ASYNC_IDENTITY_PROOF_UNSUPPORTED`. Send proof-bearing requests as streaming or synchronous dispatches.

When the proof verifies, the verified identity replaces any `tenant` or `endUser` value in the request body. For browser-embedded callers, the Runtype API doesn't trust an identity in the request body. Use a verified proof to receive a verified end-user scope.

> **Warning**
>
> Treat the proof token as a secret. Mint a short-lived token for each request. The Runtype API
> doesn't log, store, or echo it. Keep long-lived tokens out of client-side code.

For a browser-embedded widget, pass `identityProof` during session initialization. For more information, see [Verified cross-device history](/developer-guides/guides/client-conversation-history#verified-cross-device-history).

If Identity Exchange isn't enabled for your organization, conversation history requests that carry a proof return `503` with `identity_proof_not_admitted`. Dispatch and chat requests accept the proof but ignore it.

### Troubleshoot a rejected proof

When a proof fails verification, the request returns `401` with `error` set to `invalid_identity_proof`. The `errorDescription` field holds one of the following reasons:

| Reason                   | Cause                                                                                                               |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `no_proof_match`         | No active integration matches the token's issuer.                                                                   |
| `ambiguous_issuer`       | More than one active integration matches the token's issuer.                                                        |
| `malformed_proof`        | The token isn't a well-formed JWT, or it has no `iss` claim.                                                        |
| `verification_failed`    | The signature, algorithm, audience, age, expiry, subject claim, `authorizedParties`, or `claimPolicy` check failed. |
| `provider_hint_mismatch` | The `provider` value doesn't match the integration that the issuer selected.                                        |

## Scope an agent to the verified end user

A verified proof establishes identity, but it doesn't configure an agent's data scope. Set the saved agent's tenancy strategy when you create or update the agent. The following presets are available:

| Preset              | Data scope                           | Requires              |
| ------------------- | ------------------------------------ | --------------------- |
| `internal`          | Your organization                    | No tenant or end user |
| `tenant-isolated`   | One product tenant, `{{_tenant.id}}` | A verified tenant     |
| `end-user-isolated` | One end user, `{{_endUser.id}}`      | A verified end user   |

A proof supplies the tenant from the claim that `claimMap.tenant` names. The following agent configuration sets the `end-user-isolated` preset:

**`Agent config`**

```json title="Agent config"
{
  "config": {
    "tenancyStrategy": {
      "preset": "end-user-isolated"
    }
  }
}
```

Runtype rejects an execution that lacks the identity that the preset requires with `403`. When Runtype admits the execution, it applies the scope to memory, records, and secret resolution. An agent without a tenancy strategy keeps its existing scoping behavior. Keep the strategy on the saved agent definition. Don't accept it from a dispatch body that an end user controls.

To change the required assurance for a scope, add an optional `assuranceFloor` object with `tenant` or `endUser` set to `asserted` or `verified`.

The end-user identity can come from two places:

* From your backend with an API key, an `endUser` value in the request body satisfies `end-user-isolated`.
* From a browser, you must send `identityProof`. The Runtype API doesn't trust a body identity from a browser-embedded caller.

When an agent sets a tenancy strategy, Runtype derives the memory namespace from the tenant and end user and ignores `memory.profileTemplate`. To separate memory by release alias or version, set `memory.pointerScope`. For more information, see [Agents with a Tenancy Strategy](/user-guide/agents/agent-versioning-and-publishing#agents-with-a-tenancy-strategy).

To pass the verified identity to a tool without letting the model set it, list `_endUser` or `_tenant` in the tool's `hiddenParameterNames`. For details, see [Hide parameters from the model](/developer-guides/guides/runtime-tools#hide-parameters-from-the-model). To send the verified identity to a remote MCP server, reference it in a header of the agent's MCP server, for example `X-Tenant-Id: {{_tenant.id}}`. For details, see [Pass the verified identity to an MCP server](/user-guide/tools/runtime-tools#pass-the-verified-identity-to-an-mcp-server) and [Tool template variables](/developer-guides/guides/tool-template-variables).

## Send a verifiable identity assertion to your API

A plain `{{_endUser.id}}` value tells your API who the end user is, but your API has to trust the request that carries it. To let your API verify the identity itself, reference `{{_identity.token}}` in a tool. Runtype then signs a short-lived JSON Web Token (JWT) for each tool call, and your API verifies it offline against Runtype's public keys. You don't share a secret with Runtype, and you don't provision anything per end user.

The following runtime tool sends the assertion as a bearer token:

**`Identity assertion header`**

```typescript title="Identity assertion header"
{
  name: 'list_orders',
  description: 'List the current end user orders',
  toolType: 'external',
  parametersSchema: {
    type: 'object',
    properties: { status: { type: 'string' } },
    required: ['status']
  },
  config: {
    url: 'https://orders.example.com/v1/orders?status={{status}}',
    method: 'GET',
    headers: { Authorization: 'Bearer {{_identity.token}}' }
  }
}
```

You can also reference `{{_identity.token}}` in the URL or body of an `external` tool, in a header of an MCP server in the agent's `tools.mcpServers`, or in an auth header of a saved MCP server. For a saved server, see [Send the assertion to a saved MCP server](#send-the-assertion-to-a-saved-mcp-server). Runtime tools saved in your account and attached by tool ID don't support it yet.

Runtype mints the assertion only when all of the following are true:

* The saved agent has a tenancy strategy.
* The execution resolved a tenant, an end user, or both.
* The tool's URL has a fixed host. A URL such as `https://{{host}}/orders` is rejected, because the model could choose where the token goes. A host from a `{{secret:NAME}}` reference counts as fixed, because Runtype resolves the secret before it binds the audience.
* The URL uses `https`. Plain `http` is allowed only for `localhost`.
* If the tool sets `identityAudiences`, the resolved host matches one of its entries.

If any condition isn't met, Runtype doesn't call the tool and returns a `not_configured` error to the model as the tool result. The model never sees the token and can't set or replace it. `_identity` is a reserved name on every tool: Runtype removes it from the schema the model sees and drops any `_identity` argument, including from the query string or body it builds automatically. A `_identity` parameter alone doesn't request a token; only a `{{_identity...}}` template or `hiddenParameterNames: ["_identity"]` does. Runtype also removes the token from tool events, tool results, and logs.

To grade such an agent in an eval suite, give each case an `endUser` (or set the suite's `identityDefaults`). For details, see [Grade an agent that runs as an end user](/user-guide/evals/manage-evals-as-code#grade-an-agent-that-runs-as-an-end-user).

### Send the assertion to a different host per release alias

One agent version can call a different host from each release alias, for example your production API from `live` and a pull request's environment from `pr-482`. Take the host from a secret, and bind that secret per alias with [per-alias secret bindings](/user-guide/agents/agent-versioning-and-publishing#per-alias-secret-bindings). Each token's `aud` is the host that the alias's binding resolves to, so a token minted for a preview isn't accepted by your production API.

To limit the hosts that a binding can point the token at, list them in `identityAudiences`. The following tool takes its host from `PATIENT_API_HOST`, and accepts only the production host or one label under the staging domain:

```json
{
  "name": "lookup_patient",
  "toolType": "external",
  "config": {
    "url": "https://{{secret:PATIENT_API_HOST}}/v1/patients/me",
    "method": "GET",
    "headers": { "Authorization": "Bearer {{_identity.token}}" },
    "identityAudiences": ["api.example.com", "*.staging.example.com"]
  }
}
```

Each `identityAudiences` entry is a hostname, or `*.` followed by a hostname of at least two labels. A `*.` entry matches exactly one extra label, so `*.staging.example.com` matches `pr-482.staging.example.com` but not `staging.example.com` or `a.pr-482.staging.example.com`. A tool can list up to 20 entries. If the resolved host isn't listed, Runtype doesn't mint a token or call the tool. `identityAudiences` applies to `external` tools and to MCP tools that send `{{_identity.token}}` in a header. On an MCP server in the agent's `tools.mcpServers`, set `identityAudiences` on the server entry, and every tool discovered from that server inherits it. When the tool's URL has a literal host, Runtype rejects the tool at save time unless that host is listed.

### Send the assertion to a saved MCP server

A saved MCP server is one that you register in the dashboard or with `POST /v1/mcp/servers`, and then attach by ID. It can send the assertion in its auth headers. Reference `{{_identity.token}}` in the `token` of a `bearer`, `api_key`, or `custom_header` auth, or in any value of a `headers` auth. The same header values can also contain `{{_tenant.id}}`, `{{_endUser.id}}`, `{{_tenant.assurance}}`, and `{{_endUser.assurance}}`.

**`Saved MCP server that receives the identity assertion`**

```json title="Saved MCP server that receives the identity assertion"
{
  "name": "orders_mcp",
  "url": "https://mcp.orders.example.com/mcp",
  "auth": {
    "type": "headers",
    "headers": {
      "Authorization": "Bearer {{_identity.token}}",
      "X-Tenant-Id": "{{_tenant.id}}"
    }
  }
}
```

Runtype mints a new token for each connection to the server: one when it lists the server's tools at the start of a run, and one for each tool call. The token's `aud` is the server's canonical resource URI, for example `https://mcp.orders.example.com/mcp`. Runtype builds it from the saved URL: the scheme and host in lowercase, the port only when it isn't the default, and the path without a trailing slash. It drops the query and fragment. The [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri) requires a token's audience to name the exact MCP server, so two servers on the same host can't accept each other's tokens. Your server must reject a token whose `aud` isn't its own canonical URI. A token sent with the tool list has no `runtype.toolName` claim. A token sent with a tool call carries the name of the remote tool. The `_tenant` and `_endUser` values come from the same verified identity that the token carries. Other fields, such as `_endUser.email`, aren't available in a saved server's headers.

Runtype refuses to follow a redirect on a request that carries these headers. It also doesn't cache the server's tool list across runs, because the list can differ per end user.

The conditions for a tool also apply to a saved server:

* When you save the server, Runtype rejects an identity template anywhere except an auth header value, for example in the URL or a basic-auth username. It also rejects the server if its URL doesn't use `https` (or `http` for `localhost`).
* When the run has no verified tenant or end user, Runtype doesn't contact the server. The run fails before the model is called, or the tool call returns a `not_configured` error.
* The model can't set or replace the values. Runtype drops any `_identity`, `_tenant`, or `_endUser` argument that the server's headers reference, and it removes the token from tool events, tool results, and logs.

Outside an agent run, for example when you discover tools in the dashboard, Runtype has no identity to send. It leaves the identity headers out of the tool-list request and refuses tool calls.

### Send the assertion to an external agent

An external agent that Runtype calls (`agentType: 'external'`) can receive the same assertion on every request. Set `identityAssertion: true` in the agent's `externalConfig`:

**`External agent that receives the identity assertion`**

```json title="External agent that receives the identity assertion"
{
  "name": "orders-assistant",
  "agentType": "external",
  "config": { "tenancyStrategy": { "preset": "tenant-isolated" } },
  "externalConfig": {
    "endpoint": "https://agents.example.com/a2a",
    "identityAssertion": true
  }
}
```

Runtype then sends the token in the `X-Runtype-Identity` request header, next to any `auth` header you configured. The token's `aud` is the lowercase host name of the endpoint, and it has no `runtype.toolName` claim. Your agent verifies it the same way as in [Verify the token](#verify-the-token). The agent can also forward the token unchanged to an API on the same host, because `aud` still matches there.

The conditions for a tool also apply here: the agent needs a tenancy strategy, the execution must resolve a tenant or an end user, and the endpoint must use `https` (or `http` for `localhost`). If a condition isn't met, Runtype doesn't call the agent and the turn fails with an error.

Runtype sends the header from three entry points:

* Chat surfaces that use a client token, for example the Persona widget.
* `/v1/dispatch`.
* `POST /v1/agents/{id}/execute`. Pass `endUser` or `tenant` in the request body. Runtype asserts the identity that the agent's tenancy strategy admits.

On a direct execute, an assertion needs all of the following:

* The agent has a `tenancyStrategy`.
* The request resolves an `endUser` or a `tenant`.
* The API key or session belongs to an organization.

If a condition isn't met, Runtype doesn't call the agent:

* The strategy can require a scope that the request omits, for example `tenant` with the preceding `tenant-isolated` preset. The tenancy check then refuses the request first, with HTTP `403`.
* Otherwise, the request fails with HTTP `400` and code `IDENTITY_ASSERTION_UNAVAILABLE`. The response's `message` names the missing condition.

Other entry points, such as the A2A and MCP surfaces, product API surfaces, evals, and schedules, can't send an assertion yet. They refuse to call an agent that sets `identityAssertion`, so the agent never runs without one.

### Token contents

The token is a JWS signed with ES256. The header has `alg: ES256`, `typ: runtype-identity+jwt`, and the `kid` of the signing key. The payload has the following claims:

| Claim                    | Value                                                                                                                                                                                                               |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iss`                    | `https://api.runtype.com/orgs/<organizationId>`, one issuer per Runtype organization (staging: `https://api.runtype-staging.com/orgs/<organizationId>`)                                                             |
| `aud`                    | The lowercase host name of the tool URL or external agent endpoint, for example `orders.example.com`. For a saved MCP server, the server's canonical resource URI, for example `https://mcp.orders.example.com/mcp` |
| `sub`                    | The end-user ID, only when the end user's assurance is `verified`. Otherwise read `runtype.endUser.id`                                                                                                              |
| `iat`, `nbf`, `exp`      | Issue time, a not-before time 30 seconds earlier, and an expiry five minutes after issue                                                                                                                            |
| `jti`                    | A unique ID for this token                                                                                                                                                                                          |
| `runtype.orgId`          | Your Runtype organization ID                                                                                                                                                                                        |
| `runtype.productId`      | The product ID, when the execution ran under a product                                                                                                                                                              |
| `runtype.agentId`        | The saved agent ID                                                                                                                                                                                                  |
| `runtype.executionId`    | The execution (turn) that made the tool call                                                                                                                                                                        |
| `runtype.conversationId` | The conversation, when the execution has one                                                                                                                                                                        |
| `runtype.toolName`       | The name of the tool that sent the token. Absent on a token sent to an external agent                                                                                                                               |
| `runtype.integrationId`  | The Identity Exchange integration that verified the identity, when one did                                                                                                                                          |
| `runtype.tenant`         | `{ id, assurance }` for the resolved tenant                                                                                                                                                                         |
| `runtype.endUser`        | `{ id, assurance }` for the resolved end user                                                                                                                                                                       |

`assurance` is `anonymous`, `asserted`, or `verified`, graded separately for the tenant and the end user. It records how the calling organization proved the identity to Runtype. `verified` means the organization's own credential (for example an API key) or its Identity Exchange integration vouched for it. Runtype does not verify the person independently. So the assurance is only as trustworthy as the organization named in `iss`. To bind a token to one conversation or one turn, compare `runtype.conversationId` or `runtype.executionId` with the value your API expects.

### Verify the token

Runtype publishes its public keys as a JSON Web Key Set (JWKS) at `https://api.runtype.com/.well-known/jwks.json`. For staging, use `https://api.runtype-staging.com/.well-known/jwks.json`. The response can be cached for five minutes.

Every Runtype organization's tokens are signed with the same keys. The signature proves that Runtype issued the token, not which customer it came from. Your API **must** pin `iss` to the organization it trusts. Otherwise any Runtype customer could mint a token for your host.

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. `iss` is exactly `https://api.runtype.com/orgs/<organizationId>` for your customer's Runtype organization. Reject every other issuer.
3. `aud` is your API's own host name. For a saved MCP server, `aud` is the server's canonical resource URI instead.
4. `exp` and `nbf` are valid for the current time, allowing a small clock tolerance.
5. `runtype.endUser.assurance` (or `runtype.tenant.assurance`) is `verified`, if your API acts on that identity.

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

**`Verify a Runtype identity assertion`**

```typescript title="Verify a Runtype identity assertion"
import { createRemoteJWKSet, jwtVerify } from 'jose'

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

export async function verifyRuntypeIdentity(authorization: string) {
  const token = authorization.replace(/^Bearer /, '')
  const { payload } = await jwtVerify(token, runtypeKeys, {
    // Pin the one Runtype organization this API trusts.
    issuer: 'https://api.runtype.com/orgs/org_your_customer',
    audience: 'orders.example.com',
    algorithms: ['ES256'],
    typ: 'runtype-identity+jwt',
    clockTolerance: 30,
  })
  const runtype = payload.runtype as {
    endUser?: { id: string; assurance: string }
    executionId: string
  }
  if (runtype.endUser?.assurance !== 'verified') {
    throw new Error('End user is not verified')
  }
  return { endUserId: runtype.endUser.id, executionId: runtype.executionId }
}
```

Runtype rotates signing keys by publishing a new key alongside the old one. A token whose `kid` isn't in your cached key set means your cache is stale, so fetch the JWKS again. `createRemoteJWKSet` does this for you.

## Self-host the runtime

When you run an exported agent with `@runtypelabs/runtime`, your host signs the identity assertion instead of Runtype. The token has the same header, claims, audience, and five-minute lifetime, so a receiver's verifier code works unchanged. Only the issuer and the JWKS URL change. For how to run an export, see [Self-hosting (BYOC)](/developer-guides/guides/self-hosting).

### Generate a signing key

The signer needs an EC P-256 private key as a JSON Web Key (JWK). The following script generates one with `jose`:

**`generate-identity-key.ts`**

```typescript title="generate-identity-key.ts"
import { exportJWK, generateKeyPair } from 'jose'

const { privateKey } = await generateKeyPair('ES256', { extractable: true })
const jwk = { ...(await exportJWK(privateKey)), kid: 'identity-2026-10', alg: 'ES256', use: 'sig' }
console.log(JSON.stringify(jwk))
```

Store the output as a secret in your host, for example as `IDENTITY_SIGNING_KEY`. The `d` member is the private part. Never publish it.

### Create the signer and wire the adapter

Create the signer once at startup, then pass `signer.mint` to your platform adapter as `mintIdentityAssertion`:

**`server.ts`**

```typescript title="server.ts"
import {
  CloudRunAdapter,
  NoOpRecordStore,
  RuntimeContextStepExecutor,
  RuntimeToolExecutor,
  createRuntime,
  noopLoggerProvider,
} from '@runtypelabs/runtime'
import { createIdentityAssertionSigner } from '@runtypelabs/runtime/identity'

const signer = await createIdentityAssertionSigner({
  issuer: 'https://agents.example.com/orgs/acme',
  organizationId: 'org_acme',
  signingKeys: [JSON.parse(process.env.IDENTITY_SIGNING_KEY!)],
})

const adapter = new CloudRunAdapter({ mintIdentityAssertion: signer.mint })
const contextStepExecutor = new RuntimeContextStepExecutor({
  loggerProvider: noopLoggerProvider,
  recordStore: new NoOpRecordStore(),
  adapter,
  toolExecutor: () => toolExecutor,
})
const toolExecutor = new RuntimeToolExecutor({
  adapter,
  flowEngineDeps: { adapter, contextStepExecutor },
})

const runtime = createRuntime({
  agents: { [agent.name]: agent },
  keys: { mode: 'own' },
  adapter,
  contextStepExecutor,
  toolExecutor,
})
```

The runtime mints `{{_identity.token}}` when a tool runs, so it also needs the
`toolExecutor`. Without one, no tool call runs. For details, see
[Run an export in your own code](/developer-guides/guides/self-hosting#run-an-export-in-your-own-code).

The signer accepts the following options:

* `issuer`: The token's `iss`. Receivers pin this exact value.
* `organizationId`: The value of the `runtype.orgId` claim.
* `signingKeys`: Your JWKs. The first entry signs and must include its private part, `d`. The other entries are public keys that receivers can still verify with.

`MemoryAdapter`, `CloudflareAdapter`, `CloudRunAdapter`, `EcsAdapter`, and `VercelAdapter` all accept `mintIdentityAssertion` in their options.

### Pass the tenant and end user

The runtime mints a token only for an execution that carries a tenant, an end user, or both. Pass them as trusted host context on each call:

**`Pass the verified identity`**

```typescript title="Pass the verified identity"
const response = await runtime.executeAgent(agent.name, {
  messages,
  hostContext: {
    tenant: { id: 'org_01HXYZ', assurance: 'verified' },
    endUser: { id: 'user_01HABC', assurance: 'verified' },
  },
})
```

`assurance` is `anonymous`, `asserted`, or `verified`. Set `verified` only after your host verifies the credential itself, for example by checking the WorkOS access token's signature, issuer, and audience. Never copy these values from a request body that an end user controls. The runtime signs only identity from `hostContext`. A `_tenant` or `_endUser` passed in `variables` is never signed.

If you serve `/v1/dispatch` with `createDispatchHost`, return the verified identity from its `resolveHostContext` option instead. The dispatch host ignores the `endUser` and `tenant` fields in the request body. For details, see [Give each request its own end user](/developer-guides/guides/self-hosting#give-each-request-its-own-end-user).

Without a tenant or end user, `mint` returns `undefined` and the runtime fails closed: it refuses the tool call or the external-agent call. If the agent sets a tenancy strategy, also pass `handledAgentConfig: ['tenancyStrategy']`. For details, see [Declare host-owned agent settings](/developer-guides/guides/self-hosting#declare-host-owned-agent-settings).

### Serve the JWKS

Serve `signer.jwks()` at your issuer's origin, at `/.well-known/jwks.json`. The response holds only public keys:

**`JWKS route`**

```typescript title="JWKS route"
app.get('/.well-known/jwks.json', async () =>
  Response.json(await signer.jwks(), {
    headers: { 'Cache-Control': 'public, max-age=300' },
  })
)
```

### Trust a self-signed token

A self-signed token means that your host verified the identity. Runtype didn't sign it and doesn't vouch for it. A receiver must pin your issuer and fetch keys from your JWKS URL, never Runtype's. In the [verifier example](#verify-the-token), change two values:

* Set `issuer` to your signer's `issuer`, for example `https://agents.example.com/orgs/acme`.
* Pass your JWKS URL to `createRemoteJWKSet`, for example `https://agents.example.com/.well-known/jwks.json`.

### Rotate the signing key

To rotate the key, generate a new key and put it first in `signingKeys`. Keep the old key's public part, without `d`, later in the list:

**`Rotate the signing key`**

```typescript title="Rotate the signing key"
const { d: _retired, ...previousPublicKey } = previousPrivateKey

const signer = await createIdentityAssertionSigner({
  issuer: 'https://agents.example.com/orgs/acme',
  organizationId: 'org_acme',
  signingKeys: [newPrivateKey, previousPublicKey],
})
```

The new key signs at once, and the JWKS publishes both keys. A receiver that sees an unknown `kid` fetches the JWKS again. Remove the old public key after every token it signed has expired and receivers have refreshed their caches. Wait at least 10 minutes: 5 for the token lifetime and 5 for the JWKS cache.

## Connect an end user's third-party account

Credential registration lets an end user connect a GitHub account. GitHub is the only built-in connector. The agent can then act with that account's permissions. The connect request verifies the end user and returns a URL for the provider's consent screen. After the end user approves, Runtype stores the resulting token under that end user's identity.

If the GitHub connector isn't configured, the connect endpoint returns `501`. Contact Runtype to enable it.

Complete the connection in two stages.

### Start the connection

Call the connect endpoint from your backend with the end user's proof. The Runtype API returns an authorization URL for the end user's browser.

**`cURL`**

```bash title="cURL"
curl https://api.runtype.com/v1/end-user-integrations/github/connect \
  -H "Authorization: Bearer rt_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "identityProof": "YOUR_IDENTITY_PROOF",
    "redirectAfter": "https://example.com/settings/connections"
  }'
```

Replace `rt_live_YOUR_API_KEY` with your live Runtype API key. Replace `YOUR_IDENTITY_PROOF` with the end user's proof. Replace the `example.com` URL with the page to open after the connection completes.

On this endpoint, `identityProof` is the token string itself, not the `{ "provider", "token" }` object that dispatch uses. The proof must come from an `oidc-jwt` integration for your own identity provider. The endpoint doesn't accept a Runtype-hosted token. `redirectAfter` is optional. Without it, Runtype shows its own **Account connected** page after the connection completes.

The response includes `authorizeUrl`, `state`, and `endUserId`:

**`Connect response`**

```json title="Connect response"
{
  "authorizeUrl": "YOUR_GITHUB_AUTHORIZATION_URL",
  "state": "YOUR_OAUTH_STATE",
  "endUserId": "eu_YOUR_END_USER_ID"
}
```

Treat these values as opaque. Redirect the browser to `authorizeUrl` immediately, without constructing the URL yourself. The URL is single-use and expires after 10 minutes. The GitHub consent screen asks the end user to grant the `repo` and `read:user` scopes.

The connect endpoint returns the following errors:

| Status | Cause                                                                                    |
| ------ | ---------------------------------------------------------------------------------------- |
| `400`  | The request body is invalid, or `redirectAfter` isn't a permitted redirect URL.          |
| `401`  | The identity proof failed verification.                                                  |
| `403`  | The API key lacks `INTEGRATIONS:WRITE`, or the proof isn't a verified end-user identity. |
| `404`  | The provider isn't a known connector.                                                    |
| `501`  | The connector isn't configured.                                                          |

### Complete the connection

GitHub redirects the end user to the Runtype callback with an authorization code. You don't build the callback. The Runtype API exchanges the code for a token, stores the token under the end user's identity, and redirects the browser to your `redirectAfter` URL.

If the callback fails, for example because the authorization URL expired or was already used, the end user sees a **Connection failed** page. Start the connection again.

If the end user reconnects, Runtype replaces the stored token.

### Use the stored credential

Runtype stores the token under a stable key scoped to the end user. A GitHub token uses the key `GITHUB_TOKEN`. Reference it in a tool configuration as you reference any secret:

**`Secret reference`**

```plaintext title="Secret reference"
{{secret:GITHUB_TOKEN}}
```

When an agent with the `end-user-isolated` tenancy strategy runs for a verified end user, `{{secret:GITHUB_TOKEN}}` resolves to that end user's GitHub token. Each end user receives a separate value. The key is `GITHUB_TOKEN` for every end user. The end-user scope provides the isolation. If the end user hasn't connected an account, the reference doesn't resolve.

> **Warning**
>
> Don't define an organization-level or tenant-level `GITHUB_TOKEN` secret. A key can exist at only
> one level of the organization, tenant, and end-user chain. If a higher level defines the key,
> every end user resolves to that token, and end users can't connect their own accounts.

### Manage stored credentials

A connected account is a secret with an `endUserId`. You manage it with the secrets endpoints:

* To find an end user's token, call `GET /v1/secrets` and match the `key` and `endUserId` fields.
* To disconnect the account, call `DELETE /v1/secrets/{id}` with that secret's `id`.
* To store a credential for a provider without a connector, call `POST /v1/secrets` with `key`, `value`, and `endUserId` set to the end user's `eu_...` ID. An end-user secret requires a `value`.

## Scope memory without a proof

For a trusted backend without a tenancy strategy, authenticate with your API key, set the agent's memory `profileTemplate` to `{{_endUser.id}}`, and pass `endUser.id` in the dispatch body. This setup scopes memory only. It doesn't scope records or secret resolution, so don't use it for connected credentials. If you later add a tenancy strategy to the agent, Runtype ignores `profileTemplate`.

## Next steps

Use the following pages to continue:

* [Creating and configuring agents](/user-guide/agents/creating-and-configuring-agents): configure memory and end-user scope.
* [Managing secrets](/user-guide/settings/managing-secrets): create and reference secrets used by tools.
* [Self-hosting (BYOC)](/developer-guides/guides/self-hosting): run an exported agent on your own infrastructure.
* [Create an identity integration](/api-reference/runtype-api/identity-integrations/create-an-identity-integration): see the full identity integration schema.