External Principals
Provision role assignments and grants for entity-anchored external principals, then start tightly scoped sessions through a trusted backend.
External principals let you bind an external user identity to a workspace entity and scope which tools, skills, and data that user's agent session can access. This is designed for scenarios where an end user - such as a clinician, care coordinator, or partner employee - drives a conversation through the agent and should only see or invoke resources their role permits.
How It Works
External principals connect three concepts:
Role registry - Each workspace defines a set of internal roles (normalized lowercase slugs) that map to external role names from the customer's identity system. Roles are workspace-scoped and deduplicated at the registry level.
Role assignments - An assignment record binds an external user (identified by a subject key such as an email address, and optionally a vendor-specific subject ID) to a workspace entity and one or more internal roles. Assignments carry a status (active, suspended, or revoked) and track how they were provisioned (roster sync or just-in-time). Role verification status is recorded separately - a session opened with unverified roles can behave differently from one with verified roles.
Role grants - Grants map a registry role to a specific resource (integration endpoint, skill, or knowledge base scope) with a read or write access level. Grants are immutable - changes insert a new active row and mark the prior one as superseded, so the authorization state at any past point in time is reconstructable for audit.
Management Endpoints
Each of the three concepts has its own workspace-scoped resource family.
Roles
Register the workspace's internal roles and their external role name mappings.
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
^[a-z][a-z0-9_]{0,63}$Successful Response
Wire shape for read + create responses.
An external role with the given name already exists
Validation Error
POST /v1/{workspace_id}/external-roles HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 59
{
"name": "text",
"external_name": "text",
"description": "text"
}{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "text",
"external_name": "text",
"description": "text",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-01T00:00:00.000Z"
}API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
[]50Successful Response
Validation Error
GET /v1/{workspace_id}/external-roles HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "text",
"external_name": "text",
"description": "text",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-01T00:00:00.000Z"
}
],
"has_more": true,
"continuation_token": null
}Get a Role
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
Successful Response
Wire shape for read + create responses.
External role not found
Validation Error
GET /v1/{workspace_id}/external-roles/{role_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "text",
"external_name": "text",
"description": "text",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-01T00:00:00.000Z"
}Delete a Role
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
Successful Response
No content
External role not found
Validation Error
DELETE /v1/{workspace_id}/external-roles/{role_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Role Assignments
Bind an external user (subject key, optional subject ID) to a workspace entity and one or more roles. The PUT upserts the assignment for a subject.
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
falseSuccessful Response
Validation Error
PUT /v1/{workspace_id}/external-role-assignments HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 221
{
"source": "text",
"external_subject_key": "text",
"external_subject_id": "text",
"entity_id": "123e4567-e89b-12d3-a456-426614174000",
"display_name": "text",
"roles": [
"text"
],
"roles_verified": false,
"provisioned_via": "roster_sync"
}{
"id": "123e4567-e89b-12d3-a456-426614174000",
"source": "text",
"external_subject_key": "text",
"external_subject_id": "text",
"entity_id": "123e4567-e89b-12d3-a456-426614174000",
"display_name": "text",
"roles": [
"text"
],
"roles_verified": true,
"status": "active",
"provisioned_via": "roster_sync",
"last_verified_at": "2026-01-01T00:00:00.000Z",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-01T00:00:00.000Z"
}API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
[]50Successful Response
Validation Error
GET /v1/{workspace_id}/external-role-assignments HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"source": "text",
"external_subject_key": "text",
"external_subject_id": "text",
"entity_id": "123e4567-e89b-12d3-a456-426614174000",
"display_name": "text",
"roles": [
"text"
],
"roles_verified": true,
"status": "active",
"provisioned_via": "roster_sync",
"last_verified_at": "2026-01-01T00:00:00.000Z",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-01T00:00:00.000Z"
}
],
"has_more": true,
"continuation_token": null
}Get an Assignment
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
Successful Response
Assignment not found
Validation Error
GET /v1/{workspace_id}/external-role-assignments/{assignment_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"source": "text",
"external_subject_key": "text",
"external_subject_id": "text",
"entity_id": "123e4567-e89b-12d3-a456-426614174000",
"display_name": "text",
"roles": [
"text"
],
"roles_verified": true,
"status": "active",
"provisioned_via": "roster_sync",
"last_verified_at": "2026-01-01T00:00:00.000Z",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-01T00:00:00.000Z"
}Delete an Assignment
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
Successful Response
No content
Assignment not found
Validation Error
DELETE /v1/{workspace_id}/external-role-assignments/{assignment_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Role Grants
Map a role to a resource with a read or write access level.
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
Successful Response
An active grant for this (role, resource, access) already exists
Validation Error
POST /v1/{workspace_id}/role-grants HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 150
{
"role_id": "123e4567-e89b-12d3-a456-426614174000",
"resource_type": "integration_endpoint",
"resource_key": "text",
"access": "read",
"param_binding": "text"
}{
"id": "123e4567-e89b-12d3-a456-426614174000",
"role_id": "123e4567-e89b-12d3-a456-426614174000",
"resource_type": "integration_endpoint",
"resource_key": "text",
"access": "read",
"param_binding": "text",
"status": "active",
"changed_by": "123e4567-e89b-12d3-a456-426614174000",
"created_at": "2026-01-01T00:00:00.000Z"
}API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
[]50Successful Response
Validation Error
GET /v1/{workspace_id}/role-grants HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"items": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"role_id": "123e4567-e89b-12d3-a456-426614174000",
"resource_type": "integration_endpoint",
"resource_key": "text",
"access": "read",
"param_binding": "text",
"status": "active",
"changed_by": "123e4567-e89b-12d3-a456-426614174000",
"created_at": "2026-01-01T00:00:00.000Z"
}
],
"has_more": true,
"continuation_token": null
}Get a Grant
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
Successful Response
Role grant not found
Validation Error
GET /v1/{workspace_id}/role-grants/{grant_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"role_id": "123e4567-e89b-12d3-a456-426614174000",
"resource_type": "integration_endpoint",
"resource_key": "text",
"access": "read",
"param_binding": "text",
"status": "active",
"changed_by": "123e4567-e89b-12d3-a456-426614174000",
"created_at": "2026-01-01T00:00:00.000Z"
}Delete a Grant
Deleting a grant supersedes it rather than erasing the immutable audit history.
Supersede an active grant (immutable history) — never a physical delete.
API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.
Successful Response
No content
Active role grant not found
Validation Error
DELETE /v1/{workspace_id}/role-grants/{grant_id} HTTP/1.1
Host: platform.api-us.concurrence.com
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
No content
Session Resolution
When a session opens anchored to an entity that has an active role assignment, the platform resolves a session principal. The principal carries:
The external user's identity (subject key and optional subject ID)
The union of roles from the assignment
The allow-grants derived from those roles
The workspace's set of restricted resources (any resource referenced by any active grant)
This resolution happens once at session open. The principal is then used for two purposes throughout the session:
Grant Gating
A resource is restricted if any active grant in the workspace references it. Restriction is a workspace-level fact, not a per-principal one. When a tool call targets a restricted resource, the platform checks whether the principal's roles grant the required access level:
A write grant implies read (you can read what you can write)
A read grant does not imply write
Access is allow-union across all of the principal's roles - there is no deny
Unrestricted resources (those with no grant rows in the workspace) remain workspace-open. Workspaces that define no grants are completely unaffected.
Identity Injection
For integrations that use token exchange authentication, you can declare identity bindings that map authentication parameters to the principal's verified identity attributes (subject key or subject ID). At dispatch time, the bound values are resolved from the principal and injected into the token exchange request. The model never supplies these values - they are stripped from the tool schema presented to the agent and removed from any model-supplied parameters before execution.
This closes the per-user identity impersonation hole: the agent cannot supply or override a bound identity parameter, and a missing principal value fails the call closed rather than proceeding without identity.
Param Bindings
Grants can carry an optional parameter binding constraint (for example, scoping a provider ID parameter to the principal's subject ID). When a grant includes a param binding, the bound value is resolved from the principal and injected into the call parameters at dispatch time. A binding that cannot resolve from the principal fails the call closed.
Integration with Approval Gates
On text channels, the approval gate runs at dispatch, so an approval-gated write can be parked for operator review before grant gating is evaluated. The principal's grants are enforced when the write actually executes: if an operator approves a call the principal's grants deny, the write is still refused at execution. A grant-denied call may therefore appear in the approval queue, but it can never execute.
Sessions Without a Principal
Sessions anchored to entities without an active role assignment proceed without a principal. All tools and resources remain workspace-open, and the session behaves exactly as it did before external principals were introduced. This is the expected path for patient-facing sessions and any workspace that has not configured external roles.
Identity Bindings on Custom Token Exchange
When configuring a custom token exchange integration, the identity_bindings field maps declared parameter names to principal attributes:
principal.subject_key- the external user's primary identity (typically an email address)principal.subject_id- the external user's vendor-specific identifier (when available)
For deployments that only need to bind the authenticated external-user session subject into token exchange auth, use external_user.subject_key as described in External User Subject-Key Binding. Use principal.* when the binding must come from materialized External Principals role assignments.
Each bound parameter name must reference a parameter declared on the integration's token exchange headers or body fields. Bindings for undeclared parameters are rejected at configuration time.
A bound parameter is:
Removed from the tool schema the agent sees
Stripped from any model-supplied parameters at dispatch
Injected from the verified principal before the token exchange runs
If the principal does not have a value for a bound attribute (for example, principal.subject_id when no subject ID was recorded), the call fails closed rather than proceeding with an absent identity.
Example: Per-Clinician Access
External principals are the mechanism for embedding an agent that serves many end users under one workspace, where each user must only reach what their role allows - for example a clinician-support agent embedded in a partner's application, where each clinician holds one or more roles and should only invoke the tools and read the data their roles permit.
The flow:
The external system authenticates the clinician and resolves the clinician's pre-provisioned role assignment to its workspace entity.
A trusted backend creates the conversation with that
entity_idusing a workspace credential that hasConversation:CreateForPrincipal. The API derives all principal roles and grants from the active assignment; the client cannot submit them directly.The session loads the principal for that entity. As the agent calls tools, each call is gated by the principal's grants, and identity-bound parameters are injected from the principal rather than model input.
Because the scoping is per end user, this is distinct from the workspace-scoped MCP Server, which serves an entire workspace to a single credential and is not per-user scoped.
Last updated
Was this helpful?

