For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

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

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

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

Create External Role

post
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Body
namestring · min: 1 · max: 64RequiredPattern: ^[a-z][a-z0-9_]{0,63}$
external_namestring · min: 1 · max: 256Required
descriptionstring · max: 2048 · nullableOptional
Responses
201

Successful Response

application/json

Wire shape for read + create responses.

idstring · uuidRequired
namestringRequired
external_namestringRequired
descriptionstring · nullableOptional
created_atstring · date-timeRequired
updated_atstring · date-timeRequired
post/v1/{workspace_id}/external-roles
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"
}

List External Roles

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Query parameters
sort_bystring[]OptionalDefault: []
limitinteger · max: 200OptionalDefault: 50
continuation_tokenanyOptional
Responses
200

Successful Response

application/json
has_morebooleanRequired
continuation_tokenanyOptional
get/v1/{workspace_id}/external-roles
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

Get External Role

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
role_idstring · uuidRequired
Responses
200

Successful Response

application/json

Wire shape for read + create responses.

idstring · uuidRequired
namestringRequired
external_namestringRequired
descriptionstring · nullableOptional
created_atstring · date-timeRequired
updated_atstring · date-timeRequired
get/v1/{workspace_id}/external-roles/{role_id}
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

Delete External Role

delete
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
role_idstring · uuidRequired
Responses
204

Successful Response

No content

delete/v1/{workspace_id}/external-roles/{role_id}
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.

Upsert External Role Assignment

put
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Body
sourcestring · min: 1 · max: 128Required
external_subject_keystring · min: 1 · max: 512Required
external_subject_idstring · max: 512 · nullableOptional
entity_idstring · uuidRequired
display_namestring · max: 512 · nullableOptional
rolesstring[] · max: 64Optional
roles_verifiedbooleanOptionalDefault: false
provisioned_viastring · enumRequiredPossible values:
Responses
200

Successful Response

application/json
idstring · uuidRequired
sourcestringRequired
external_subject_keystringRequired
external_subject_idstring · nullableOptional
entity_idstring · uuidRequired
display_namestring · nullableOptional
rolesstring[]Required
roles_verifiedbooleanRequired
statusstring · enumRequiredPossible values:
provisioned_viastring · enumRequiredPossible values:
last_verified_atstring · date-time · nullableOptional
created_atstring · date-timeRequired
updated_atstring · date-timeRequired
put/v1/{workspace_id}/external-role-assignments
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"
}

List External Role Assignments

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Query parameters
sort_bystring[]OptionalDefault: []
limitinteger · max: 200OptionalDefault: 50
continuation_tokenanyOptional
Responses
200

Successful Response

application/json
has_morebooleanRequired
continuation_tokenanyOptional
get/v1/{workspace_id}/external-role-assignments
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

Get External Role Assignment

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
assignment_idstring · uuidRequired
Responses
200

Successful Response

application/json
idstring · uuidRequired
sourcestringRequired
external_subject_keystringRequired
external_subject_idstring · nullableOptional
entity_idstring · uuidRequired
display_namestring · nullableOptional
rolesstring[]Required
roles_verifiedbooleanRequired
statusstring · enumRequiredPossible values:
provisioned_viastring · enumRequiredPossible values:
last_verified_atstring · date-time · nullableOptional
created_atstring · date-timeRequired
updated_atstring · date-timeRequired
get/v1/{workspace_id}/external-role-assignments/{assignment_id}
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

Delete External Role Assignment

delete
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
assignment_idstring · uuidRequired
Responses
204

Successful Response

No content

delete/v1/{workspace_id}/external-role-assignments/{assignment_id}
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.

Create Role Grant

post
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Body
role_idstring · uuidRequired
resource_typestring · enumRequiredPossible values:
resource_keystring · min: 1 · max: 512Required
accessstring · enumRequiredPossible values:
param_bindingstring · max: 256 · nullableOptional
Responses
201

Successful Response

application/json
idstring · uuidRequired
role_idstring · uuidRequired
resource_typestring · enumRequiredPossible values:
resource_keystringRequired
accessstring · enumRequiredPossible values:
param_bindingstring · nullableOptional
statusstring · enumRequiredPossible values:
changed_bystring · uuid · nullableOptional
created_atstring · date-timeRequired
post/v1/{workspace_id}/role-grants
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"
}

List Role Grants

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
Query parameters
sort_bystring[]OptionalDefault: []
limitinteger · max: 200OptionalDefault: 50
continuation_tokenanyOptional
statusstring · enum · nullableOptionalPossible values:
Responses
200

Successful Response

application/json
has_morebooleanRequired
continuation_tokenanyOptional
get/v1/{workspace_id}/role-grants
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

Get Role Grant

get
Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
grant_idstring · uuidRequired
Responses
200

Successful Response

application/json
idstring · uuidRequired
role_idstring · uuidRequired
resource_typestring · enumRequiredPossible values:
resource_keystringRequired
accessstring · enumRequiredPossible values:
param_bindingstring · nullableOptional
statusstring · enumRequiredPossible values:
changed_bystring · uuid · nullableOptional
created_atstring · date-timeRequired
get/v1/{workspace_id}/role-grants/{grant_id}
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.

Delete Role Grant

delete

Supersede an active grant (immutable history) — never a physical delete.

Authorizations
AuthorizationstringRequired

API key issued via POST /v1/{workspace_id}/api-keys. Pass the returned api_key value as a Bearer token.

Path parameters
workspace_idstring · uuidRequired
grant_idstring · uuidRequired
Responses
204

Successful Response

No content

delete/v1/{workspace_id}/role-grants/{grant_id}
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:

  1. The external system authenticates the clinician and resolves the clinician's pre-provisioned role assignment to its workspace entity.

  2. A trusted backend creates the conversation with that entity_id using a workspace credential that has Conversation:CreateForPrincipal. The API derives all principal roles and grants from the active assignment; the client cannot submit them directly.

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