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

# Identity Exchange with Clerk Organizations

> Admit a verified Clerk user and organization to Runtype, and scope agents to the buyer organization

This guide connects a Clerk application that uses Organizations to Runtype Identity Exchange. When you finish, each request carries a verified end user (the Clerk user) and a verified tenant (the Clerk organization), and an agent can isolate data by organization and pass the organization to your tools.

For how Identity Exchange works with any OpenID Connect (OIDC) provider, see [End-user identity and credentials](/developer-guides/guides/end-user-identity).

## Prerequisites

You need the following:

* A Clerk application with **Organizations** enabled.
* A Runtype API key with the `INTEGRATIONS:WRITE` permission.

## Create a Clerk JWT template

Runtype requires every integration to name a specific `audience`, and it rejects a token whose `aud` claim does not match. A default Clerk session token has no `aud` claim, so create a JWT template that adds one.

To create the template, complete the following steps:

1. In the Clerk Dashboard, open the **JWT templates** page and create a blank template named `runtype`.

2. Set the template claims to the following JSON:

   **`Clerk JWT template claims`**

   ```json title="Clerk JWT template claims"
   {
     "aud": "YOUR_RUNTYPE_AUDIENCE",
     "org_id": "{{org.id}}",
     "email": "{{user.primary_email_address}}"
   }
   ```

   Replace `YOUR_RUNTYPE_AUDIENCE` with a name for the Runtype relying party, such as `runtype-storefront`. Use the same value as `audience` when you register the integration.

3. Set the token lifetime to 60 seconds. Runtype rejects a token issued more than 10 minutes ago, with 30 seconds of clock skew allowed.

Clerk sets the `azp` claim to the origin of the page that requested the token. You list those origins in `authorizedParties` when you register the integration.

## Register the integration

Register one integration for your Clerk instance. Runtype selects the integration by the token's verified issuer, so create only one active integration for each Clerk instance.

The following request registers a Clerk integration:

**`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": "Storefront Clerk",
    "provider": "clerk",
    "descriptor": {
      "kind": "oidc-jwt",
      "issuer": "https://YOUR_CLERK_FRONTEND_API",
      "jwksUri": "https://YOUR_CLERK_FRONTEND_API/.well-known/jwks.json",
      "audience": "YOUR_RUNTYPE_AUDIENCE",
      "allowedAlgorithms": ["RS256"],
      "authorizedParties": ["https://shop.example.com"],
      "claimMap": {
        "subject": "sub",
        "tenant": "org_id",
        "email": "email"
      },
      "claimPolicy": {
        "rejectImpersonation": true,
        "rejectPendingOrg": true
      }
    }
  }'
```

Replace the following placeholders:

* `rt_live_YOUR_API_KEY`: your live Runtype API key.
* `YOUR_CLERK_FRONTEND_API`: the host of your Clerk Frontend API URL, for example `your-app.clerk.accounts.dev` for a development instance.
* `YOUR_RUNTYPE_AUDIENCE`: the `aud` value from your JWT template.
* `https://shop.example.com`: every origin that requests tokens for Runtype.

The response contains the integration `id`, which starts with `idint_`. Runtype uses this ID as the isolation namespace for identities from this integration.

The following table explains the Clerk-specific settings:

| Field                             | Why to set it                                                                                                                                                             |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `audience`                        | Required. Wildcards and empty values are rejected. Clerk adds `aud` only through a JWT template.                                                                          |
| `allowedAlgorithms`               | Clerk signs tokens with `RS256`.                                                                                                                                          |
| `authorizedParties`               | Runtype rejects a token whose `azp` claim is not in this list. Set it so that a token minted for another origin on the same Clerk instance cannot be replayed to Runtype. |
| `claimMap.tenant`                 | Maps the Clerk organization to the verified tenant. Without it, a proof carries no tenant, and a `tenant-isolated` agent rejects the request.                             |
| `claimPolicy.rejectImpersonation` | Rejects a token that has an `act` claim, which Clerk adds when an administrator impersonates a user.                                                                      |
| `claimPolicy.rejectPendingOrg`    | Rejects a token whose `sts` claim is `pending`, which Clerk sets while the session has an unfinished task, such as choosing an organization.                              |

`claimMap` paths use dot notation, so a claim nested in an object, such as `o.id`, also works.

## Send a proof on each request

Mint a fresh token for every request, and send it as `identityProof`. Runtype verifies the proof on each request and does not store it. Set `provider` to `clerk`. If you send a `provider` value, it must match the provider of the integration that the issuer selects, or Runtype rejects the proof with `provider_hint_mismatch`.

In the browser, request a token from the JWT template:

**`Browser`**

```typescript title="Browser"
const token = await clerk.session?.getToken({ template: 'runtype' })
```

Send the token as a bearer token to your backend. Never send the Clerk `__session` cookie to Runtype.

Your backend attaches the token to a dispatch request:

**`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": "Show my open orders" }],
    "identityProof": {
      "provider": "clerk",
      "token": "YOUR_CLERK_TOKEN"
    }
  }'
```

Replace `agent_YOUR_AGENT_ID` with the saved agent ID and `YOUR_CLERK_TOKEN` with the token from `getToken`. Asynchronous dispatch (`Prefer: respond-async`) does not accept `identityProof` and returns `400 ASYNC_IDENTITY_PROOF_UNSUPPORTED`.

For an embedded chat widget, pass the same object as `identityProof` to `POST /v1/client/init` and `POST /v1/client/chat`. The Persona widget (version 4.24.0 or later) does this for you when you set `identityProvider` and `getIdentityProof`:

**`Persona widget config`**

```typescript title="Persona widget config"
config: {
  clientToken: 'YOUR_CLIENT_TOKEN',
  identityProvider: 'clerk',
  getIdentityProof: async () =>
    (await clerk.session?.getToken({ template: 'runtype' })) ?? null,
}
```

The widget calls `getIdentityProof` before every chat request and sends the token as `identityProof: { provider: 'clerk', token }`. If the function returns `null`, the widget stops the turn. Without `identityProvider`, the widget sends the proof only when it starts a session, so an agent with the `tenant-isolated` Tenancy Strategy rejects its chat requests with `tenant_scope_required`. For widget setup and conversation history, see [Conversation history in an embedded widget](/developer-guides/guides/client-conversation-history).

## Scope the agent to the organization

A verified proof sets `_tenant` and `_endUser` for the execution. To isolate data by Clerk organization, save the agent with the `tenant-isolated` Tenancy Strategy:

**`Agent config`**

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

To send the organization to your API or MCP server, reference `{{_tenant.id}}` in a tool header. Runtype fills it from the verified proof, and the model cannot set it. To let your API verify the identity itself, send `{{_identity.token}}` instead. For the rules, see [Tool template variables](/developer-guides/guides/tool-template-variables#identity-in-templates). To choose between organization and per-user isolation, see [Tenancy strategies](/developer-guides/guides/tenancy-strategies).

## Verify the setup

Check that Runtype admitted the proof:

* **Widget.** The `POST /v1/client/init` response carries `visitor.identityStatus`. The value `admitted` means that Runtype verified the proof. `not_provided` means that the request had no proof. `ignored` means that the request had a proof but Identity Exchange admission is not enabled for your organization.
* **Conversation history routes.** The `X-History-Identity-Status` response header carries the same values.
* **Tools.** A tool that sends `{{_tenant.id}}` receives the Clerk organization ID, such as `org_2abc`.

## Troubleshooting

Runtype returns `401` with `error: invalid_identity_proof` when it rejects a proof. The `errorDescription` field names the reason:

| Reason                   | Likely cause                                                                                                                      |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `no_proof_match`         | The token's `iss` does not match an active integration. Check `issuer` against the Clerk Frontend API URL.                        |
| `verification_failed`    | The signature, `aud`, `azp`, expiry, or a claim policy failed. Check that the client used the `runtype` template and a new token. |
| `provider_hint_mismatch` | The `provider` value in `identityProof` differs from the integration's `provider`.                                                |
| `malformed_proof`        | The token is not a JWT. Check that you sent the token string and not the session cookie.                                          |
| `unsupported_algorithm`  | The token's signing algorithm is not in `allowedAlgorithms`.                                                                      |

Runtype returns `403` with `error: Tenancy lock-down` when a proof verifies but the agent's Tenancy Strategy rejects the scope:

* `tenant_scope_required`: the token has no `org_id`. The user has no active organization, or the integration has no `claimMap.tenant`.
* `tenant_assurance_below_floor`: the tenant is not verified. This happens when the request carries a tenant but no admitted proof.

## Next steps

* [Tenancy strategies](/developer-guides/guides/tenancy-strategies): choose what Runtype isolates by organization or user
* [Tool template variables](/developer-guides/guides/tool-template-variables): send the verified organization to your API or MCP server
* [End-user identity and credentials](/developer-guides/guides/end-user-identity): register other identity providers and connect end-user accounts