Skip to navigation

End-user identity and credentials

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.

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 toUseSection
Prove who the end user isAn identity proof from your identity provider or from Runtype-hosted identityProve an end user’s identity
Isolate memory, records, and secrets per end userThe end-user-isolated tenancy strategy on the agentScope an agent to the verified end user
Let the agent act with the end user’s GitHub accountA connected account stored under the end user’s identityConnect an end user’s third-party account
Separate memory only, from your own backendA plain end-user ID in the request bodyScope memory without a proof
Sign identity assertions from your own runtimeA signer in @runtypelabs/runtime with your own key and issuerSelf-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 methodUse it whenWhat you provide
Identity ExchangeYou already have an identity provider, such as Clerk, WorkOS, Auth0, Okta, or your own OpenID Connect (OIDC) issuerAn integration that identifies your issuer and a token for each request
Runtype-hosted identityYou want Runtype to handle end-user sign-inA 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
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:

FieldRequiredNotes
kindYesSet to oidc-jwt for a verifiable JSON Web Token (JWT).
issuerYesUse an https URL. Runtype selects the integration by the token’s verified issuer.
jwksUriYesUse an https endpoint that serves the issuer’s public keys.
audienceYesA string or an array of strings. Name a specific relying party. Wildcards are rejected.
allowedAlgorithmsYesUse asymmetric algorithms such as RS256 or ES256. Symmetric algorithms are rejected.
claimMapYesMap identity fields to token claims with dot notation. subject is required. tenant, email, roles, and permissions are optional.
authorizedPartiesNoAn allowlist of azp claim values. Runtype rejects a token whose azp isn’t in the list.
claimPolicyNoProvider-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.
clockSkewSecNoThe clock skew that Runtype allows when it checks token times, from 0 to 120 seconds. The default is 30.
maxAssuranceNoLowers 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.

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

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

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.

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:

ReasonCause
no_proof_matchNo active integration matches the token’s issuer.
ambiguous_issuerMore than one active integration matches the token’s issuer.
malformed_proofThe token isn’t a well-formed JWT, or it has no iss claim.
verification_failedThe signature, algorithm, audience, age, expiry, subject claim, authorizedParties, or claimPolicy check failed.
provider_hint_mismatchThe 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:

PresetData scopeRequires
internalYour organizationNo tenant or end user
tenant-isolatedOne product tenant, {{_tenant.id}}A verified tenant
end-user-isolatedOne 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
{
"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.

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

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

{
"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
{
"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 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
{
"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. 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:

ClaimValue
isshttps://api.runtype.com/orgs/<organizationId>, one issuer per Runtype organization (staging: https://api.runtype-staging.com/orgs/<organizationId>)
audThe 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
subThe end-user ID, only when the end user’s assurance is verified. Otherwise read runtype.endUser.id
iat, nbf, expIssue time, a not-before time 30 seconds earlier, and an expiry five minutes after issue
jtiA unique ID for this token
runtype.orgIdYour Runtype organization ID
runtype.productIdThe product ID, when the execution ran under a product
runtype.agentIdThe saved agent ID
runtype.executionIdThe execution (turn) that made the tool call
runtype.conversationIdThe conversation, when the execution has one
runtype.toolNameThe name of the tool that sent the token. Absent on a token sent to an external agent
runtype.integrationIdThe 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
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).

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

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

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.

Serve the JWKS

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

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, 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
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
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
{
"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:

StatusCause
400The request body is invalid, or redirectAfter isn’t a permitted redirect URL.
401The identity proof failed verification.
403The API key lacks INTEGRATIONS:WRITE, or the proof isn’t a verified end-user identity.
404The provider isn’t a known connector.
501The 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
{{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.

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: