# Introduction to the Administration API
Source: https://docs.corti.ai/about/admin-api
Programmatic access to manage your Corti Console account
## What is the Admin API?
The `Admin API` lets you manage your Corti Console programmatically. It is built for administrators who want to automate account operations.
This `Admin API` is separate from the `Corti API` used for speech to text, text generation, and agentic workflows:
* Authentication and scope for the `Admin API` uses email-and-password to obtain a bearer token via `/auth/token`. This token is only used for API administration.
* The `Admin API` endpoints `/customers` and `/users` manage Embedded Assistant end-users.
Please [contact us](mailto:help@corti.ai) if you have questions.
### Use Cases
The following functionality is currently supported by the `Admin API`:
| Feature | Functionality | Scope |
| :------------------- | :----------------------------------------------------------------------- | :----------- |
| **Authentication** | Authenticate user and get access token | All projects |
| **Manage Customers** | Create, update, list, and delete customer accounts within your project | All projects |
| **Manage Users** | Create, update, list, and delete users associated with customer accounts | All projects |
Permissions mirror the Corti Console - only project admins or owners can create, update, or delete resources.
## Quickstart
* Sign up or log in at [console.corti.app](https://console.corti.app/)
* Ensure your account has a password set
Best practice: use a dedicated service account for Admin API automation. Assign only the minimal required role and rotate credentials regularly.
Call `/auth/token` with your Console email and password to obtain a JWT access token.
See API Reference: [Authenticate user and get access token](/api-reference/admin/auth/authenticate-user-and-get-access-token)
```bash theme={null}
curl -X POST https://api.console.corti.app/functions/v1/public/auth/token \
-H "Content-Type: application/json" \
-d '{
"email": "your-email@example.com",
"password": "your-password"
}'
```
Example response:
```json theme={null}
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "bearer",
"expiresIn": 3600
}
```
Include the token in the Authorization header for subsequent requests:
```bash theme={null}
curl -X GET https://api.console.corti.app/functions/v1/public/projects/{projectId}/customers \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
Tokens expire after `expiresIn` seconds. Once expired, call the `auth/token` endpoint again to obtain a new token.
***
## Top Pages
Obtain an access token
Create a new customer in a project
Create a new user within a customer
Please [contact us](mailto:help@corti.ai) for support or more information
# Compliance & Trust
Source: https://docs.corti.ai/about/compliance
# Help Center
Source: https://docs.corti.ai/about/help
# Public Roadmap
Source: https://docs.corti.ai/about/roadmap
# A2A JSON-RPC binding
Source: https://docs.corti.ai/agentic-v2/a2a/a2a-json-rpc-binding
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/agents/{agentId}/a2a
The `JSONRPC` protocol binding for A2A v1.0. Accepts a single JSON-RPC 2.0
request whose `method` is one of `SendMessage`, `SendStreamingMessage`,
`GetTask`, `ListTasks`, `CancelTask`, or `SubscribeToTask`.
Streaming methods (`SendStreamingMessage`, `SubscribeToTask`) respond with
`text/event-stream`; all others respond with a single JSON-RPC response.
# Cancel a task (A2A REST)
Source: https://docs.corti.ai/agentic-v2/a2a/cancel-a-task-a2a-rest
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/agents/{agentId}/a2a/tasks/{taskId}:cancel
# Get a task (A2A REST)
Source: https://docs.corti.ai/agentic-v2/a2a/get-a-task-a2a-rest
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents/{agentId}/a2a/tasks/{taskId}
# List tasks (A2A REST)
Source: https://docs.corti.ai/agentic-v2/a2a/list-tasks-a2a-rest
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents/{agentId}/a2a/tasks
# Send a message (A2A REST)
Source: https://docs.corti.ai/agentic-v2/a2a/send-a-message-a2a-rest
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/agents/{agentId}/a2a/message:send
The `HTTP+JSON` binding of A2A `SendMessage`.
# Stream a message (A2A REST)
Source: https://docs.corti.ai/agentic-v2/a2a/stream-a-message-a2a-rest
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/agents/{agentId}/a2a/message:stream
The `HTTP+JSON` binding of A2A `SendStreamingMessage`. Responds with a
`text/event-stream` of `Task`, `statusUpdate`, and `artifactUpdate` events.
# Subscribe to task events (A2A REST)
Source: https://docs.corti.ai/agentic-v2/a2a/subscribe-to-task-events-a2a-rest
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/agents/{agentId}/a2a/tasks/{taskId}:subscribe
Resubscribe to an in-flight task's event stream over SSE.
# Get the A2A agent card
Source: https://docs.corti.ai/agentic-v2/agent-card/get-the-a2a-agent-card
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents/{agentId}/.well-known/agent-card.json
Returns the A2A v1.0 agent card describing the agent's capabilities,
skills, and supported protocol interfaces. Served at the standard
`.well-known` location for agent discovery.
# Create an agent
Source: https://docs.corti.ai/agentic-v2/agents/create-an-agent
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/agents
Creates a new agent. The server assigns the UUIDv7 `id`.
# Delete an agent
Source: https://docs.corti.ai/agentic-v2/agents/delete-an-agent
/agentic/auto-generated-openapi-v2.yml delete /v2/agentic/agents/{agentId}
Deletes a `persistent` agent. `ephemeral` agents are expired in place.
Idempotent: deleting an already-deleted agent returns `204`.
# Get an agent
Source: https://docs.corti.ai/agentic-v2/agents/get-an-agent
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents/{agentId}
# List agents
Source: https://docs.corti.ai/agentic-v2/agents/list-agents
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents
Lists agents visible to the caller. `private` agents are visible only to
their creator/service principal; `unlisted` agents are omitted (fetch by
ID instead); `public` agents are listed tenant-wide.
The `visibility`, `lifecycle`, `label`, and `q` filter parameters are accepted but not yet honored by the server; the response is unfiltered.
# Update an agent (partial)
Source: https://docs.corti.ai/agentic-v2/agents/update-an-agent-partial
/agentic/auto-generated-openapi-v2.yml patch /v2/agentic/agents/{agentId}
Partially updates an agent using JSON Merge Patch (RFC 7386).
Omitted fields are unchanged; `null` clears a field; arrays replace.
# Get an artifact
Source: https://docs.corti.ai/agentic-v2/artifacts/get-an-artifact
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/contexts/{contextId}/tasks/{taskId}/artifacts/{artifactId}
Returns an artifact produced by a task within a context. File parts may
carry inline `bytes` or a `uri` to fetch the content out of band.
# Attach a connector
Source: https://docs.corti.ai/agentic-v2/connectors/attach-a-connector
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/agents/{agentId}/connectors
# Get an agent-scoped connector
Source: https://docs.corti.ai/agentic-v2/connectors/get-an-agent-scoped-connector
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents/{agentId}/connectors/{agentConnectorId}
# List an agent's connectors
Source: https://docs.corti.ai/agentic-v2/connectors/list-an-agents-connectors
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents/{agentId}/connectors
# Remove a connector
Source: https://docs.corti.ai/agentic-v2/connectors/remove-a-connector
/agentic/auto-generated-openapi-v2.yml delete /v2/agentic/agents/{agentId}/connectors/{agentConnectorId}
# Update an agent-scoped connector (partial)
Source: https://docs.corti.ai/agentic-v2/connectors/update-an-agent-scoped-connector-partial
/agentic/auto-generated-openapi-v2.yml patch /v2/agentic/agents/{agentId}/connectors/{agentConnectorId}
Partially updates an agent-scoped connector using JSON Merge Patch
(RFC 7386). `type` is immutable.
**Future scope**: not yet implemented; the server returns `501`.
# Delete or expire a context
Source: https://docs.corti.ai/agentic-v2/contexts/delete-or-expire-a-context
/agentic/auto-generated-openapi-v2.yml delete /v2/agentic/contexts/{contextId}
# Export the observability trace for a context
Source: https://docs.corti.ai/agentic-v2/contexts/export-the-observability-trace-for-a-context
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/contexts/{contextId}/trace
Returns the execution traces for the context — LLM calls, tool
executions, and token usage — in OpenInference format. Traces are
ordered newest-first and paginated; each page returns up to `pageSize`
traces with their spans inlined.
# Get a context
Source: https://docs.corti.ai/agentic-v2/contexts/get-a-context
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/contexts/{contextId}
Returns the context's metadata together with its `tasks`, oldest first.
Each task carries its full message `history`; the user's prompt for a
task is the `ROLE_USER` message within that task's history (there is no
separate top-level message list).
# Get a task within a context
Source: https://docs.corti.ai/agentic-v2/contexts/get-a-task-within-a-context
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/contexts/{contextId}/tasks/{taskId}
# List contexts
Source: https://docs.corti.ai/agentic-v2/contexts/list-contexts
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/contexts
Lists contexts matching the filters.
**Future scope**: not yet implemented; the server currently returns an empty page and ignores all parameters.
# List tasks within a context
Source: https://docs.corti.ai/agentic-v2/contexts/list-tasks-within-a-context
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/contexts/{contextId}/tasks
# Delete all feedback for a task
Source: https://docs.corti.ai/agentic-v2/feedback/delete-all-feedback-for-a-task
/agentic/auto-generated-openapi-v2.yml delete /v2/agentic/contexts/{contextId}/tasks/{taskId}/feedback
Soft-deletes every feedback resource the authenticated user submitted for the task. The task must exist, belong to the supplied context, and belong to the authenticated customer. Idempotent: deleting when there is no feedback returns `204`.
# List feedback for a task
Source: https://docs.corti.ai/agentic-v2/feedback/list-feedback-for-a-task
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/contexts/{contextId}/tasks/{taskId}/feedback
Returns all feedback resources submitted for the task by the authenticated user, newest-first. The task must exist, belong to the supplied context, and belong to the authenticated customer. Feedback is scoped to the calling user via row-level security, so the response contains only that user's feedback.
# Submit feedback for a task
Source: https://docs.corti.ai/agentic-v2/feedback/submit-feedback-for-a-task
/agentic/auto-generated-openapi-v2.yml post /v2/agentic/contexts/{contextId}/tasks/{taskId}/feedback
Submits feedback about a task as a whole or about a specific user-visible
message within the task. The task must exist, belong to the supplied
context, and belong to the authenticated customer. Multiple feedback
resources may be submitted for the same task or message.
# Get registry connector details and configuration schema
Source: https://docs.corti.ai/agentic-v2/registry/get-registry-connector-details-and-configuration-schema
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/registry/connectors/{connectorId}
# List available registry connectors
Source: https://docs.corti.ai/agentic-v2/registry/list-available-registry-connectors
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/registry/connectors
# Get an agent's usage
Source: https://docs.corti.ai/agentic-v2/usage/get-an-agents-usage
/agentic/auto-generated-openapi-v2.yml get /v2/agentic/agents/{agentId}/usage
Returns invocation metrics for the agent over the half-open `[from, to)`
time range (UTC), bucketed at the requested `granularity`. The response
echoes the resolved range and granularity, a `totals` summary across the
whole range, and one `buckets` entry per period that had activity (the
array is empty when there was none). When `from`/`to` are omitted, the
range defaults to the last 30 days.
# A2A protocol
Source: https://docs.corti.ai/agentic/a2a-protocol
Learn about the Agent-to-Agent protocol in the Agentic Framework: v1.0 only, the two protocol bindings, and how A2A relates to MCP.
The **Agent-to-Agent (A2A)** protocol is an open standard that enables secure, framework-agnostic communication between autonomous AI agents. Instead of building bespoke integrations whenever you want agents to collaborate, A2A gives Corti and other systems a common language agents can use to discover, talk to, and delegate work to one another.
For the full technical specification, see the official A2A project docs at [a2a-protocol.org](https://a2a-protocol.org/latest/).
## Why Corti uses A2A
We chose A2A because it:
* **Standardizes agent communication**: Agents can talk to each other without siloed, point-to-point integrations. That makes composite workflows easier to build and maintain.
* **Supports real workflows**: A2A includes discovery, task negotiation, and streaming updates, so agents can coordinate long-running or multi-step jobs.
* **Preserves security and opacity**: Agents exchange structured messages without sharing internal memory or tools. That protects intellectual property and keeps interactions predictable.
* **Leverages open tooling**: There are open source SDKs in multiple languages and example implementations you can reuse.
## A2A v1.0 in Corti
The Agentic Framework supports **A2A v1.0 only**. The previous v0.3 protocol version is no longer supported. All requests and responses follow the v1.0 specification.
### A2A-Version header
The server returns an `A2A-Version: 1.0` header on every A2A response, indicating the protocol version used to handle the request. Client SDKs may also send this header on requests.
## Protocol bindings
A2A v1.0 defines two protocol bindings. Corti supports both, and they share the same base URL:
```
/v2/agentic/agents/{agentId}/a2a
```
### JSON-RPC binding
The JSON-RPC binding sends a JSON-RPC 2.0 envelope to the base URL. The `method` field determines the operation:
| JSON-RPC method | Description |
| ---------------------- | ---------------------------------------------- |
| `SendMessage` | Send a message (blocking or non-blocking) |
| `SendStreamingMessage` | Send a message and stream the response via SSE |
| `GetTask` | Get a task by ID |
| `ListTasks` | List tasks for the agent |
| `CancelTask` | Cancel a task |
| `SubscribeToTask` | Subscribe to task events via SSE |
```json theme={null}
{
"jsonrpc": "2.0",
"id": "msg-001",
"method": "SendMessage",
"params": {
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "What is the ICD-10 code for asthma?" }]
}
}
}
```
### HTTP+JSON binding
The HTTP+JSON binding uses path-suffixed endpoints. Each JSON-RPC method maps to a specific HTTP endpoint:
| JSON-RPC method | HTTP endpoint |
| ---------------------- | ------------------------------------ |
| `SendMessage` | `POST /a2a/message:send` |
| `SendStreamingMessage` | `POST /a2a/message:stream` |
| `ListTasks` | `GET /a2a/tasks` |
| `GetTask` | `GET /a2a/tasks/{taskId}` |
| `CancelTask` | `POST /a2a/tasks/{taskId}:cancel` |
| `SubscribeToTask` | `POST /a2a/tasks/{taskId}:subscribe` |
The HTTP+JSON binding is simpler for most use cases. Use the JSON-RPC binding if your client or tooling expects a JSON-RPC envelope.
### REST query parameters
The HTTP+JSON binding supports query parameters on certain endpoints:
| Parameter | Endpoints | Description |
| --------------- | --------------------------------------------------- | ----------------------------------------------------- |
| `historyLength` | `GET /a2a/tasks/{taskId}`, `POST /a2a/message:send` | Cap the number of history messages returned per task |
| `pageSize` | `GET /a2a/tasks` | Page size for list responses |
| `pageToken` | `GET /a2a/tasks` | Opaque cursor from a prior response's `nextPageToken` |
| `contextId` | `GET /a2a/tasks` | Filter tasks by context |
## Agent discovery
A2A enables agent discovery through [agent cards](/agentic/agent-cards). Every agent exposes its card at the `.well-known/agent-card.json` location, describing its capabilities, skills, and supported protocol bindings. You can retrieve a card to inspect an agent before integrating. The agent card endpoint requires the same authentication (bearer token and tenant header) as all other v2 endpoints.
## Relationship to MCP
MCP (Model Context Protocol) and A2A serve different purposes in the Agentic Framework:
* **A2A** is the conversational surface: how clients talk to agents, send messages, receive tasks, and stream responses
* **MCP** is a connector type: how agents call external tool servers to retrieve data or perform actions
In v2, MCP servers are attached as [connectors](/agentic/connectors) (type `mcp`) to an agent. The agent runtime calls the MCP server when it needs data or actions from that tool. A2A is the protocol the client uses to talk to the agent itself.
The v1 MCP protocol and authentication pages are preserved in the [v1 archived documentation](/agentic/v1/mcp-protocol). In v2, MCP concepts are covered by the [connectors](/agentic/connectors) and [connector auth](/agentic/guides/connector-auth) pages.
## Open source SDKs and tooling
For links to Corti's official SDK and the official A2A project SDKs (Python, JavaScript/TypeScript, Java, Go, and .NET), see [SDKs and integrations](/agentic/sdks-integrations).
## Next steps
* Learn about [agent cards](/agentic/agent-cards) for A2A discovery
* Read about [connectors](/agentic/connectors) and how MCP fits as a connector type
* Follow the [quickstart](/agentic/quickstart) to send your first A2A message
# Agent cards
Source: https://docs.corti.ai/agentic/agent-cards
Learn how A2A agent cards enable discovery and describe an agent's capabilities, skills, and supported protocol bindings.
An agent card is a JSON document that describes an agent's identity, capabilities, skills, and supported protocol bindings. It serves as a machine-readable business card that lets clients discover agents and determine how to communicate with them.
Agent cards follow the A2A v1.0 specification and are served at the standard `.well-known` location.
## Retrieve an agent card
Every agent exposes its card at:
```
GET /v2/agentic/agents/{agentId}/.well-known/agent-card.json
```
This endpoint requires authentication (bearer token and tenant header), same as all other v2 API endpoints.
The response includes an `A2A-Version: 1.0` header indicating the protocol version the server uses.
## Card structure
| Field | Type | Description |
| --------------------- | --------- | --------------------------------------------------------------------------- |
| `name` | string | Agent display name |
| `description` | string | What the agent does |
| `version` | string | Agent card version (SemVer) |
| `capabilities` | object | Capability flags: `streaming`, `pushNotifications` |
| `defaultInputModes` | string\[] | Default input media types (e.g. `text/plain`) |
| `defaultOutputModes` | string\[] | Default output media types |
| `provider` | object | Publishing organization and URL |
| `skills` | object\[] | Skills the agent exposes, each with `id`, `name`, `description`, and `tags` |
| `supportedInterfaces` | object\[] | A2A protocol bindings the agent supports |
| `documentationUrl` | string | URL providing additional documentation about the agent |
| `iconUrl` | string | Optional URL to an icon for the agent |
### Capabilities
The `capabilities` object advertises what the agent can do:
* **`streaming`**: Whether the agent supports streaming responses via SSE. If `true`, you can use `message:stream` and `tasks:subscribe`.
* **`pushNotifications`**: Whether the agent can push task updates to a client-supplied webhook.
`pushNotifications` is future scope. The `tasks/pushNotificationConfig/*` management endpoints are not yet implemented. Expect this field to be `false` until they ship.
### Supported interfaces
Each entry in `supportedInterfaces` declares one A2A protocol binding:
| Property | Description |
| ----------------- | ------------------------------------------- |
| `protocolBinding` | The binding type: `JSONRPC` or `HTTP+JSON` |
| `protocolVersion` | Always `1.0` (Corti supports A2A v1.0 only) |
| `url` | The endpoint URL for this binding |
Both bindings share the same base URL (`/v2/agentic/agents/{agentId}/a2a`). The `JSONRPC` binding sends a JSON-RPC envelope; the `HTTP+JSON` binding uses path-suffixed endpoints like `message:send` and `message:stream`. See [A2A protocol](/agentic/a2a-protocol) for details.
### Skills
Each skill in the `skills` array represents a capability the agent exposes through its connectors. The `id` is a bare UUID (the connector's external ID). Skills help clients understand what an agent can do before sending a message.
## Using cards for discovery
Agent cards enable a discovery workflow:
1. A client fetches the card from a known agent URL.
2. The client inspects `supportedInterfaces` to determine which protocol binding to use.
3. The client checks `capabilities.streaming` to decide whether to use blocking or streaming calls.
4. The client reviews `skills` to understand what the agent can do.
5. The client sends a message using the appropriate binding.
Retrieving an agent card requires the same authentication as any other v2 API request. You need a valid bearer token and tenant header.
## Card example
The following is a typical agent card for a coding agent:
```json theme={null}
{
"name": "coder",
"description": "Returns ICD-10 codes for a clinical encounter.",
"version": "0.1.0",
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"provider": {
"organization": "Corti",
"url": "https://corti.ai"
},
"skills": [
{
"id": "con.0192f4c8-7baf-7083-a46f-81d2bd70cf95",
"name": "coding-expert",
"description": "ICD-10 coding.",
"tags": ["expert"]
}
],
"supportedInterfaces": [
{
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0",
"url": "https://api.eu.corti.app/v2/agentic/agents/agt.0192f4c8-2c5a-7b3e-9f1a-3c8d6e2b7a40/a2a"
},
{
"protocolBinding": "HTTP+JSON",
"protocolVersion": "1.0",
"url": "https://api.eu.corti.app/v2/agentic/agents/agt.0192f4c8-2c5a-7b3e-9f1a-3c8d6e2b7a40/a2a"
}
]
}
```
## Next steps
* Learn about [connectors](/agentic/connectors), which power the skills advertised in the card
* Read the [A2A protocol](/agentic/a2a-protocol) page for details on the two protocol bindings
* Follow the [quickstart](/agentic/quickstart) to create an agent and retrieve its card
# Agent SDK API reference
Source: https://docs.corti.ai/agentic/agent-sdk/api-reference
Full method and type reference for the Corti Agent SDK in TypeScript and Python.
The Corti Agent SDK is in **alpha v2 private preview**. The API may change between releases. Contact [help@corti.ai](mailto:help@corti.ai) to request access.
This page is a complete reference for every public class, method, and type exported by the SDK.
## AgentsClient and AgentsResource
In TypeScript, agent CRUD lives on `client.agents` (`AgentsResource`). In Python, it lives on `AgentsClient(client)`.
| Method | Returns | Description |
| --------------------------------------- | ------------------------------------------------------- | ------------------------------------------ |
| `create(body)` / `create(...)` | `Agent` (TS) / `AgentHandle` (Python) | Create a new agent |
| `get(agentId)` / `get(agent_id)` | `Agent` (TS) / `AgentHandle` (Python) | Fetch an existing agent by ID |
| `list(params?)` / `list()` | `AgentListResponse` (TS) / `list[AgentHandle]` (Python) | List agents in the tenant |
| `update(agentId, body)` / `update(...)` | `Agent` (TS) / `AgentHandle` (Python) | Partial update via JSON Merge Patch |
| `delete(agentId)` / `delete()` | `void` | Delete an agent |
| `wrap(agent)` (Python only) | `AgentHandle` | Wrap a raw API dict without a network call |
In TypeScript, `create()` and `get()` return raw `Agent` objects. Wrap them with `new AgentHandle(agent, client)` to get conversation helpers.
## AgentHandle
| Member | Type | Description |
| ------------------------------------------------ | ---------------------------------- | --------------------------------------------------- |
| `id` | `string` | Agent ID (server-assigned) |
| `name` | `string` | Agent name |
| `description` | `string \| undefined` | Agent description |
| `systemPrompt` / `system_prompt` | `string \| undefined` | System prompt |
| `model` | `string \| undefined` (TS only) | Model the agent uses |
| `visibility` | `string` | `private`, `unlisted`, or `public` |
| `lifecycle` | `string` | `ephemeral` or `persistent` |
| `connectors` | `Connector[]` | Attached connectors |
| `labels` | `Record` (TS only) | Free-form labels |
| `raw` | `Agent` / `Dict` | Underlying API response object |
| `createContext(opts?)` / `create_context(...)` | `AgentContext` | Open a new conversation thread (lazy) |
| `getContext(id, opts?)` / `get_context(id, ...)` | `AgentContext` | Resume an existing thread by ID |
| `run(input, opts?)` | `Promise` | One-shot invoke: create context, send, return reply |
| `stream(input, opts?)` (TS only) | `AsyncGenerator` | One-shot streaming invoke |
| `update(patch)` / `update(...)` | `Promise` | Partially update the agent, returns a new handle |
| `refresh()` | `Promise` | Re-fetch the agent from the API |
| `delete()` | `Promise` | Delete the agent |
## AgentContext
| Member | Type | Description |
| ------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------- |
| `id` | `string \| undefined` | Thread ID, `undefined` / `None` until the first turn completes |
| `sendText(text, opts?)` / `send_text(text, ...)` | `Promise` | Send plain text |
| `sendMessage(parts, opts?)` / `send_message(parts, ...)` | `Promise` | Send arbitrary `Part[]` (text, data, file) |
| `streamMessage(parts, opts?)` / `stream_message(parts)` | `AsyncGenerator` | Stream incremental events for the reply |
| `getTask(taskId, opts?)` / `get_task(task_id, ...)` (TS only) | `Promise` | Fetch a task by ID |
| `cancelTask(taskId, opts?)` / `cancel_task(task_id, ...)` (TS only) | `Promise` | Cancel a running task |
### SendMessageOptions
| Field | Type | Description |
| ----------------------------------------- | ------------------------- | ----------------------------------------- |
| `historyLength` | `number` | Number of history messages to include |
| `blocking` | `boolean` | Wait for task completion before returning |
| `acceptedOutputModes` | `string[]` | Output modes the client accepts |
| `metadata` | `Record` | Free-form metadata |
| `timeoutInSeconds` / `timeout_in_seconds` | `number` | Per-request timeout override (default 60) |
| `abortSignal` (TS only) | `AbortSignal` | Abort the request |
| `credentials` (Python only) | `CredentialStore` | MCP auth credentials |
## MessageResponse
| Member | Type | Description |
| -------------------------- | ------------------------------ | --------------------------------------------------------------------------------------- |
| `text` | `string \| null` | All text parts from the agent reply, joined |
| `status` | `string` | Task state: `completed`, `failed`, `working`, `submitted`, `canceled`, `input-required` |
| `state` (TS only) | `TaskState` | Raw task state enum value |
| `message` (TS only) | `Message \| undefined` | The agent's reply message |
| `task` (TS only) | `Task \| undefined` | The full A2A task object |
| `taskId` / `task_id` | `string \| undefined` | Task ID for this invocation |
| `contextId` / `context_id` | `string \| undefined` | Thread ID, set after the first turn |
| `artifacts` | `Artifact[]` | Structured outputs, deduplicated by parts content |
| `raw` | `SendMessageResponse` / `Dict` | Full, unmodified API response |
## Composition primitives
| Function | Returns | Description |
| -------------------------------------------------------- | --------------------------- | --------------------------------------------------------- |
| `workflow(steps)` | `Workflow` | Build a deterministic pipeline |
| `Workflow.run(input)` | `Promise` | Execute the pipeline |
| `parallel(steps)` | `Parallel` | Build a fan-out block |
| `Parallel.run(input)` | `Promise` | Run all steps concurrently |
| `stateGraph()` / `stateGraph()` | `StateGraph` | Build a graph with cycles and typed shared state |
| `StateGraph.addNode(name, nodeFn)` / `add_node(...)` | `StateGraph` | Register a node. Use `agentNode()` for agent-backed nodes |
| `StateGraph.addEdge(from, to)` / `add_edge(...)` | `StateGraph` | `to` may be a node name, `END`, or an `EdgeRouter` |
| `StateGraph.run(start, state, opts?)` | `Promise` | Execute. `opts.maxIterations` bounds cycles (default 25) |
| `agentNode(agent, inputFn, mergeFn)` / `agent_node(...)` | `NodeFn` | Wrap an agent as a graph node |
| `END` | `EndType` | Sentinel to terminate a `StateGraph` run |
## Connector helpers
### TypeScript
| Helper | Returns | Description |
| ------------------------------------------ | ------------------------- | ----------------------------------------------- |
| `connectors.registry(name, opts?)` | `RegistryConnectorCreate` | Use a published Corti registry connector |
| `connectors.mcp({ name, url, auth? })` | `McpConnectorCreate` | Attach an MCP server |
| `connectors.agent(agentId, opts?)` | `AgentConnectorCreate` | Wire another Corti agent as a sub-agent |
| `connectors.a2a(url, opts?)` | `A2AConnectorCreate` | Connect to a remote A2A agent |
| `connectors.schema({ name, schema, ... })` | `SchemaConnectorCreate` | Define a JSON Schema tool for structured output |
### Python
| Helper | Returns | Description |
| ------------------------------------------------------------------- | --------------------- | -------------------------------------------- |
| `connectors.registry(name, *, system_prompt?)` | `RegistryConnector` | Use a named expert from the registry |
| `connectors.mcp(mcp_url, *, name?, transport?, auth_type?, token?)` | `McpConnector` | Attach an MCP server |
| `connectors.from_agent(agent_id)` | `CortiAgentConnector` | Reference another Corti agent as a sub-agent |
| `connectors.a2a(a2a_url)` | `A2AConnector` | A2A protocol (reserved, not yet supported) |
### Auth helpers (TypeScript only)
| Helper | Returns | Description |
| --------------------------------------------- | --------------- | ------------------------------------------------------- |
| `auth.none()` | `ConnectorAuth` | No auth required |
| `auth.bearer()` | `ConnectorAuth` | Bearer token auth |
| `auth.apiKey(ref?)` | `ConnectorAuth` | API key auth, optionally with a stored secret reference |
| `auth.oauth2({ scope?, redirectUrl?, ref? })` | `ConnectorAuth` | OAuth2 auth |
## Types
### Parts
| Type | Shape | Notes |
| ---------- | ---------------------------------------------- | ------------------------------ |
| `Part` | `TextPart \| DataPart \| FilePart` | Message content |
| `TextPart` | `{ text: string }` | Plain text |
| `DataPart` | `{ data: unknown }` | Structured payload |
| `FilePart` | `{ file: { name?, mimeType?, bytes?, uri? } }` | Send `bytes` (base64) or `uri` |
### Tasks and messages
| Type | Shape | Notes |
| ----------- | --------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `Task` | Full A2A task object | Exposed via `MessageResponse.task` / `MessageResponse.raw` |
| `Message` | `{ role, parts, messageId, contextId?, taskId? }` | An A2A message |
| `Artifact` | `{ artifactId, name?, parts }` | Structured output from the agent |
| `TaskState` | `submitted \| working \| input-required \| completed \| canceled \| failed` | Returned in `MessageResponse.status` |
### Composition types
| Type | Shape | Notes |
| -------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `Runnable` | `{ run(input): Promise }` | What `workflow`/`parallel` accept besides `AgentHandle` |
| `WorkflowStep` | `AgentHandle \| Parallel \| { agent, when?, transform?, retries?, retryDelay? }` | A workflow step |
| `WorkflowResult` | `{ output, steps, stoppedEarly }` | Result of `Workflow.run()` |
| `ParallelStep` | `AgentHandle \| { agent, input?, credentials? }` | A parallel step |
| `ParallelResult` | `{ results, fulfilled, rejected }` | Result of `Parallel.run()` |
| `EdgeRouter` | `(state: S) => string \| END` | Function form of `addEdge` |
| `NodeFn` | `(state: S) => Promise> \| Partial` | Custom graph node function |
| `StateGraphStep` | `{ node: string, delta: Partial }` | Per-step trace entry |
| `StateGraphResult` | `{ state, steps, iterations, terminatedBy }` | Result of `StateGraph.run()` |
### Error types
| Type | Description |
| ----------------- | ------------------------------------------------------------------------- |
| `CortiError` | Base class for all SDK errors |
| `ManagementError` | Management-plane error (agent CRUD, connectors, etc.) |
| `A2AError` | A2A-plane error (message send, stream, task operations) |
| `HttpError` | HTTP error when the response body cannot be parsed as a known error shape |
## TypeScript vs Python naming
The wire format (A2A JSON-RPC) is identical: both SDKs talk to the same API. The naming conventions differ:
| TypeScript | Python |
| --------------------------- | ---------------------------------------------------- |
| `new CortiClient({...})` | `async with CortiClient(...) as client:` |
| `sendText()` | `await send_text()` |
| `sendMessage()` | `await send_message()` |
| `streamMessage()` | `stream_message()` |
| `createContext()` | `create_context()` |
| `getContext(id)` | `get_context(id)` |
| `systemPrompt` | `system_prompt` |
| `timeoutInSeconds` | `timeout_in_seconds=` (keyword-only) |
| `connectors.agent(agentId)` | `connectors.from_agent(agent_id)` |
| `connectors.mcp({ url })` | `connectors.mcp(mcp_url=...)` |
| `agentNode()` | `agent_node()` |
| `stateGraph()` | `stateGraph()` (same: Python also exports camelCase) |
| `terminatedBy` | `terminated_by` |
| `stoppedEarly` | `stopped_early` |
| `retryDelay` | `retry_delay` |
# Common pitfalls
Source: https://docs.corti.ai/agentic/agent-sdk/common-pitfalls
Avoid the most frequent mistakes when using the Corti Agent SDK.
The Corti Agent SDK is in **alpha v2 private preview**. The API may change between releases. Contact [help@corti.ai](mailto:help@corti.ai) to request access.
## Timeouts
**Orchestrators time out at 60 seconds by default.** Anything that fans out to sub-agents or MCP connectors should pass `timeoutInSeconds: 180` (or higher) to `run()` or `sendText()`. The default 60-second timeout is fine for single-agent calls but will cut off orchestrators that delegate to multiple connectors.
```ts title="JavaScript" theme={null}
// Bad: default 60s timeout may be too short for orchestrators
const reply = await handle.run("Complex clinical question...");
// Good: raise the timeout for orchestrators
const reply = await handle.run("Complex clinical question...", { timeoutInSeconds: 180 });
```
```python title="Python" theme={null}
# Bad: default 60s timeout may be too short for orchestrators
reply = await agent.run("Complex clinical question...")
# Good: raise the timeout for orchestrators
reply = await agent.run("Complex clinical question...", timeout_in_seconds=180)
```
## Context IDs
**You do not need to manage context IDs.** Keep the `AgentContext` object in memory across turns. The SDK tracks the thread automatically. Only use `handle.getContext(id)` / `agent.get_context(id)` when resuming after a process restart.
**`ctx.id` is `undefined` / `None` until the first turn completes.** Do not persist it before then.
**`createContext()` does not accept a context ID.** Passing `{ contextId }` will be silently ignored. Use `handle.getContext(contextId)` / `agent.get_context(context_id)` to resume an existing thread.
## Workflow vs stateGraph
**`workflow` is linear, `stateGraph` has cycles.** Do not use `workflow` for branching that revisits earlier agents. If you need loops, conditional routing based on accumulated state, or a reviewer-approval cycle, use `stateGraph` instead.
## Parallel failures
**`parallel` swallows individual failures.** Inspect `rejected` if you need fail-fast semantics. When used inside a `workflow`, if all parallel steps fail, the workflow throws.
```ts title="JavaScript" theme={null}
const result = await parallel([a, b, c]).run(input);
if (result.rejected.length > 0) {
console.warn(`${result.rejected.length} steps failed`);
for (const err of result.rejected) {
console.error(err);
}
}
```
```python title="Python" theme={null}
result = await parallel([a, b, c]).run(input)
if result.rejected:
print(f"{len(result.rejected)} steps failed")
for err in result.rejected:
print(err)
```
## Persistent agents
**Persistent agents accumulate.** The default is `ephemeral`; only use `persistent` if you truly need the agent to survive restarts, and always delete what you no longer need.
```ts title="JavaScript" theme={null}
const agent = await client.agents.create({ name: "my-bot", description: "...", lifecycle: "persistent" });
// ... use it across sessions ...
// Clean up when done
const handle = new AgentHandle(agent, client);
await handle.delete();
```
```python title="Python" theme={null}
agent = await agents.create(name="my-bot", description="...", lifecycle="persistent")
# ... use it across sessions ...
# Clean up when done
await agent.delete()
```
## ESM only (TypeScript)
The TypeScript package is ESM-only (`"type": "module"`). Import with `import`, not `require`. For CommonJS projects, use dynamic `import()` or set your project to ESM.
## Self-sufficient prompts
**Sub-agents that ask for clarification will stall an orchestrator.** Prompt worker agents with instructions like "Never ask for clarification" so they always produce output. An orchestrator that delegates to a sub-agent which asks a question will hang waiting for user input that never comes.
## Credential keys must match connector names (Python only)
In the Python SDK (v1 architecture), the credential key in the `CredentialStore` must match the connector's `name`:
```python theme={null}
# The connector name "my-mcp" must match the credential key
agent = await agents.create(
name="auth-demo",
description="...",
connectors=[connectors.mcp(mcp_url=MCP_URL, name="my-mcp", auth_type="bearer")],
)
ctx = agent.create_context(credentials={
"my-mcp": {"type": "token", "token": MCP_TOKEN}, # key matches connector name
})
```
In the TypeScript v2 SDK, auth is configured on the connector itself at creation time, so this pattern does not apply.
## Streaming field names
**Use `event.message`, not `event.msg`.** The streaming event field for message updates is `message`. The field for artifact updates is `artifactUpdate`, not `artifact`.
```ts title="JavaScript" theme={null}
// Correct
if (event.message) { /* ... */ }
if (event.artifactUpdate) { /* ... */ }
// Wrong: these fields do not exist
if (event.msg) { /* ... */ }
if (event.artifact) { /* ... */ }
```
# Multi-agent composition
Source: https://docs.corti.ai/agentic/agent-sdk/composition
Build deterministic workflows, fan out in parallel, and route with state graphs using the Corti Agent SDK.
The Corti Agent SDK is in **alpha v2 private preview**. The API may change between releases. Contact [help@corti.ai](mailto:help@corti.ai) to request access.
The Agent SDK provides three composition primitives for coordinating multiple agents. Start with `workflow`; only reach for `stateGraph` when you need cycles or branching that depends on accumulated state.
## At a glance
The three patterns differ in topology:
```mermaid theme={null}
flowchart LR
Input --> A[Agent A summarize] --> B[Agent B classify] --> C[Agent C escalate] --> Output
B -.->|when: not urgent| Skip[skipped]
Skip --> Output
```
Each step runs in order. A `when` predicate can skip a step, and `transform` can rewrite the input to the next step.
```mermaid theme={null}
flowchart TD
Input --> A[Agent A differential]
Input --> B[Agent B red flags]
Input --> C[Agent C workup]
A --> Join[Join fulfilled results with newlines]
B --> Join
C --> Join
Join --> Output
```
All agents run at the same time on the same input. Fulfilled results are collected; rejected steps are isolated.
```mermaid theme={null}
flowchart TD
Start --> Triage
Triage -->|urgent| Coder
Triage -->|routine| End[END]
Coder --> Reviewer
Reviewer -->|approved| End
Reviewer -->|rejected| Coder
```
Nodes share a typed state object. Routing functions inspect the state after each node and pick the next node, `END`, or loop back. `maxIterations` bounds cycles.
## Workflow
A `Workflow` is a fixed list of steps. Each step receives the previous step's response (or a transform of it) and returns a new response.
```mermaid theme={null}
flowchart LR
Input --> A[Agent A summarize] --> B[Agent B classify] --> C{when?}
C -->|yes| D[Agent C escalate] --> Output
C -->|no| Output
D -.->|failed| Stop[stoppedEarly]
Stop --> Output
```
```ts title="JavaScript" theme={null}
import { workflow } from "@corti/agent-sdk";
const result = await workflow([
summarizer, // step 1: bare agent
classifier, // step 2: bare agent
{ // step 3: full config
agent: escalator,
when: (prev) => (prev.text ?? "").toLowerCase().includes("urgent"),
transform: () => note,
retries: 2,
retryDelay: 500,
},
]).run(note);
console.log(result.output.text); // final step's text
console.log(result.steps); // per-step responses
console.log(result.stoppedEarly); // true if a step failed
```
```python title="Python" theme={null}
from corti_agent_sdk import workflow
result = await workflow([
summarizer, # step 1: bare agent
classifier, # step 2: bare agent
{ # step 3: full config
"agent": escalator,
"when": lambda prev: "urgent" in (prev.text or ""),
"transform": lambda _: note,
"retries": 2,
"retry_delay": 0.5,
},
]).run(note)
print(result.output.text) # final step's text
print(result.steps) # per-step responses
print(result.stopped_early) # True if a step failed
```
### Step options
Each step can be a bare `AgentHandle`, a `Parallel` group (auto-wrapped), or a configuration object:
| Field | Description |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `agent` | Any object with a `run(input)` method: an `AgentHandle`, a `Parallel` group, or a custom runnable |
| `when` | Predicate on the previous response. If false, the step is skipped and the previous response passes forward unchanged |
| `transform` | Map the previous response to a new input for this step. Default: `prev.text` |
| `retries` | Additional attempts when a step returns `status: "failed"` (default 0) |
| `retryDelay` / `retry_delay` | Delay between retries in milliseconds (TypeScript) or seconds (Python), default 1000 ms / 1.0 s |
### WorkflowResult
| Field | Type | Description |
| -------------------------------- | ------------------- | ----------------------------------------------------------- |
| `output` | `MessageResponse` | The last executed response |
| `steps` | `MessageResponse[]` | Responses from every executed step (skipped steps excluded) |
| `stoppedEarly` / `stopped_early` | `boolean` | True when a step failed and stopped execution early |
## Parallel
`Parallel` runs multiple agents concurrently on the same input. Use it standalone or drop it into a workflow step list.
```mermaid theme={null}
flowchart TD
Input --> A[Agent A]
Input --> B[Agent B]
Input --> C[Agent C]
A -->|fulfilled| Join[Join with newlines]
B -->|fulfilled| Join
C -->|rejected| Rejected[rejected array]
Join --> Output
```
```ts title="JavaScript" theme={null}
import { parallel, workflow } from "@corti/agent-sdk";
// Standalone
const fanout = await parallel([differential, redFlags, workup]).run(presentation);
fanout.fulfilled; // MessageResponse[]: successful results
fanout.rejected; // unknown[]: exceptions from failed steps
// Inside a workflow: fulfilled outputs are joined with newlines
const { output } = await workflow([
parallel([differential, redFlags, workup]),
synthesizer,
]).run(presentation);
```
```python title="Python" theme={null}
from corti_agent_sdk import parallel, workflow
# Standalone
fanout = await parallel([differential, red_flags, workup]).run(presentation)
fanout.fulfilled # List[MessageResponse]: successful results
fanout.rejected # List: exceptions from failed steps
# Inside a workflow: fulfilled outputs are joined with newlines
result = await workflow([
parallel([differential, red_flags, workup]),
synthesizer,
]).run(presentation)
```
### Per-step overrides
Pass per-step input or credentials when branches need different data:
```ts title="JavaScript" theme={null}
parallel([
{ agent: differential, input: "specialised prompt for differential" },
{ agent: redFlags, input: "focus on red flags only" },
workup, // bare handle: uses shared input
]);
```
```python title="Python" theme={null}
parallel([
{"agent": differential, "input": "specialised prompt for differential"},
{"agent": red_flags, "input": "focus on red flags only"},
workup, # bare handle: uses shared input
])
```
### ParallelResult
| Field | Type | Description |
| ----------- | --------------------------------------- | -------------------------------------------------------- |
| `results` | `PromiseSettledResult[]` / `List[Dict]` | One entry per step with `status` and `value` or `reason` |
| `fulfilled` | `MessageResponse[]` | Responses from steps that completed without raising |
| `rejected` | `unknown[]` | Exceptions from steps that raised |
`Parallel` swallows individual failures. Inspect `rejected` if you need fail-fast semantics. When used inside a `Workflow`, if all parallel steps fail, the workflow throws.
## StateGraph
When you need **cycles** or branching that depends on accumulated state, use `StateGraph`. Each node mutates a typed state object; edges route to the next node, including loops bounded by `maxIterations`.
```mermaid theme={null}
flowchart TD
Triage -->|severity: urgent| Coder
Triage -->|severity: routine| End[END]
Coder --> Reviewer
Reviewer -->|approved| End
Reviewer -->|rejected| Coder
Coder -.->|maxIterations| End
```
### Concepts
* **State**: a plain typed object that accumulates across every node execution. Each node returns a `Partial` that is shallow-merged in.
* **Nodes**: async functions `(state: S) => Promise>`. Use `agentNode()` to wrap an `AgentHandle`.
* **Edges**: a static node name, `END`, or a routing function `(state: S) => string | END` that runs after the node updates the state.
* **`END`**: sentinel that stops execution. A node with no registered edge also terminates the run.
### Minimal example
```ts title="JavaScript" theme={null}
import { stateGraph, agentNode, END } from "@corti/agent-sdk";
interface TriageState {
note: string;
severity: string;
codes: string;
approved: boolean;
}
const graph = stateGraph()
.addNode("triage", agentNode(
triageAgent,
(s) => s.note,
(r) => ({ severity: r.text ?? "" }),
))
.addNode("coder", agentNode(
coderAgent,
(s) => s.note,
(r) => ({ codes: r.text ?? "" }),
))
.addNode("reviewer", agentNode(
reviewerAgent,
(s) => `Note: ${s.note}\n\nProposed codes: ${s.codes}`,
(r) => ({ approved: (r.text ?? "").toLowerCase().startsWith("approved") }),
))
.addEdge("triage", (s) =>
s.severity.toLowerCase().includes("urgent") ? "coder" : END,
)
.addEdge("coder", "reviewer")
.addEdge("reviewer", (s) => (s.approved ? END : "coder"));
const result = await graph.run("triage", { note, severity: "", codes: "", approved: false }, { maxIterations: 10 });
result.state; // final TriageState
result.steps; // per-node deltas
result.iterations; // number of node executions
result.terminatedBy; // "end" | "maxIterations" | "noEdge"
```
```python title="Python" theme={null}
from corti_agent_sdk import stateGraph, agent_node, END
class TriageState(TypedDict):
note: str
severity: str
codes: str
approved: bool
graph = (
stateGraph()
.add_node("triage", agent_node(
triage_agent,
input_fn=lambda s: s["note"],
merge_fn=lambda r: {"severity": r.text or ""},
))
.add_node("coder", agent_node(
coder_agent,
input_fn=lambda s: s["note"],
merge_fn=lambda r: {"codes": r.text or ""},
))
.add_node("reviewer", agent_node(
reviewer_agent,
input_fn=lambda s: f"Note: {s['note']}\n\nProposed codes: {s['codes']}",
merge_fn=lambda r: {"approved": (r.text or "").lower().startswith("approved")},
))
.add_edge("triage", lambda s: "coder" if "urgent" in s["severity"].lower() else END)
.add_edge("coder", "reviewer")
.add_edge("reviewer", lambda s: END if s["approved"] else "coder")
)
result = await graph.run("triage", {"note": note, "severity": "", "codes": "", "approved": False}, max_iterations=10)
result.state # final state dict
result.steps # per-node deltas
result.iterations # number of node executions
result.terminated_by # "end" | "maxIterations" | "noEdge"
```
### agentNode
Wraps an `AgentHandle` as a node function. Provide two callbacks: one to extract the agent's input from state, and one to merge the response back.
```ts title="JavaScript" theme={null}
agentNode(
myAgent,
(state) => state.input, // extract input from state
(response) => ({ output: response.text ?? "" }), // merge response into state
)
```
```python title="Python" theme={null}
agent_node(
my_agent,
input_fn=lambda s: s["input"],
merge_fn=lambda r: {"output": r.text or ""},
)
```
### Custom nodes
You can add non-agent nodes that transform state directly:
```ts title="JavaScript" theme={null}
graph.addNode("normalize", (s) => ({ note: s.note.trim() }));
```
```python title="Python" theme={null}
graph.add_node("normalize", lambda s: {"note": s["note"].strip()})
```
### Result shape
| Field | Type | Description |
| -------------------------------- | -------------------------------------- | ---------------------------------------------------------- |
| `state` | `S` | Final accumulated state after all nodes ran |
| `steps` | `StateGraphStep[]` | Per-node history: `node` name, `delta`, post-delta `state` |
| `iterations` | `number` | Total node executions (including repeated nodes in cycles) |
| `terminatedBy` / `terminated_by` | `"end" \| "maxIterations" \| "noEdge"` | Why the graph stopped |
### Choosing between workflow and stateGraph
| | `workflow()` | `stateGraph()` |
| ------------ | ------------------------------ | ------------------------------------------------ |
| Shape | Linear list of steps | Named nodes with explicit edges |
| Shared state | Previous response text only | Typed object, accumulated |
| Branching | `when` predicate (skip or run) | Routing function (pick any node) |
| Cycles | None | Supported, bounded by `maxIterations` |
| Best for | Known fixed pipelines | Conditional flows, review loops, dynamic routing |
## Decision guide
| You want to... | Use |
| -------------------------------------------------------------- | ------------ |
| Run a fixed pipeline of agents (A to B to C) | `workflow` |
| Run agents concurrently on the same input | `parallel` |
| Conditionally branch, loop, or share typed state across agents | `stateGraph` |
# Agent SDK core concepts
Source: https://docs.corti.ai/agentic/agent-sdk/concepts
Learn the building blocks of the Corti Agent SDK: client, agents, contexts, connectors, message responses, streaming, and credentials.
The Corti Agent SDK is in **alpha v2 private preview**. The API may change between releases. Contact [help@corti.ai](mailto:help@corti.ai) to request access.
This page covers the core building blocks you use in every Agent SDK application.
## CortiClient
The root client. You create one instance and pass it to every resource client. It handles authentication, base URL resolution, and the `Tenant-Name` and `A2A-Version` headers automatically.
```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/agent-sdk";
// Replace these with your values
const TENANT = "";
const TOKEN = "";
const client = new CortiClient({ token: TOKEN, tenant: TENANT });
```
```python title="Python" theme={null}
from corti_agent_sdk import CortiClient
# Replace these with your values
CLIENT_ID = ""
CLIENT_SECRET = ""
ENVIRONMENT = ""
TENANT = ""
client = CortiClient(
tenant_name=TENANT,
environment=ENVIRONMENT,
auth={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
)
```
The TypeScript client accepts a `tokenProvider` function instead of a static `token` for OAuth flows where the token expires. The client calls it before every request and uses the return value.
The client exposes typed resource clients:
| Property | Covers |
| ------------------ | --------------------------------------------------------------------- |
| `client.agents` | Agent CRUD (`POST`, `GET`, `PATCH`, `DELETE` on `/v2/agentic/agents`) |
| `client.contexts` | Contexts, tasks, and traces |
| `client.registry` | Browse pre-built registry connectors |
| `client.usage` | Agent usage metrics |
| `client.feedback` | Submit task feedback |
| `client.agentCard` | A2A agent card |
| `client.models` | List available models |
The resource clients above are available in the TypeScript SDK. The Python SDK currently provides `AgentsClient` only; additional resource clients will be added in the v2 rebuild.
The TypeScript package also exposes a `client.raw` property: the underlying `openapi-fetch` client for direct API access when you need an endpoint the SDK does not wrap yet.
## Agents
An agent is a reusable unit of behaviour: a name, a description, a system prompt, and a set of tools (connectors). You create agents via the agents resource client, which returns typed `AgentHandle` objects.
### Create an agent
```ts title="JavaScript" theme={null}
import { AgentHandle, connectors } from "@corti/agent-sdk";
// Replace these with your values
const REGISTRY_NAME = "@corti/medical-coding";
const TENANT = "";
const TOKEN = "";
const client = new CortiClient({ token: TOKEN, tenant: TENANT });
const agent = await client.agents.create({
name: "my-agent",
description: "Handles medical coding queries",
systemPrompt: "You are a medical coding assistant.",
lifecycle: "persistent",
connectors: [connectors.registry(REGISTRY_NAME)],
});
const handle = new AgentHandle(agent, client);
```
```python title="Python" theme={null}
from corti_agent_sdk import CortiClient, AgentsClient, connectors
# Replace these with your values
CLIENT_ID = ""
CLIENT_SECRET = ""
ENVIRONMENT = ""
REGISTRY_NAME = "@corti/medical-coding"
TENANT = ""
async with CortiClient(
tenant_name=TENANT,
environment=ENVIRONMENT,
auth={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
) as client:
agents = AgentsClient(client)
agent = await agents.create(
name="my-agent",
description="Handles medical coding queries",
system_prompt="You are a medical coding assistant.",
lifecycle="persistent",
connectors=[connectors.registry(name=REGISTRY_NAME)],
)
```
### Fetching and listing
```ts title="JavaScript" theme={null}
const handle = new AgentHandle(await client.agents.get(""), client);
const list = await client.agents.list();
```
```python title="Python" theme={null}
agent = await agents.get("")
all_agents = await agents.list()
```
### Updating
Only the fields you pass are sent. Passing `connectors` **replaces** the full set.
```ts title="JavaScript" theme={null}
const updated = await handle.update({ systemPrompt: "Be more concise." });
```
```python title="Python" theme={null}
updated = await agent.update(system_prompt="Be more concise.")
```
### AgentHandle
An `AgentHandle` wraps an agent response and provides conversation helpers. In TypeScript, `client.agents.create()` returns a raw `Agent` object; you wrap it explicitly. In Python, `AgentsClient.create()` returns an `AgentHandle` directly.
| Member | Description |
| ------------------------------------------------------- | ------------------------------------------------------------ |
| `id` | Server-assigned agent ID |
| `name`, `description`, `systemPrompt` / `system_prompt` | Agent metadata |
| `raw` | The underlying API response object |
| `createContext()` / `create_context()` | Open a new conversation thread |
| `getContext(id)` / `get_context(id)` | Resume an existing thread by ID |
| `run(input, opts?)` | One-shot: create a context, send a message, return the reply |
| `stream(input, opts?)` | One-shot streaming (TypeScript only) |
| `update(patch)` | Partially update the agent, returns a new handle |
| `refresh()` | Re-fetch the agent from the API |
| `delete()` | Delete the agent |
### Lifecycle: ephemeral vs persistent
Agents default to `ephemeral`, which means the server cleans them up automatically. Use `persistent` only when the agent must survive process restarts.
| | `ephemeral` (default) | `persistent` |
| -------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------- |
| Cleaned up by server | Yes, automatically | No, you must call `delete()` |
| Visible in `agents.list()` | No | Yes |
| Survives process restarts | No | Yes, store `agent.id` |
| Best for | Scripts, request handlers, tests, one-off tasks | Long-lived bots or services where recreating the agent on every deploy is undesirable |
Lifecycle controls the agent definition (system prompt, connectors). Conversation threads (`AgentContext`) are always managed automatically regardless of lifecycle. You never need to create or delete threads manually.
Use ephemeral unless you have a specific reason not to. Persistent agents accumulate in your tenant if you forget to delete them.
## Contexts and conversations
An `AgentContext` represents a single conversation thread. Contexts are lazy: no network call is made until the first message is sent, at which point the server creates the thread and returns a `contextId` that the SDK tracks automatically.
### Multi-turn conversation
```ts title="JavaScript" theme={null}
const ctx = handle.createContext();
const reply1 = await ctx.sendText("What is the ICD-10 code for hypertension?");
console.log(reply1.text); // "The ICD-10 code is I10."
// Follow-ups on the same context remember prior turns
const reply2 = await ctx.sendText("What about type 2 diabetes?");
console.log(reply2.text); // "The ICD-10 code is E11.9."
```
```python title="Python" theme={null}
ctx = agent.create_context()
reply1 = await ctx.send_text("What is the ICD-10 code for hypertension?")
print(reply1.text) # "The ICD-10 code is I10."
# Follow-ups on the same context remember prior turns
reply2 = await ctx.send_text("What about type 2 diabetes?")
print(reply2.text) # "The ICD-10 code is E11.9."
```
You do not need to manage context IDs yourself. Keep the context object in memory across turns. Only persist `ctx.id` if you need to resume the exact same thread after a process restart, and use `handle.getContext(id)` to do so.
### One-shot helper
No context object needed for single-shot invocations:
```ts title="JavaScript" theme={null}
const reply = await handle.run("What is the ICD-10 code for asthma?");
```
```python title="Python" theme={null}
reply = await agent.run("What is the ICD-10 code for asthma?")
```
### Resuming a thread across sessions
If you need to continue a thread after a process restart, persist `ctx.id` (only available after the first turn) and use `handle.getContext(id)` / `agent.get_context(id)` next time.
```ts title="JavaScript" theme={null}
// Session 1
const ctx = handle.createContext();
await ctx.sendText("Hello");
const savedId = ctx.id!; // persist this
// Session 2
const ctx2 = handle.getContext(savedId);
await ctx2.sendText("What did I say last time?");
```
```python title="Python" theme={null}
# Session 1
ctx = agent.create_context()
await ctx.send_text("Hello")
saved_id = ctx.id # persist this
# Session 2
ctx2 = agent.get_context(saved_id)
await ctx2.send_text("What did I say last time?")
```
This is the only valid way to pass a context ID. Do not construct `AgentContext` yourself or pass IDs to `createContext()`.
### sendText vs sendMessage vs streamMessage
All three live on `AgentContext` and send to the same thread (the `contextId` is shared). The only differences are the input shape and whether the response is buffered or streamed.
| Method | Input | Output | Use when |
| ------------------------------------------------ | --------------------------- | ----------------------------- | ------------------------------------------------- |
| `sendText(text)` / `send_text(text)` | `string` | `Promise` | You only need to send plain text |
| `sendMessage(parts)` / `send_message(parts)` | `Part[]` (text, data, file) | `Promise` | You need to attach data, files, or mix part kinds |
| `streamMessage(parts)` / `stream_message(parts)` | `Part[]` | `AsyncGenerator` | You want incremental tokens as they arrive |
`sendText` is a convenience wrapper for `sendMessage`:
```ts title="JavaScript" theme={null}
ctx.sendText("hello");
// is equivalent to
ctx.sendMessage([{ text: "hello" }]);
```
```python title="Python" theme={null}
await ctx.send_text("hello")
# is equivalent to
await ctx.send_message([{"text": "hello"}])
```
When to reach for `sendMessage` over `sendText`:
```ts title="JavaScript" theme={null}
// Mix text + structured data in one turn
await ctx.sendMessage([
{ text: "Use the attached patient record:" },
{ data: { patientId: "abc", age: 62 } },
]);
// Send a file
await ctx.sendMessage([
{ file: { name: "scan.pdf", mimeType: "application/pdf", uri: "https://..." } },
]);
```
```python title="Python" theme={null}
# Mix text + structured data in one turn
await ctx.send_message([
{"text": "Use the attached patient record:"},
{"data": {"patientId": "abc", "age": 62}},
])
# Send a file
await ctx.send_message([
{"file": {"name": "scan.pdf", "mimeType": "application/pdf", "uri": "https://..."}}
])
```
`streamMessage` returns events incrementally; `sendMessage` waits for `completed` or `failed` and returns the final aggregate. Streaming has no buffered `MessageResponse`: assemble the text yourself by concatenating `event.message.parts`.
## Connectors
Connector factories build typed connector definitions. You declare connectors at agent creation time in the `connectors` array. The agent can call them autonomously when its prompt suggests doing so.
```ts title="JavaScript" theme={null}
import { connectors } from "@corti/agent-sdk";
connectors.registry("@corti/medical-coding");
connectors.mcp({ name: "my-mcp", url: "https://mcp.example.com" });
connectors.agent("");
connectors.a2a("https://remote-agent.example.com");
connectors.schema({ name: "structured-output", schema: { type: "object" } });
```
```python title="Python" theme={null}
from corti_agent_sdk import connectors
connectors.registry(name="@corti/medical-coding")
connectors.mcp(mcp_url="https://mcp.example.com")
connectors.from_agent(agent_id="")
connectors.a2a(a2a_url="https://remote-agent.example.com")
```
| Factory | Type | Required fields | Description |
| ----------------------------------------- | ---------- | ---------------------- | ----------------------------------------------------------------- |
| `registry(name)` | `registry` | `name` | Use a published Corti registry connector |
| `mcp({ name, url })` / `mcp(mcp_url=...)` | `mcp` | `url` / `mcp_url` | Attach an MCP server |
| `agent(agentId)` / `from_agent(agent_id)` | `agent` | `agentId` / `agent_id` | Wire another Corti agent in as a sub-agent |
| `a2a(url)` / `a2a(a2a_url)` | `a2a` | `url` / `a2a_url` | Connect to a remote A2A agent |
| `schema({ name, schema })` | `schema` | `name`, `schema` | Define a JSON Schema tool for structured output (TypeScript only) |
The `schema` connector type is new in v2 and currently available in the TypeScript package only. The Python package will add it in the v2 rebuild.
### Registry connectors
Typical registry connectors include:
| Name | What it does |
| ------------------- | -------------------------------------------------------------- |
| `web-search-expert` | Live web search, good for recent guidelines and drug approvals |
| `coding-expert` | ICD-10 and medical coding, translates clinical terms to codes |
| `pubmed-expert` | PubMed literature search, finds relevant clinical studies |
To see what connectors are available in your tenant, browse the [registry connector reference](/agentic/registry/overview).
### Building a clinical orchestrator
Use multiple connectors together to build a well-rounded clinical orchestrator:
```ts title="JavaScript" theme={null}
import { CortiClient, AgentHandle, connectors } from "@corti/agent-sdk";
// Replace these with your values
const TENANT = "";
const TOKEN = "";
const client = new CortiClient({ token: TOKEN, tenant: TENANT });
// A small worker agent
const symptomExtractor = await client.agents.create({
name: "symptom-extractor",
description: "Extracts symptoms from a clinical note.",
systemPrompt:
"You are a symptom extractor. Reply with ONLY a comma-separated list of symptoms. Never ask for clarification.",
});
const extractorHandle = new AgentHandle(symptomExtractor, client);
// An orchestrator that wires sub-agent + registry connectors
const orchestrator = await client.agents.create({
name: "triage-orchestrator",
description: "Triages a clinical note.",
systemPrompt:
"Pass the note to symptom-extractor, then write a one-paragraph triage recommendation.",
connectors: [
connectors.agent(extractorHandle.id),
connectors.registry("web-search-expert"),
connectors.registry("coding-expert"),
],
});
const orchestratorHandle = new AgentHandle(orchestrator, client);
// Orchestrators fan out: raise the timeout
const reply = await orchestratorHandle.run(
"62yo with sudden severe headache, photophobia, neck stiffness.",
{ timeoutInSeconds: 180 },
);
console.log(reply.text);
```
```python title="Python" theme={null}
import asyncio
from corti_agent_sdk import CortiClient, AgentsClient, connectors
# Replace these with your values
CLIENT_ID = ""
CLIENT_SECRET = ""
ENVIRONMENT = ""
TENANT = ""
async def main():
async with CortiClient(
tenant_name=TENANT,
environment=ENVIRONMENT,
auth={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
) as client:
agents = AgentsClient(client)
symptom_extractor = await agents.create(
name="symptom-extractor",
description="Extracts symptoms from a clinical note.",
system_prompt="You are a symptom extractor. Reply with ONLY a comma-separated list of symptoms.",
)
orchestrator = await agents.create(
name="triage-orchestrator",
description="Triages a clinical note.",
system_prompt="Pass the note to symptom-extractor, then write a one-paragraph triage recommendation.",
connectors=[
connectors.from_agent(agent_id=symptom_extractor.id),
connectors.registry(name="web-search-expert"),
connectors.registry(name="coding-expert"),
],
)
# Orchestrators fan out: raise the timeout
reply = await orchestrator.run(
"62yo with sudden severe headache, photophobia, neck stiffness.",
timeout_in_seconds=180,
)
print(reply.text)
asyncio.run(main())
```
## MessageResponse
Every non-streaming call returns a `MessageResponse` that promotes the fields you most often need to the top level.
| Member | Type | Description |
| -------------------------- | --------------------- | --------------------------------------------------------------------------------------- |
| `text` | `string \| null` | All text parts from the agent reply, joined |
| `status` | `string` | Task state: `completed`, `failed`, `working`, `submitted`, `canceled`, `input-required` |
| `artifacts` | `Artifact[]` | Structured outputs the agent attached, deduplicated |
| `contextId` / `context_id` | `string \| undefined` | The thread ID, set after the first turn |
| `taskId` / `task_id` | `string \| undefined` | The task ID for this invocation |
| `raw` | `object` | The full, unmodified API response |
## Streaming
Streaming responses arrive as an async generator of `StreamEvent` objects. Each event is one of `task`, `message`, `statusUpdate`, or `artifactUpdate`.
```ts title="JavaScript" theme={null}
for await (const event of ctx.streamMessage([{ text: "Explain photosynthesis." }])) {
// Status transitions: "submitted" -> "working" -> "completed" | "failed"
if (event.statusUpdate) {
console.log(`[${event.statusUpdate.status.state}]`);
}
// Incremental message parts
if (event.message) {
for (const part of event.message.parts) {
if (part.text) process.stdout.write(part.text);
}
}
// event.artifactUpdate: emitted when the agent attaches structured outputs
}
```
```python title="Python" theme={null}
async for event in ctx.stream_message([{"text": "Explain photosynthesis."}]):
if "statusUpdate" in event:
print(f"[{event['statusUpdate']['status']['state']}]")
if "message" in event:
for part in event["message"]["parts"]:
if "text" in part:
print(part["text"], end="")
```
`streamMessage` tracks the same `contextId` as `sendMessage`. You can freely mix streaming and non-streaming calls on a single context.
## Connector auth
In the TypeScript v2 SDK, connector auth is configured on the connector itself at agent creation time, not forwarded as credentials at call time. Use the `auth` factory to specify the auth type:
```ts title="JavaScript" theme={null}
import { connectors, auth } from "@corti/agent-sdk";
// Replace these with your values
const MCP_URL = "https://mcp.example.com";
connectors.mcp({
name: "my-mcp",
url: MCP_URL,
auth: auth.bearer(),
});
connectors.mcp({
name: "secure-mcp",
url: MCP_URL,
auth: auth.apiKey(""),
});
connectors.mcp({
name: "oauth-mcp",
url: MCP_URL,
auth: auth.oauth2({ scope: "read", redirectUrl: "https://app.example.com/callback" }),
});
```
The Python SDK (still on v1 architecture) uses a `CredentialStore` passed to `create_context()`. The credential key must match the connector's `name`:
```python title="Python" theme={null}
# Replace these with your values
MCP_TOKEN = ""
MCP_URL = "https://mcp.example.com"
agent = await agents.create(
name="auth-demo",
description="Calls an auth-protected MCP server.",
connectors=[connectors.mcp(mcp_url=MCP_URL, name="my-mcp", auth_type="bearer")],
)
ctx = agent.create_context(credentials={
"my-mcp": {"type": "token", "token": MCP_TOKEN},
})
reply = await ctx.send_text("List the tools you have access to.")
print(reply.status) # "completed"
```
When an MCP connector requires auth, the agent may reply with status `auth-required` until you provide credentials. The Python SDK forwards credentials transparently and re-sends them if the agent asks again.
# Agent SDK overview
Source: https://docs.corti.ai/agentic/agent-sdk/overview
Get started with the Corti Agent SDK for TypeScript and Python, in alpha v2 private preview.
The Corti Agent SDK is in **alpha v2 private preview**. The API surface may change between releases, and the Python package is still being rebuilt for v2 parity. Do not use it in production without a dedicated support arrangement. Contact [help@corti.ai](mailto:help@corti.ai) to request access.
The Corti Agent SDK (`@corti/agent-sdk` for TypeScript, `corti-agent-sdk` for Python) is a developer-friendly wrapper for building multi-agent systems on the [Agentic Framework](/agentic/overview) v2 API. The TypeScript package is the v2 reference implementation. The Python package is being rebuilt for v2 parity; until then, the two may differ in available features.
The TypeScript package is built on [openapi-ts](https://openapi-ts.dev/) with `openapi-fetch` as its only runtime dependency. The Python package uses [httpx](https://www.python-httpx.org/) with async/await.
## What it provides
* **Agent lifecycle**: create, list, get, update, and delete agents via typed resource clients
* **A2A messaging**: send messages, stream responses, and manage conversation contexts with automatic context ID tracking
* **Connector factories**: build `registry`, `mcp`, `agent`, `a2a`, and `schema` (TypeScript only) connectors with typed factory helpers
* **Multi-agent composition**: deterministic `workflow` pipelines, `parallel` fan-out, and `stateGraph` routing with cycles and typed shared state
* **Structured errors**: typed error classes (`CortiError`, `ManagementError`, `HttpError`, `A2AError`) for both management and A2A planes
* **Extended resources**: contexts, registry, usage, feedback, agent cards, and models
## Prerequisites
* A Corti API tenant with credentials
* **TypeScript**: Node.js 18 or later (uses native `fetch` and `AsyncIterable`)
* **Python**: Python 3.9 or later
The TypeScript package is ESM-only. For CommonJS projects, use dynamic `import()` or set your project to ESM.
## Install
```bash title="JavaScript" theme={null}
npm install @corti/agent-sdk
```
```bash title="Python" theme={null}
pip install corti-agent-sdk
```
## Quick example
```ts title="JavaScript" theme={null}
import { CortiClient, AgentHandle, connectors } from "@corti/agent-sdk";
// Replace these with your values
const REGISTRY_NAME = "@corti/medical-coding";
const TENANT = "";
const TOKEN = "";
const client = new CortiClient({ token: TOKEN, tenant: TENANT });
const agent = await client.agents.create({
name: "my-agent",
description: "Handles medical coding queries",
connectors: [connectors.registry(REGISTRY_NAME)],
});
const handle = new AgentHandle(agent, client);
const ctx = handle.createContext();
const reply = await ctx.sendText("ICD-10 code for hypertension?");
console.log(reply.text); // "The ICD-10 code is I10."
```
```python title="Python" theme={null}
import asyncio
from corti_agent_sdk import CortiClient, AgentsClient, connectors
# Replace these with your values
CLIENT_ID = ""
CLIENT_SECRET = ""
ENVIRONMENT = ""
REGISTRY_NAME = "@corti/medical-coding"
TENANT = ""
async def main():
async with CortiClient(
tenant_name=TENANT,
environment=ENVIRONMENT,
auth={"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET},
) as client:
agents = AgentsClient(client)
agent = await agents.create(
name="my-agent",
description="Handles medical coding queries",
connectors=[connectors.registry(name=REGISTRY_NAME)],
)
ctx = agent.create_context()
reply = await ctx.send_text("ICD-10 code for hypertension?")
print(reply.text) # "The ICD-10 code is I10."
asyncio.run(main())
```
## Decision guide
| You want to... | Use |
| -------------------------------------------------------------- | ------------------------------------ |
| Send one message and get one reply | `agent.run(text)` or `ctx.sendText` |
| Hold a multi-turn conversation | `agent.createContext()` + `sendText` |
| Resume a thread from a previous process or session | `agent.getContext(contextId)` |
| Stream tokens as the agent writes them | `ctx.streamMessage([...])` |
| Run a fixed pipeline of agents (A to B to C) | `workflow([...])` |
| Run agents concurrently on the same input | `parallel([...])` |
| Conditionally branch, loop, or share typed state across agents | `stateGraph()` |
| Attach an MCP server, sub-agent, or registry connector | `connectors.*` in `create({...})` |
**Rule of thumb:** start with `workflow`. Only reach for `stateGraph` when you need cycles or branching that depends on accumulated state.
## Next steps
Learn about the client, agents, contexts, connectors, message responses, and streaming.Build workflows, fan out in parallel, and route with state graphs.Full method and type reference for both languages.Avoid the most frequent mistakes when using the SDK.
# Create Agent
Source: https://docs.corti.ai/agentic/agents/create-agent
/agentic/auto-generated-openapi.yml post /agents
This endpoint allows the creation of a new agent that can be utilized in the `POST /agents/{id}/v1/message:send` endpoint.
# Delete Agent by ID
Source: https://docs.corti.ai/agentic/agents/delete-agent-by-id
/agentic/auto-generated-openapi.yml delete /agents/{id}
This endpoint deletes an agent by its identifier. Once deleted, the agent can no longer be used in threads.
# Delete Context by ID
Source: https://docs.corti.ai/agentic/agents/delete-context-by-id
/agentic/auto-generated-openapi.yml delete /agents/{id}/v1/contexts/{contextId}
This endpoint deletes a context (thread) and scrubs all associated data including messages, memories, and memory chunks for the given agent. Thread and task metadata is soft-deleted for audit purposes, while content columns are irreversibly overwritten.
# Get Agent by ID
Source: https://docs.corti.ai/agentic/agents/get-agent-by-id
/agentic/auto-generated-openapi.yml get /agents/{id}
This endpoint retrieves an agent by its identifier. The agent contains information about its capabilities and the experts it can call.
# Get Agent Card
Source: https://docs.corti.ai/agentic/agents/get-agent-card
/agentic/auto-generated-openapi.yml get /agents/{id}/agent-card.json
This endpoint retrieves the agent card in JSON format, which provides metadata about the agent, including its name, description, and the experts it can call.
# Get Context by ID
Source: https://docs.corti.ai/agentic/agents/get-context-by-id
/agentic/auto-generated-openapi.yml get /agents/{id}/v1/contexts/{contextId}
This endpoint retrieves all tasks and top-level messages associated with a specific context for the given agent.
# Get Task by ID
Source: https://docs.corti.ai/agentic/agents/get-task-by-id
/agentic/auto-generated-openapi.yml get /agents/{id}/v1/tasks/{taskId}
This endpoint retrieves the status and details of a specific task associated with the given agent. It provides information about the task's current state, history, and any artifacts produced during its execution.
# List Agents
Source: https://docs.corti.ai/agentic/agents/list-agents
/agentic/auto-generated-openapi.yml get /agents
This endpoint retrieves a list of all agents that can be called by the Corti Agent Framework.
# List Registry Experts
Source: https://docs.corti.ai/agentic/agents/list-registry-experts
/agentic/auto-generated-openapi.yml get /agents/registry/experts
This endpoint retrieves the experts registry, which contains information about all available experts that can be referenced when creating agents through the AgentsCreateExpertReference schema.
# Send Message to Agent
Source: https://docs.corti.ai/agentic/agents/send-message-to-agent
/agentic/auto-generated-openapi.yml post /agents/{id}/v1/message:send
This endpoint sends a message to the specified agent to start or continue a task. The agent processes the message and returns a response. If the message contains a task ID that matches an ongoing task, the agent will continue that task; otherwise, it will start a new task.
# Update Agent by ID
Source: https://docs.corti.ai/agentic/agents/update-agent-by-id
/agentic/auto-generated-openapi.yml patch /agents/{id}
This endpoint updates an existing agent. Only the fields provided in the request body will be updated; other fields will remain unchanged.
# System architecture
Source: https://docs.corti.ai/agentic/architecture
Learn how the Agentic Framework's agent runtime receives requests, routes through connectors, and returns results.
The Agentic Framework uses an agent runtime architecture to power AI solutions. Instead of a monolithic LLM, the runtime coordinates between connectors, contexts, and the reasoning layer to deliver specialized, safe, and auditable results.
## Architecture overview
The architecture consists of three core components:
* **Agent runtime**: The central reasoning layer that receives user requests, plans steps, selects and invokes [connectors](/agentic/connectors), and generates responses
* **Connectors**: Typed integrations that provide tools and data sources to the agent. Connectors replace v1's experts, MCP servers, and sub-agents with a unified model
* **[Contexts](/agentic/context-memory)**: Maintain persistent conversation state, enabling the agent to make informed decisions and ensuring continuity across messages
Together, this architecture enables complex workflows through protocol-based composition while maintaining strict data isolation and stateless reasoning.
## Request flow
When you send a message to an agent, the following flow occurs:
1. **Message received**: The A2A server receives the message via the HTTP+JSON or JSON-RPC binding
2. **Context resolution**: The server resolves or creates a [context](/agentic/context-memory) to scope the conversation
3. **Reasoning and planning**: The agent runtime analyzes the request and determines which steps to take
4. **Connector selection**: The runtime decides which connectors to call, in what order, and with what data
5. **Connector invocation**: Selected connectors are invoked to retrieve information or perform actions
6. **Response generation**: The runtime aggregates results from connectors and generates the final response
7. **Response returned**: The client receives either a `Task` (for long-running work) or a `Message` (for quick responses)
## What the agent runtime does
The agent runtime is the central intelligence layer. Its core responsibilities include:
* **Reasoning and planning**: Analyzes user requests and determines the necessary steps to complete them
* **Connector selection**: Decides which connectors to call, in what order, and with what data
* **Task decomposition**: Breaks complex requests into discrete steps that can be handled by individual connectors
* **Response generation**: Aggregates results from connectors and generates the final response
* **Context management**: Has access to the [context](/agentic/context-memory), ensuring continuity across the conversation
* **Safety enforcement**: Enforces guardrails, type validation, and policy-driven constraints to ensure safe operation in production environments
The runtime does not perform specialized work itself. Instead, it delegates to appropriate connectors and coordinates their activities to accomplish complex workflows.
## Interaction mechanisms
The A2A protocol supports two interaction patterns:
* **Request/response**: You send a message and wait for the response. For long-running tasks, you can poll the task endpoint for status and results. Use `message:send` with `configuration.returnImmediately: false` (the default) for this pattern.
* **Streaming with Server-Sent Events (SSE)**: You open an SSE stream to receive incremental updates in real time. Use `message:stream` for streaming a new message, or `tasks/{id}:subscribe` to stream updates for an existing task. The server writes event IDs but does not yet read the `Last-Event-ID` header, so resumption without gaps is not yet implemented.
See [Stream responses](/agentic/guides/stream-responses) for details on SSE streaming.
## Observability
The Agentic Framework provides two observability surfaces:
* **[Traces](/agentic/guides/export-traces)**: OpenInference-format traces for each context, showing spans for LLM calls, connector invocations, and tool usage. Useful for debugging and performance analysis.
* **[Usage](/agentic/guides/view-usage)**: Aggregated usage metrics per agent, including invocations, unique contexts, and token consumption. Useful for cost tracking and capacity planning.
## What changed from v1
In v1, the architecture was described as an "Orchestrator + Experts + Memory" triad. The orchestrator was a separate concept that delegated to specialized experts and MCP servers.
In v2, the orchestrator is the agent runtime, and experts, MCP servers, and sub-agents are all unified under the [connectors](/agentic/connectors) model. The memory concept is formalized as [contexts](/agentic/context-memory) with their own API endpoints.
| v1 concept | v2 equivalent |
| ------------ | -------------------------------- |
| Orchestrator | Agent runtime |
| Experts | `registry` connectors |
| MCP servers | `mcp` connectors |
| Sub-agents | `agent` connectors |
| Memory | Contexts (first-class resources) |
The v1 Orchestrator page is preserved in the [v1 archived documentation](/agentic/v1/orchestrator).
## Next steps
* Learn about [connectors](/agentic/connectors) and the five connector types
* Read about [contexts and memory](/agentic/context-memory)
* Understand the [A2A protocol](/agentic/a2a-protocol) and its two bindings
# Connectors
Source: https://docs.corti.ai/agentic/connectors
Learn about the unified connector model in the Agentic Framework: the five connector types, the type discriminator, connector auth, and how connectors replace v1 experts.
Connectors are the tools and data sources an agent uses to perform its work. In v2, the Agentic Framework unifies all external integrations under a single connector model. Whether you are attaching a pre-built registry connector, a remote MCP server, another agent, or a custom schema tool, you use the same `connectors` array on the agent.
## What is a connector
A connector is a typed integration attached to an agent. When the agent reasons about a user's message, it selects and invokes connectors to retrieve information, call external tools, or delegate work to other agents.
Every connector shares a common base:
| Property | Type | Description |
| --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Server-generated connector identifier (prefixed UUIDv7, e.g. `con.0192f4c8-...`) |
| `type` | string | The connector discriminator (see below) |
| `enabled` | boolean | Whether the connector is active for invocations (default: `true`). Only `schema` connectors return this field today; `mcp`, `registry`, `agent`, and `a2a` connectors omit it (treat as enabled). |
Additional properties depend on the connector `type`.
## The type discriminator
Every connector has a `type` field that determines its shape and behavior. The v2 API ships five connector types:
| Type | Description |
| ---------- | ------------------------------------------------------------------- |
| `registry` | A pre-built connector from the Corti registry (replaces v1 experts) |
| `mcp` | A remote MCP server you bring yourself |
| `agent` | Another Corti agent in the same tenant |
| `a2a` | A remote A2A agent discovered by endpoint URL |
| `schema` | A custom tool defined by a JSON Schema |
The `type` field is immutable: you cannot change a connector's type after creation. To switch types, remove the connector and attach a new one.
## Connector types
### Registry connectors
Registry connectors are pre-built integrations maintained by Corti and partners. They are the simplest to attach: you reference the connector by its namespaced `name`.
```json theme={null}
{
"type": "registry",
"name": "coding-expert",
"config": {
"codingSystem": "icd-10"
}
}
```
| Property | Required | Description |
| -------- | -------- | ----------------------------------------------------------------------- |
| `type` | yes | Always `registry` |
| `name` | yes | Registry connector name (e.g. `coding-expert`) |
| `config` | no | Connector-specific configuration, validated against the registry schema |
The `config` field is accepted on create but not yet persisted by the server — it is silently dropped. Configuration will be honored once the feature is fully implemented.
Browse available registry connectors via the [registry API](/agentic/guides/use-registry) or the [registry connector catalog](/agentic/registry/overview).
### MCP connectors
MCP connectors connect to remote Model Context Protocol servers you operate yourself. You provide the server URL and optional authentication.
```json theme={null}
{
"type": "mcp",
"name": "policybot",
"url": "https://mcp.example.com",
"auth": {
"type": "bearer"
}
}
```
| Property | Required | Description |
| -------- | -------- | -------------------------------------------------------------------- |
| `type` | yes | Always `mcp` |
| `name` | yes | Display name for the MCP connector |
| `url` | yes | MCP server endpoint URL |
| `auth` | no | Authentication configuration (see [Connector auth](#connector-auth)) |
The MCP server must implement the Model Context Protocol. See [MCP protocol](/agentic/v1/mcp-protocol) in the v1 archived docs for protocol details.
### Agent connectors
Agent connectors delegate to another Corti agent in the same tenant. This enables multi-agent composition: one agent can call another as a tool.
```json theme={null}
{
"type": "agent",
"agentId": "agt.0192f4c8-2c5a-7b3e-9f1a-3c8d6e2b7a40"
}
```
| Property | Required | Description |
| --------- | -------- | --------------------- |
| `type` | yes | Always `agent` |
| `agentId` | yes | The target agent's ID |
### A2A connectors
A2A connectors connect to remote A2A agents outside your Corti tenant. You provide the agent's A2A endpoint URL (typically a `.well-known/agent-card.json` location).
```json theme={null}
{
"type": "a2a",
"name": "external-research-agent",
"url": "https://marginalia.polycode.co.uk/.well-known/agent-card.json"
}
```
| Property | Required | Description |
| -------- | -------- | ------------------------------------------ |
| `type` | yes | Always `a2a` |
| `url` | yes | Remote agent A2A endpoint URL |
| `name` | no | Optional display name for the remote agent |
### Schema connectors
Schema connectors define a custom tool by providing a JSON Schema. The LLM uses the schema's `name` and `description` to decide when to call the tool, and the schema validates the tool's output shape.
```json theme={null}
{
"type": "schema",
"name": "submit_code",
"description": "Submit the final ICD-10 code with a confidence score.",
"transition": "complete",
"schema": {
"type": "object",
"properties": {
"code": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 }
},
"required": ["code"]
}
}
```
| Property | Required | Description |
| ------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `type` | yes | Always `schema` |
| `name` | yes | Tool name the LLM calls |
| `schema` | yes | JSON Schema defining the tool's output shape |
| `description` | no | What the tool does; read by the LLM to decide when to call it |
| `transition` | no | If set to `complete` or `input_required`, calling this tool terminates the agent loop in that state |
| `enabled` | no | Whether the connector is active (default: `true`) |
The `transition` field lets you build structured-output agents. When set to `complete`, the agent stops reasoning after the tool is called, validates the output against the schema, and returns the result. No further LLM call is made.
## Connector auth
Connectors that call external services can authenticate using the `auth` field. Only MCP connectors support `auth` configuration; A2A connectors automatically forward the caller's bearer token. The `ConnectorAuth` schema supports three types:
| Type | Description |
| -------- | ------------------------------------------------------------- |
| `none` | No authentication (default if `auth` is omitted) |
| `bearer` | Bearer token authentication |
| `oauth2` | OAuth2 authentication with optional `scope` and `redirectUrl` |
For `bearer` and `oauth2`, credentials are provided at call time via the `authorizationData` mechanism.
```json theme={null}
{
"type": "oauth2",
"scope": "read:policies",
"redirectUrl": "https://app.corti.ai/oauth/callback"
}
```
See [Configure connector authentication](/agentic/guides/connector-auth) for a detailed guide.
## Managing connectors
You can manage connectors in two ways:
1. **Via the agent's `connectors` array**: When you create or PATCH an agent, you provide the full `connectors` array. PATCH replaces the entire array wholesale.
2. **Via the connector sub-resource**: You can attach, update, remove, or list individual connectors without rewriting the full agent. The sub-resource endpoints use JSON Merge Patch for updates.
| Operation | Method and path |
| ------------------ | -------------------------------------------------------------- |
| List connectors | `GET /v2/agentic/agents/{agentId}/connectors` |
| Attach a connector | `POST /v2/agentic/agents/{agentId}/connectors` |
| Get a connector | `GET /v2/agentic/agents/{agentId}/connectors/{connectorId}` |
| Update a connector | `PATCH /v2/agentic/agents/{agentId}/connectors/{connectorId}` |
| Remove a connector | `DELETE /v2/agentic/agents/{agentId}/connectors/{connectorId}` |
The connector PATCH (update) endpoint is in private preview and returns HTTP 501. To change a connector, remove it and attach a new one. The other sub-resource operations (list, attach, get, remove) are fully functional.
See [Manage connectors](/agentic/guides/manage-connectors) for a detailed guide.
## What replaced v1 experts
In v1, the Agentic Framework used three separate concepts: Experts (pre-built specialized agents), MCP servers (remote tool servers), and sub-agents (other Corti agents). v2 unifies all three under the connector model:
| v1 concept | v2 connector type |
| ---------- | --------------------------------------------- |
| Expert | `registry` connector |
| MCP server | `mcp` connector |
| Sub-agent | `agent` connector |
| (new) | `a2a` connector (remote A2A agents) |
| (new) | `schema` connector (custom JSON Schema tools) |
If you are migrating from v1, see the [migration guide](/agentic/guides/migrate-v1-to-v2) for a detailed mapping.
## Relationship to the registry
The registry is a catalog of pre-built connectors maintained by Corti and partners. Registry connectors are listed and discoverable via the [registry API](/agentic/guides/use-registry). Each registry entry includes:
* A stable `id` (e.g. `coding-expert`)
* A `type` indicating what connector kind it provisions
* A `configSchema` describing accepted configuration
* Capabilities, tags, and documentation links
When you attach a `registry` connector to an agent, you reference it by its `name` (which matches the registry entry's `id`). The platform provisions the connector and validates any `config` you provide against the registry's `configSchema`.
## Next steps
* Follow the [quickstart](/agentic/quickstart) to create an agent with connectors
* Learn how to [manage connectors](/agentic/guides/manage-connectors) via the API
* Browse the [registry connector catalog](/agentic/registry/overview)
* Read about [connector authentication](/agentic/guides/connector-auth)
# Context and memory
Source: https://docs.corti.ai/agentic/context-memory
Learn how contexts work in the Agentic Framework: first-class resources with their own endpoints, task grouping, data isolation, and trace export.
A **context** in the Agentic Framework makes use of memory from previous text and data in the conversation so far. Think of it as a thread that maintains conversation history. Understanding how context works is essential for building effective integrations that maintain continuity across multiple messages.
## What is a context
A context (identified by a server-generated `contextId`) is a logical grouping of related messages, tasks, and artifacts, providing context across a multi-turn conversation. It enables you to associate multiple tasks with a single patient encounter, call, or workflow, ensuring continuity and proper scoping of shared knowledge.
The `contextId` is **always created on the server**. You never generate it client-side. This ensures proper state management and prevents conflicts.
In v2, contexts are first-class resources with their own API endpoints. You can inspect and delete contexts independently of agents. Contexts are created implicitly when you send a message; there is no create or update endpoint.
Context listing (`GET /v2/agentic/contexts`) is in private preview — use `GET /v2/agentic/contexts/{contextId}` to retrieve a specific context.
## Context API endpoints
| Operation | Method and path |
| ----------------------- | ----------------------------------------------------- |
| List contexts | `GET /v2/agentic/contexts` |
| Get a context | `GET /v2/agentic/contexts/{contextId}` |
| Delete a context | `DELETE /v2/agentic/contexts/{contextId}` |
| Export traces | `GET /v2/agentic/contexts/{contextId}/trace` |
| List tasks in a context | `GET /v2/agentic/contexts/{contextId}/tasks` |
| Get a task in a context | `GET /v2/agentic/contexts/{contextId}/tasks/{taskId}` |
### Listing contexts
The `GET /v2/agentic/contexts` listing endpoint is in private preview — it returns an empty list. Use `GET /v2/agentic/contexts/{contextId}` to retrieve a specific context.
`GET /v2/agentic/contexts` returns a paginated list of contexts. You can filter by:
* `agentId`: Restrict to contexts owned by a specific agent
* `from`: Inclusive lower bound on `createdAt` (RFC 3339)
* `to`: Exclusive upper bound on `createdAt` (RFC 3339)
### Getting a context
`GET /v2/agentic/contexts/{contextId}` returns the context's metadata together with its tasks, oldest first. Each task carries its full message `history`; the user's prompt for a task is the `ROLE_USER` message within that task's history. Use the `historyLength` query parameter to cap the number of history messages returned per task.
### Deleting a context
`DELETE /v2/agentic/contexts/{contextId}` deletes the context and its associated data. This is irreversible.
See [Work with contexts](/agentic/guides/work-with-contexts) for a detailed guide.
## Data isolation and scoping
Contexts provide strict data isolation. Data can **never** leak across contexts. Each `contextId` creates a completely isolated conversation scope. Messages, tasks, artifacts, and any data within one context are completely inaccessible to agents working in a different context. This ensures:
* **Privacy and security**: Patient data from one encounter cannot accidentally be exposed to another encounter
* **Data integrity**: Information from different workflows remains properly separated
* **Compliance**: You can confidently scope sensitive data to specific contexts without risk of cross-contamination
When you need to share information across contexts, you must explicitly pass it via data parts in your messages. There is no automatic data sharing between contexts.
## Using context for automatic memory management
The simplest way to use context is to let the framework automatically manage conversation memory:
1. **First message**: Send your message without a `contextId`. The server creates a new context automatically.
2. **Response**: The server's response includes the newly created `contextId` in the task or message object.
3. **Subsequent messages**: Include that `contextId` in your requests. Memory from previous messages in that context is automatically managed and available to the agent.
When you include a `contextId` in your request, the agent has access to all previous messages, artifacts, and state within **that specific context only**. Data from other contexts is completely isolated and inaccessible.
### Standalone requests
If you don't want automatic memory management, always send messages without a `contextId`. Each message is treated as a standalone request without access to prior conversation history. This is useful for one-off queries, testing, and scenarios where you want explicit control over what context is included.
## Passing additional context with each request
In addition to automatic memory management via `contextId`, you can pass additional context in each request by including data parts in your message. This is useful when you want to provide specific structured data, summaries, or other context that should be considered for that particular request.
```json theme={null}
{
"message": {
"role": "ROLE_USER",
"parts": [
{
"text": "Generate a summary of this patient encounter"
},
{
"data": {
"patientId": "pat_12345",
"encounterDate": "2026-05-19",
"chiefComplaint": "Chest pain",
"vitalSigns": {
"bloodPressure": "120/80",
"heartRate": 72,
"temperature": 98.6
}
}
}
]
}
}
```
This approach allows you to:
* Provide structured data (patient records, clinical facts, etc.) alongside text
* Include summaries or distilled information from external sources
* Pass metadata or configuration that should be considered for this specific request
* Combine automatic memory (via `contextId`) with explicit context (via data parts)
## How memory works
The Agentic Framework uses an intelligent memory system that automatically indexes content within a context. Semantic retrieval requires a memory connector to be attached to the agent.
Automatic semantic retrieval (just-in-time prompt injection) is in private preview. The framework automatically indexes all text and data parts, but retrieval requires a memory connector and an explicit `search_memory` tool call by the LLM — it is not injected automatically.
### Automatic indexing
Every text part and data part you send in messages is automatically indexed and stored in the context's memory. This includes text content from user and agent messages, structured data from data part objects, artifacts generated by tasks, and any other content that flows through the context.
### Semantic retrieval
The memory system operates like a RAG (Retrieval Augmented Generation) pipeline. When a memory connector is attached to the agent and the agent decides to call the `search_memory` tool:
1. **Semantic search**: The tool performs semantic search across all indexed content in the context's memory
2. **Relevant retrieval**: It retrieves the most semantically relevant information based on the current query or task
3. **Agent integration**: The retrieved content is returned to the LLM as tool output, making it available for reasoning
The agent must choose to call the `search_memory` tool; the framework does not automatically inject retrieved context into prompts.
## Context vs. reference task IDs
The framework provides two mechanisms for linking related work:
* **`contextId`**: Groups multiple related messages, tasks, and artifacts together. This provides automatic memory management and is sufficient for most use cases.
* **`referenceTaskIds`**: An optional list of specific past task IDs within the same context that should be treated as explicit inputs or background. These are scoped to a context; they reference tasks within the same `contextId`.
In most situations, you can ignore `referenceTaskIds` since the automatic memory provided by `contextId` is sufficient. Use `referenceTaskIds` only when you need to explicitly direct the agent to pay attention to specific tasks or artifacts within the context.
## Context TTL and expiration
The `expiresAt` field is in private preview — not yet persisted by the server. Contexts persist until explicitly deleted.
Contexts can have an expiration time (`expiresAt`). When a context expires, it is automatically cleaned up. A `null` `expiresAt` means the context does not expire.
Ephemeral agents (with `lifecycle: "ephemeral"`) may have shorter context TTLs. Persistent agents (with `lifecycle: "persistent"`) typically have longer-lived contexts. Check the `expiresAt` field on context responses to understand the lifetime of a specific context.
## Trace export
You can export OpenInference-format traces for a context to inspect the agent's reasoning, connector calls, and tool usage:
```
GET /v2/agentic/contexts/{contextId}/trace
```
Traces contain spans for LLM calls, connector invocations, and tool usage, with attributes like `llm.token_count.total`, `tool.name`, and `input.value`. This is useful for debugging, performance analysis, and compliance auditing.
See [Export traces](/agentic/guides/export-traces) for a detailed guide.
## Context and interaction IDs
If you are using contexts alongside Corti's internal interaction representation (for example, when integrating with other Corti products that use `interactionId`), note that these two concepts are currently not linked:
* `contextId` (from the Agentic Framework) and `interactionId` (from Corti's internal systems) are separate concepts that you need to map yourself in your application
* There is no automatic association between a Corti `interactionId` and an Agentic Framework `contextId`
**Recommended approach**: Use a fresh context per interaction. Store the mapping between your `interactionId` and `contextId`(s) in your own application state. If you need to share data across multiple contexts within the same interaction, explicitly pass it via data part objects.
## Next steps
* Learn how to [work with contexts](/agentic/guides/work-with-contexts) via the API
* Read about [exporting traces](/agentic/guides/export-traces) for observability
* Understand [core concepts](/agentic/core-concepts) for the full vocabulary
# Core concepts
Source: https://docs.corti.ai/agentic/core-concepts
Learn the fundamental building blocks of the Agentic Framework: agents, connectors, tasks, messages, parts, artifacts, and contexts.
This page adds Corti-specific detail on top of the core A2A concepts. For the canonical definition of these concepts, see the A2A documentation on [Core Concepts and Components](https://a2a-protocol.org/latest/topics/key-concepts).
The Agentic Framework uses a set of core concepts that define how agents, connectors, and external systems interact. Understanding these building blocks is essential for developing on the Corti platform and for integrating your own systems using the A2A protocol.
## Core actors
At Corti, these actors map to concrete products and integrations:
* **User**: A clinician, contact-center agent, knowledge worker, or an automated service in your environment. The user initiates a request that requires assistance from one or more Corti-powered agents.
* **A2A Client (Client Agent)**: The application that calls Corti. This is your application or server. The client initiates communication using the A2A protocol and orchestrates how results are used in your product.
* **A2A Server (Remote Agent)**: A Corti agent that exposes an HTTP endpoint implementing the A2A protocol. It receives requests from clients, processes tasks, and returns results or status updates.
## Prefixed UUIDs
All resource identifiers in v2 use type-prefixed UUIDv7 format. The prefix indicates the resource type, making IDs self-documenting and easy to distinguish in logs and traces:
| Prefix | Resource | Example |
| ------- | ---------------- | ------------------------------------------- |
| `agt.` | Agent | `agt.0192f4c8-2c5a-7b3e-9f1a-3c8d6e2b7a40` |
| `con.` | Connector | `con.0192f4c8-7baf-7083-a46f-81d2bd70cf95` |
| `ctx.` | Context | `ctx.0192f4c8-3d6b-7c4f-a02b-4d9e7f3c8b51` |
| `task.` | Task | `task.0192f4c8-4e7c-7d50-b13c-5eaf8a4d9c62` |
| `msg.` | Message | `msg.0192f4c8-5f8d-7e61-924d-6fb09b5ead73` |
| `art.` | Artifact | `art.0192f4c8-6a9e-7f72-a35e-70c1ac6fbe84` |
| `fb.` | Feedback | `fb.0192f4c8-7e2a-7b3c-9d4e-5f6a7b8c9d01` |
| `usr.` | User / principal | `usr.0192f4c8-8bc0-7194-8570-92e3ce81d0a6` |
On input, you can send either a prefixed or bare UUIDv7. The server always returns prefixed IDs in responses.
## Fundamental communication elements
The following elements are fundamental to A2A communication and how Corti uses them:
A JSON metadata document describing an agent's identity, capabilities, endpoint, skills, and supported protocol bindings. Served at the standard `.well-known/agent-card.json` location without authentication.
**Key purpose**: Enables discovery and understanding of how to call an agent securely and effectively. See [Agent cards](/agentic/agent-cards).
A stateful unit of work initiated by a message, with a unique ID and defined lifecycle. Tasks have states (submitted, working, completed, failed, canceled, input-required, auth-required, rejected), a history of messages, and can produce artifacts.
**Key purpose**: Powers long-running operations in Corti (for example, document generation or multi-step workflows) and enables tracking and collaboration. See [Task lifecycle](/agentic/task-lifecycle).
A single turn of communication between a client and an agent, containing content and a role (`ROLE_USER` or `ROLE_AGENT`). Messages have a `messageId` (prefixed UUIDv7), ordered `parts`, optional `referenceTaskIds`, and `metadata`.
**Key purpose**: Carries instructions, clinical context, user questions, and agent responses between your application and agents.
The fundamental content container used within messages and artifacts. A part has `text`, `file`, or `data` properties. Unlike v1, v2 parts do not use a `kind` discriminator.
**Key purpose**: Lets Corti exchange text, structured JSON, and files in a consistent way across agents and tools.
A named output generated by an agent during a task (for example, a document, coding result, or structured data). An artifact has an `artifactId`, an optional `name`, and one or more parts.
**Key purpose**: Represents concrete results such as SOAP notes, call summaries, coding suggestions, or other structured outputs.
A server-generated identifier (`contextId`) that logically groups related tasks, messages, and artifacts, providing context across a series of interactions.
**Key purpose**: Enables you to associate multiple tasks with a single patient encounter, call, or workflow, ensuring continuity and proper scoping of shared knowledge. See [Context and memory](/agentic/context-memory).
A typed integration attached to an agent that provides tools and data sources. Connectors replace v1's experts, MCP servers, and sub-agents with a unified model.
**Key purpose**: Lets agents retrieve information, call external tools, and delegate work. See [Connectors](/agentic/connectors).
## Agent metadata
Agents in v2 carry first-class metadata that controls visibility, lifecycle, model selection, and organization:
| Field | Type | Default | Description |
| -------------- | ------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `description` | string | none | Human-readable description of the agent |
| `systemPrompt` | string | none | System prompt that guides the agent's reasoning and behavior |
| `visibility` | enum | `private` | Who can use the agent: `private` (creator only), `unlisted` (usable by ID, hidden from lists), `public` (listed tenant-wide) |
| `lifecycle` | enum | `ephemeral` | How long the agent persists: `ephemeral` (short-lived, expired automatically) or `persistent` (retained until explicitly deleted) |
| `model` | string | tenant default | Model identifier for the agent |
| `labels` | object | none | Free-form `string` to `string` metadata for filtering and organization (max 64 keys) |
The `visibility` and `labels` fields are in private preview — accepted by the API but not yet persisted. All agents are returned as `visibility: "private"` and `labels` are never returned.The `model` field is supported in the v2 API. Model precedence over connector-level models is being finalized. See the [migration guide](/agentic/guides/migrate-v1-to-v2) if you are migrating from v1 where model was configured per-expert.
## Messages and parts
A message represents a single turn of communication between a client and an agent. It includes:
* `messageId`: A prefixed UUIDv7 (e.g. `msg.0192f4c8-...`)
* `role`: Either `ROLE_USER` (sent by the client) or `ROLE_AGENT` (sent by the agent)
* `parts`: An ordered list of content parts
* `referenceTaskIds`: Optional list of task IDs this message references
* `extensions`: Optional URIs of A2A extensions that contributed to this message
* `metadata`: Free-form metadata, including Corti's `$timestamp` (RFC 3339) for timing
### Part types
A part is the fundamental content container. In v2, parts use property-based discrimination (no `kind` field):
* **Text part**: Contains plain text in the `text` property
* **File part**: Contains a file in the `file` property with `name`, `mimeType`, `uri`, or inline `bytes` (base64)
* **Data part**: Contains structured JSON in the `data` property
```json theme={null}
{
"text": "Generate a summary of this patient encounter"
}
```
```json theme={null}
{
"data": {
"patientId": "pat_12345",
"encounterDate": "2026-05-19",
"chiefComplaint": "Chest pain"
}
}
```
## Artifacts
An artifact represents a tangible output or concrete result generated by an agent during task processing. Unlike general messages, artifacts are the actual deliverables. An artifact has:
* `artifactId`: A prefixed UUIDv7 (e.g. `art.0192f4c8-...`)
* `name`: An optional human-readable name
* `parts`: Content parts containing the artifact data
In Corti, artifacts typically correspond to business outputs such as clinical notes, extracted facts, coding suggestions, or generated documents.
## Agent response: Task or message
The agent response can be a new `Task` (when the agent needs to perform a long-running operation) or a `Message` (when the agent can respond immediately):
* For quick operations (for example, a short completion or a classification), the agent responds with a `Message`
* For longer workflows (for example, generating a full clinical document, coordinating multiple connectors, or waiting on downstream systems), the agent responds with a `Task` that you can monitor and retrieve artifacts from
See [Task lifecycle](/agentic/task-lifecycle) for details on task states and streaming.
## Next steps
* Read about [connectors](/agentic/connectors), the unified integration model
* Learn about [agent cards](/agentic/agent-cards) for A2A discovery
* Follow the [quickstart](/agentic/quickstart) to create your first agent
# FAQ
Source: https://docs.corti.ai/agentic/faq
Frequently asked questions about the Agentic Framework.
Common questions and answers to help you get the most out of the Agentic Framework and the underlying A2A-based APIs.
The **agent runtime** is the central reasoning layer of the Agentic Framework. It receives user requests, reasons about what needs to be done, and delegates work to connectors. The runtime does not perform specialized work itself. Instead, it plans, selects appropriate connectors, and coordinates their activities to accomplish complex workflows.
A **connector** is a typed integration that provides tools and data sources to the agent. Connectors replace v1's experts, MCP servers, and sub-agents with a unified model. The runtime composes complex workflows by chaining multiple connectors together.
In summary: the agent runtime coordinates and delegates; connectors execute specialized work.
For more details, see [Architecture](/agentic/architecture) and [Connectors](/agentic/connectors).
**A2A (Agent-to-Agent)** is the protocol your application uses to communicate with Corti agents. It handles agent-to-agent communication: sending messages, receiving tasks, streaming responses, and managing the agent lifecycle. A2A is the conversational surface.
**MCP (Model Context Protocol)** is one of five connector types in v2. When you attach an MCP connector to an agent, the agent can call the MCP server's tools at runtime. MCP handles agent-to-tool interactions.
In the Agentic Framework: A2A is how clients talk to agents; MCP (as a connector type) is how agents talk to external tool servers. The two protocols complement each other and serve different layers.
For more information, see [A2A protocol](/agentic/a2a-protocol) and [Connectors](/agentic/connectors).
The agent response can be a new `Task` (for long-running work) or a `Message` (for quick responses). A Task represents a stateful unit of work with a unique ID and defined lifecycle.
Tasks are used for:
* Long-running operations (for example, generating a full clinical document)
* Multi-step workflows that coordinate multiple connectors
* Operations that may need to wait on downstream systems
* Any work that benefits from tracking and monitoring
Messages (with immediate responses) are used for very quick operations like simple classifications or completions that can be resolved immediately without asynchronous processing.
For more details, see [Task lifecycle](/agentic/task-lifecycle) and [Core concepts](/agentic/core-concepts).
Use **text parts** for messages that will be directly available to the agent runtime and the LLM. Text part content is immediately available for reasoning and response generation.
Use **data parts** for structured JSON data that will be stored in memory and accessed through semantic retrieval. Data part content is automatically indexed and stored in the context's memory, enabling retrieval when needed. Data parts are useful for structured data like patient records, clinical facts, workflow parameters, or EHR identifiers.
You can combine both in a single message: use a text part for the primary instruction or question, and a data part to provide structured context.
For more details, see [Core concepts](/agentic/core-concepts) and [Context and memory](/agentic/context-memory).
Both `Message` and `Artifact` use the same underlying part primitives, but they serve different roles:
* **Message** (with `role: "ROLE_AGENT"`): Represents a single turn of communication from the agent. Best for conversational output, intermediate reasoning, clarifications, or status updates.
* **Artifact**: Represents a tangible, durable output of a task (for example, a SOAP note, coding suggestions, or a generated document). Has its own `artifactId`, name, and parts.
A useful mental model: **Messages are how agents talk; Artifacts are what they produce.**
For more details, see [Task lifecycle](/agentic/task-lifecycle).
The Agentic Framework provides automatic memory management through contexts. The `contextId` is always created on the server. Send your first message without a `contextId`, and the server returns one in the response. Include that `contextId` in subsequent messages to maintain conversation history automatically.
You can also pass additional context in each request using data parts to include structured data alongside the automatic memory.
For comprehensive guidance, see [Context and memory](/agentic/context-memory).
No. Contexts provide strict data isolation. Data can never leak across contexts. Each `contextId` creates a completely isolated conversation scope where messages, tasks, artifacts, and any data within one context are completely inaccessible to agents working in a different context.
If you need to share information across contexts, you must explicitly pass it via data parts in your messages.
For more details, see [Context and memory](/agentic/context-memory).
The agent runtime analyzes incoming requests and uses reasoning to determine which connectors are needed. It considers the nature of the request, the available connectors, and their capabilities.
You can influence connector selection by writing system prompts in the agent configuration. System prompts guide how the runtime reasons about task decomposition and connector selection. The runtime can compose multiple connectors, calling them in sequence or parallel as needed.
For more information, see [Architecture](/agentic/architecture) and [Connectors](/agentic/connectors).
Agents have three visibility levels:
* **`private`** (default): Only the creator can use the agent.
* **`unlisted`**: Usable by anyone who knows the agent ID, but hidden from list results.
* **`public`**: Listed tenant-wide, visible to all users in the tenant.
For more details, see [Create an agent](/agentic/guides/create-agent).
* **`ephemeral`** (default): Short-lived agents that expire automatically. Good for one-off tasks and testing.
* **`persistent`**: Retained until explicitly deleted. Good for production agents.
For more details, see [Create an agent](/agentic/guides/create-agent).
v2 uses JSON Merge Patch (RFC 7386) for agent and connector updates. Omitted fields are unchanged (not removed), `null` clears a field, and the `connectors` array is replaced wholesale.
For example, to rename an agent and clear its system prompt:
```json theme={null}
{
"name": "coder-v2",
"systemPrompt": null
}
```
This leaves all other fields unchanged. See [Create an agent](/agentic/guides/create-agent) for details.
All resource identifiers in v2 use type-prefixed UUIDv7 format. The prefix indicates the resource type, making IDs self-documenting in logs and traces:
* `agt.` for agents
* `con.` for connectors
* `ctx.` for contexts
* `task.` for tasks
* `msg.` for messages
* `art.` for artifacts
For more details, see [Core concepts](/agentic/core-concepts).
Key changes in v2 include:
* Experts, MCP servers, and sub-agents are unified under the connector model
* API paths move from `/agents` to `/v2/agentic/agents`
* A2A protocol is upgraded to v1.0 (v0.3 not carried forward to v2, still available on v1)
* Agents gain `visibility`, `lifecycle`, `model`, and `labels` metadata
* Contexts, connectors, registry, usage, feedback, traces, and agent cards all have dedicated API endpoints
* All IDs use prefixed UUIDv7 format
* PATCH uses JSON Merge Patch semantics
See the [v1-to-v2 migration guide](/agentic/guides/migrate-v1-to-v2) for a full mapping.
# Configure connector authentication
Source: https://docs.corti.ai/agentic/guides/connector-auth
Learn how to configure authentication for outbound connectors using the ConnectorAuth schema.
This guide shows you how to configure authentication for connectors that call external services (MCP connectors only). You will learn about the three auth types and how to configure them at connector creation time.
## Prerequisites
* An existing agent (see [Create an agent](/agentic/guides/create-agent))
* Understanding of [connectors](/agentic/connectors) and [connector management](/agentic/guides/manage-connectors)
## Auth types
The `ConnectorAuth` schema supports three authentication mechanisms:
| Type | Description |
| -------- | ------------------------------------------------------------- |
| `none` | No authentication (default if `auth` is omitted) |
| `bearer` | Bearer token authentication |
| `oauth2` | OAuth2 authentication with optional `scope` and `redirectUrl` |
## Configuring auth on connector create
### Bearer auth
```bash theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"type": "mcp",
"name": "policybot",
"url": "https://mcp.example.com",
"auth": {"type": "bearer"}
}'
```
### OAuth2 auth
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"type": "mcp",
"name": "ehr-connector",
"url": "https://mcp.example.com",
"auth": {
"type": "oauth2",
"scope": "read:policies",
"redirectUrl": "https://app.corti.ai/oauth/callback"
}
}'
```
## v1 migration
In v1, MCP connector authentication used an `authorizationType` field. In v2, this is replaced by `ConnectorAuth.type`:
| v1 `authorizationType` | v2 `ConnectorAuth.type` |
| ---------------------- | ----------------------------------------------- |
| `none` | `none` (or omit `auth`) |
| `bearer` | `bearer` |
| `inherit` | `none` (caller's token forwarded automatically) |
| `oauth2.0` | `oauth2` |
See the [migration guide](/agentic/guides/migrate-v1-to-v2) for the full v1-to-v2 mapping.
## Next steps
* Learn how to [manage connectors](/agentic/guides/manage-connectors) via the API
* Read about [connectors](/agentic/connectors) for the full type reference
* Browse [registry connectors](/agentic/guides/use-registry) for pre-built options
# Create and configure an agent
Source: https://docs.corti.ai/agentic/guides/create-agent
Learn how to create, list, get, patch, and delete agents using the v2 Agentic API.
This guide shows you how to create and configure an agent with connectors, metadata, and labels. You will learn the full agent CRUD lifecycle using the v2 Agentic API.
## Prerequisites
* An access token or client credentials
* Your tenant name
* Basic understanding of [connectors](/agentic/connectors) and [core concepts](/agentic/core-concepts)
## Create a minimal agent
The simplest agent has just a name. All other fields are optional:
```bash theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{ "name": "my-agent" }'
```
The response includes the agent's `id` (a prefixed UUIDv7), `visibility` (default: `private`), `lifecycle` (default: `ephemeral`), and an empty `connectors` array.
## Create an agent with connectors
To make an agent useful, attach connectors at creation time:
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"name": "coder",
"description": "Returns ICD-10 codes for a clinical encounter.",
"systemPrompt": "Respond with only the ICD-10 code.",
"model": "corti-default",
"visibility": "private",
"lifecycle": "persistent",
"connectors": [
{"type": "registry", "name": "coding-expert"},
{"type": "mcp", "name": "policybot", "url": "https://mcp.example.com"},
{
"type": "schema",
"name": "submit_code",
"description": "Submit the final ICD-10 code with a confidence score.",
"transition": "complete",
"schema": {
"type": "object",
"properties": {
"code": {"type": "string"},
"confidence": {"type": "number", "minimum": 0, "maximum": 1}
},
"required": ["code"]
}
}
],
"labels": {"team": "coding", "env": "prod"}
}'
```
The `visibility` and `labels` fields are in private preview — accepted by the API but not yet persisted. All agents are returned as `visibility: "private"` and `labels` are silently dropped.The `model` field is supported in the v2 API. In the current implementation, a model is configured per connector rather than per agent; the precedence of an agent-level `model` over connector-level models is being finalized.
## List agents
Filter agents by visibility, lifecycle, labels, or free-text search:
The `visibility`, `lifecycle`, `label`, and `q` listing filters are in private preview — accepted by the API but silently ignored. Only `pageSize` and `pageToken` are functional.
```bash theme={null}
# All public agents
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents?visibility=public" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
# Persistent agents with label team=coding
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents?lifecycle=persistent&label=team%3Dcoding" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
# Free-text search
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents?q=coder" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Get a single agent
```bash theme={null}
AGENT_ID=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Patch an agent
Use JSON Merge Patch to update an agent. Omitted fields are unchanged; `null` clears; `connectors` is replaced wholesale:
```bash theme={null}
curl -X PATCH "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/merge-patch+json" \
-d '{
"name": "coder-v2",
"systemPrompt": "Respond with only the SNOMED CT code.",
"connectors": [
{"type": "registry", "name": "coding-expert"}
]
}'
```
The `connectors` array in a PATCH replaces the entire list. To modify individual connectors without replacing the whole array, use the [connector sub-resource endpoints](/agentic/guides/manage-connectors).The server currently honors only `name`, `description`, `systemPrompt`, `model`, and `connectors` in PATCH requests. The `visibility`, `lifecycle`, and `labels` fields are accepted but not yet persisted.
## Delete an agent
```bash theme={null}
curl -X DELETE "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
Deleting a persistent agent is irreversible. Ephemeral agents expire automatically.
## Common patterns
### Ephemeral vs. persistent
* **Ephemeral** (`lifecycle: "ephemeral"`): Short-lived agents that expire automatically. Good for one-off tasks and testing.
* **Persistent** (`lifecycle: "persistent"`): Retained until explicitly deleted. Good for production agents.
### Private vs. public
* **Private** (`visibility: "private"`): Only the creator can use the agent.
* **Unlisted** (`visibility: "unlisted"`): Usable by anyone who knows the ID, but hidden from list results.
* **Public** (`visibility: "public"`): Listed tenant-wide.
## Next steps
* Learn how to [send a message](/agentic/guides/send-message) to your agent
* Learn how to [manage connectors](/agentic/guides/manage-connectors) individually
* Read about [connector auth](/agentic/guides/connector-auth) for outbound connectors
# Export OpenInference traces
Source: https://docs.corti.ai/agentic/guides/export-traces
Learn how to export OpenInference traces for a context for observability and debugging.
This guide shows you how to export OpenInference-format traces for a context. Traces provide detailed observability into the agent's reasoning, connector calls, and tool usage.
## Prerequisites
* An existing context with at least one task (see [Send a message](/agentic/guides/send-message))
* An access token or client credentials
## What are OpenInference traces
OpenInference is an open standard for tracing LLM applications. Each trace contains spans that represent individual operations: LLM calls, connector invocations, tool usage, and more. Each span has attributes like `llm.token_count.total`, `tool.name`, and `input.value`.
## Export traces
```bash theme={null}
# Replace these with your values
CONTEXT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/trace" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
### Pagination
The trace endpoint supports pagination with `pageSize` (default: 50, max: 200) and `pageToken` query parameters. The response includes `nextPageToken` (opaque cursor for the next page, or `null` if there are no more pages) and `totalSize` (not currently populated by the server):
```bash theme={null}
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/trace?pageSize=100&pageToken=${PAGE_TOKEN}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Trace structure
The response contains a `traces` array, ordered newest first. Each trace has:
| Field | Description |
| ------------------ | --------------------------------------------- |
| `trace.id` | Trace identifier (OTel trace ID, 32-char hex) |
| `trace.name` | Human-readable trace name |
| `trace.start_time` | When the trace started |
| `trace.thread_id` | Context ID associated with the trace |
| `spans` | Array of OpenInference spans in this trace |
Each span has:
| Field | Description |
| ---------------- | ----------------------------------------------------------- |
| `name` | Human-readable span name |
| `span_id` | Unique span identifier |
| `parent_span_id` | Parent span ID (omitted for root span) |
| `start_time` | When the span started |
| `attributes` | Span attributes (e.g. `llm.token_count.total`, `tool.name`) |
## Using traces for debugging
Traces help you understand:
* **Which connectors were called**: Look for spans with `tool.name` attributes
* **How many tokens were consumed**: Look for `llm.token_count.total` attributes
* **What inputs were sent to connectors**: Look for `input.value` attributes
* **How long each step took**: Compare `start_time` across spans
* **The reasoning chain**: Follow `parent_span_id` to reconstruct the call tree
## Next steps
* Learn how to [work with contexts](/agentic/guides/work-with-contexts) via the API
* Read about [viewing usage](/agentic/guides/view-usage) for aggregated metrics
* Understand the [architecture](/agentic/architecture) for how traces fit into the system
# Manage connectors
Source: https://docs.corti.ai/agentic/guides/manage-connectors
Learn how to list, attach, update, and remove connectors using the connector sub-resource endpoints.
This guide shows you how to manage connectors attached to an agent using the connector sub-resource endpoints. You will learn about the full connector CRUD lifecycle and JSON Merge Patch semantics.
## Prerequisites
* An existing agent (see [Create an agent](/agentic/guides/create-agent))
* Understanding of [connectors](/agentic/connectors) and the five connector types
## Two ways to manage connectors
You can manage connectors in two ways:
1. **Via the agent's `connectors` array**: When you PATCH an agent, the `connectors` array is replaced wholesale. This is good for bulk changes.
2. **Via the connector sub-resource**: Attach, update, remove, or list individual connectors without rewriting the full agent. This is good for incremental changes.
This guide focuses on the sub-resource approach.
## List connectors
```bash theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
The response is a JSON object with a `connectors` array, each discriminated by `type`:
```json theme={null}
{
"connectors": [
{"type": "registry", "id": "con.0192f4c8-...", "name": "coding-expert"},
{"type": "mcp", "id": "con.0192f4c8-...", "name": "policybot", "url": "https://mcp.example.com"}
]
}
```
## Attach a connector
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"type": "registry",
"name": "coding-expert",
"config": {"codingSystem": "icd-10"}
}'
```
Each connector type has different required fields. See [Connectors](/agentic/connectors) for the full schema of each type.
### Attach an MCP connector
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"type": "mcp",
"name": "policybot",
"url": "https://mcp.example.com",
"auth": {"type": "bearer"}
}'
```
### Attach a schema connector
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"type": "schema",
"name": "submit_code",
"description": "Submit the final ICD-10 code.",
"transition": "complete",
"schema": {
"type": "object",
"properties": {"code": {"type": "string"}},
"required": ["code"]
}
}'
```
## Get a connector
```bash theme={null}
CONNECTOR_ID=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors/${CONNECTOR_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Update a connector
The connector PATCH endpoint is in private preview and returns HTTP 501. To change a connector, remove it and attach a new one.
Use JSON Merge Patch to update a connector. The `type` field is immutable:
```bash theme={null}
curl -X PATCH "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors/${CONNECTOR_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/merge-patch+json" \
-d '{
"enabled": false,
"name": "policybot-v2",
"url": "https://mcp.example.com/v2"
}'
```
| Field | Patch behavior |
| --------- | ----------------------------------------------------- |
| `enabled` | Omit to keep; set to toggle |
| `name` | Omit to keep; `null` has no effect (name is required) |
| `url` | Omit to keep; `null` clears it |
| `config` | Omit to keep; `null` clears it |
| `auth` | Omit to keep; `null` clears it |
## Remove a connector
```bash theme={null}
curl -X DELETE "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors/${CONNECTOR_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Next steps
* Read about [connector auth](/agentic/guides/connector-auth) for outbound connectors
* Browse [registry connectors](/agentic/guides/use-registry) for pre-built options
* Learn about the [connectors concept](/agentic/connectors) for the full type reference
# Migrate from v1 to v2
Source: https://docs.corti.ai/agentic/guides/migrate-v1-to-v2
Learn the path changes, experts-to-connectors mapping, PATCH semantics, and A2A protocol changes for migrating from v1 to v2.
This guide helps you migrate from the v1 Agentic Framework API to the v2 API. It covers path changes, the experts-to-connectors mapping, PATCH semantics, A2A protocol changes, and new features.
The v1 API reference remains available in the API Reference tab as "Agentic APIs v1 (deprecated)". The v1 documentation is preserved in the [v1 archived docs](/agentic/v1/overview).
## API path changes
All v2 endpoints move under `/v2/agentic/`:
| v1 path | v2 path |
| ------------------------------------ | --------------------------------------------------------- |
| `POST /agents` | `POST /v2/agentic/agents` |
| `GET /agents` | `GET /v2/agentic/agents` |
| `GET /agents/{id}` | `GET /v2/agentic/agents/{id}` |
| `PATCH /agents/{id}` | `PATCH /v2/agentic/agents/{id}` |
| `DELETE /agents/{id}` | `DELETE /v2/agentic/agents/{id}` |
| `GET /agents/{id}/agent-card.json` | `GET /v2/agentic/agents/{id}/.well-known/agent-card.json` |
| `POST /agents/{id}/v1/message:send` | `POST /v2/agentic/agents/{id}/a2a/message:send` |
| `GET /agents/{id}/v1/tasks/{taskId}` | `GET /v2/agentic/agents/{id}/a2a/tasks/{taskId}` |
| `GET /agents/registry/experts` | `GET /v2/agentic/registry/connectors` |
## Experts to connectors
In v1, the Agentic Framework used three separate concepts: Experts, MCP servers, and sub-agents. v2 unifies all three under the connector model.
| v1 concept | v2 connector type | Example |
| ---------- | ----------------- | ------------------------------------------------------------ |
| Expert | `registry` | `{"type": "registry", "name": "coding-expert"}` |
| MCP server | `mcp` | `{"type": "mcp", "name": "policybot", "url": "https://..."}` |
| Sub-agent | `agent` | `{"type": "agent", "agentId": "agt.0192f4c8-..."}` |
### v1 agent creation (with experts)
```json theme={null}
{
"name": "coder",
"experts": [{"name": "coding-expert"}]
}
```
### v2 agent creation (with connectors)
```json theme={null}
{
"name": "coder",
"connectors": [
{"type": "registry", "name": "coding-expert"}
]
}
```
## PATCH semantics
v1 already used partial-update PATCH semantics: omitted fields were unchanged, and arrays (`experts`, `mcpServers`) were replaced wholesale when provided. v2 adopts **JSON Merge Patch** (RFC 7386), which adds the ability to explicitly clear fields with `null`:
* **Omitted fields**: Unchanged (same as v1)
* **`null`**: Clears the field (new in v2 — in v1, `null` decoded to a nil Go pointer and was treated as "unchanged")
* **`connectors` array**: Replaced wholesale (same as v1's `experts` array)
```json theme={null}
{
"name": "coder-v2",
"systemPrompt": null,
"connectors": [
{"type": "registry", "name": "coding-expert"}
]
}
```
This PATCH renames the agent, clears the system prompt, and replaces the entire connectors array with a single registry connector. All other fields remain unchanged.
The `Content-Type` header for PATCH requests is `application/merge-patch+json` in v2.
## A2A v0.3 to v1.0
v2 supports A2A v1.0 only. v0.3 is not carried forward to v2 (still available on the v1 API surface). Key changes:
| v1 (A2A v0.3) | v2 (A2A v1.0) |
| ------------------------------------- | ------------------------------------------------------------------------ |
| `role: "user"` | `role: "ROLE_USER"` |
| `kind: "message"` on message envelope | Removed (no `kind` field) |
| `kind: "text"` on parts | Removed (use `text` property directly) |
| `kind: "data"` on parts | Removed (use `data` property directly) |
| No `A2A-Version` header | `A2A-Version: 1.0` header on A2A endpoints (optional, defaults to `1.0`) |
| Single binding (HTTP+JSON) | Two bindings: JSON-RPC and HTTP+JSON |
### v1 message format
```json theme={null}
{
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "Hello"}],
"messageId": "msg-001",
"kind": "message"
}
}
```
### v2 message format
```json theme={null}
{
"message": {
"role": "ROLE_USER",
"parts": [{"text": "Hello"}],
"messageId": "msg.0192f4c8-5f8d-7e61-924d-6fb09b5ead73"
}
}
```
## Removed features
| v1 feature | v2 replacement |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `?ephemeral=true` query param on agent create | `lifecycle: "ephemeral"` in request body |
| `experts` array on agent create/patch | `connectors` array with `type: "registry"` |
| `authorizationType` on MCP connectors | `auth.type` in `ConnectorAuth` object |
| v1 agent card path (`/agent-card.json`) | `.well-known/agent-card.json` |
| `agentType` field (expert, orchestrator, interviewing-expert) | Removed — all agents use the same model; specialization comes from connectors and system prompts |
| `transportType` on MCP connectors (stdio/streamable\_http/sse) | Removed — MCP connectors use streamable HTTP |
| Inline expert creation (`type: "new"` with system prompt and MCP servers) | Removed — use `registry` connectors referencing pre-built experts |
| `pushNotificationConfig` on message send | Removed (future scope) |
| `kind: "task"` and `kind: "file"` on parts | Removed (use `text`, `file`, or `data` properties directly) |
| `description` required on agent create | `description` is now optional (only `name` is required) |
| Artifact `description`, `metadata`, `extensions` fields | Removed (artifacts now have `artifactId`, `name`, `parts` only) |
| Task states `submitted`, `working` | Renamed to `TASK_STATE_SUBMITTED`, `TASK_STATE_WORKING` (PascalCase enum) |
## New features in v2
v2 introduces several new capabilities:
* **Agent metadata**: `visibility` (private, unlisted, public), `lifecycle` (ephemeral, persistent), `model`, `labels`
* **Connector sub-resources**: Individual connector CRUD via `/agents/{id}/connectors` endpoints
* **Contexts API**: List, get, and delete contexts via `/v2/agentic/contexts` endpoints
* **Registry API**: Browse and inspect pre-built connectors via `/v2/agentic/registry/connectors`
* **Usage API**: Agent usage metrics via `/agents/{id}/usage`
* **Feedback API**: Submit, list, and delete feedback via `/contexts/{id}/tasks/{id}/feedback`
* **Trace export**: OpenInference traces via `/contexts/{id}/trace`
* **Agent cards**: Standard `.well-known/agent-card.json` location (requires authentication)
* **Streaming**: `message:stream` and `tasks:subscribe` SSE endpoints
* **Prefixed UUIDv7 IDs**: All resource IDs use type-prefixed format (e.g. `agt.`, `ctx.`, `task.`)
* **JSON-RPC binding**: Alternative to HTTP+JSON for A2A communication
## Next steps
* Read the [v2 overview](/agentic/overview) for the full v2 feature set
* Follow the [quickstart](/agentic/quickstart) to create a v2 agent
* Browse the [v1 archived documentation](/agentic/v1/overview) for reference
* Use the [v1 API reference](/api-reference) (labeled "Agentic APIs v1 (deprecated)") for v1 endpoint details
# Send a message to an agent
Source: https://docs.corti.ai/agentic/guides/send-message
Learn how to send a message using the A2A HTTP+JSON binding, handle task and message responses, and manage context.
This guide shows you how to send a message to an agent and handle the response. You will learn about blocking vs. non-blocking requests, context management, and error handling.
## Prerequisites
* An existing agent (see [Create an agent](/agentic/guides/create-agent))
* An access token or client credentials
* Your tenant name
## Send a message
Use the HTTP+JSON binding to send a message:
```bash theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:send" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "What is the ICD-10 code for asthma?" }]
}
}'
```
## Message structure
A message contains:
| Field | Required | Description |
| ------------------ | -------- | ---------------------------------------------------------- |
| `role` | yes | `ROLE_USER` or `ROLE_AGENT` |
| `parts` | yes | Ordered content parts (text, file, or data) |
| `messageId` | no | Message identifier (UUID); server generates one if omitted |
| `contextId` | no | Context ID for conversation continuity |
| `referenceTaskIds` | no | Task IDs this message references |
| `extensions` | no | URIs of A2A extensions |
| `metadata` | no | Free-form metadata |
The request body also accepts an optional `configuration` object:
| Field | Default | Description |
| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `returnImmediately` | `false` | If `true`, return as soon as the task is submitted. If `false` (default), wait until the task reaches a terminal or interrupted state. |
| `historyLength` | unset | Cap the number of history messages returned per task |
| `acceptedOutputModes` | unset | Output media types the client accepts |
## Blocking vs. non-blocking
By default, `message:send` is blocking: the server waits for the task to complete before returning. Set `configuration.returnImmediately` to `true` to return as soon as the task is submitted:
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:send" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "Generate a full clinical note for this encounter." }]
},
"configuration": {
"returnImmediately": true
}
}'
```
With `returnImmediately: true`, the response returns immediately with the task in `TASK_STATE_SUBMITTED` or `TASK_STATE_WORKING` state. Poll the task endpoint or [subscribe via SSE](/agentic/guides/stream-responses) to get updates.
## Handling the response
The response contains either a `task` or a `message` object:
* **`task`**: The agent is performing long-running work. Check `task.status.state` for the current state. When `TASK_STATE_COMPLETED`, the agent's response is in `task.history` (the last `ROLE_AGENT` message). Artifacts are in `task.artifacts`.
* **`message`**: The agent responded directly with a message. The text is in `message.parts[0].text`.
```json theme={null}
{
"task": {
"id": "task.0192f4c8-...",
"contextId": "ctx.0192f4c8-...",
"status": {
"state": "TASK_STATE_COMPLETED",
"timestamp": "2026-05-19T12:00:01Z"
},
"history": [
{"role": "ROLE_USER", "messageId": "msg.0192f4c8-...", "parts": [{"text": "What is the ICD-10 code for asthma?"}]},
{"role": "ROLE_AGENT", "messageId": "msg.0192f4c8-...", "parts": [{"text": "J45.909"}]}
]
}
}
```
## Context management
The server automatically creates a context if you don't provide a `contextId`. The response includes the `contextId` in the task object. To continue a conversation, include that `contextId` in subsequent messages:
```bash theme={null}
CONTEXT_ID="ctx.0192f4c8-..."
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:send" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "What about the code for acute exacerbation?" }],
"contextId": "'"$CONTEXT_ID"'"
}
}'
```
See [Context and memory](/agentic/context-memory) for details on how context works.
## Using data parts for structured context
Pass structured data alongside text using data parts:
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:send" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [
{ "text": "Code this encounter:" },
{ "data": {
"chiefComplaint": "Acute asthma exacerbation",
"patientAge": 45,
"vitalSigns": {"heartRate": 110, "respiratoryRate": 28}
}
}
]
}
}'
```
## Error handling
Errors follow the A2A `google.rpc.Status` error format:
```json theme={null}
{
"error": {
"code": 400,
"status": "INVALID_ARGUMENT",
"message": "Message must include at least one part.",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "INVALID_REQUEST",
"domain": "a2a-protocol.org",
"metadata": {
"timestamp": "2026-05-19T12:00:00Z"
}
}
]
}
}
```
Common error codes:
| HTTP status | `status` | Description |
| ----------- | ------------------ | --------------------------------------- |
| 400 | `INVALID_ARGUMENT` | Invalid request (malformed message) |
| 401 | `UNAUTHENTICATED` | Unauthorized (missing or invalid token) |
| 404 | `NOT_FOUND` | Agent not found |
## Next steps
* Learn how to [stream responses](/agentic/guides/stream-responses) via SSE
* Understand the [task lifecycle](/agentic/task-lifecycle) for long-running work
* Read about [contexts](/agentic/guides/work-with-contexts) for conversation management
# Stream agent responses
Source: https://docs.corti.ai/agentic/guides/stream-responses
Learn how to stream agent responses via SSE using message:stream and tasks:subscribe, including resumption with Last-Event-ID.
This guide shows you how to stream agent responses in real time using Server-Sent Events (SSE). You will learn about the two streaming endpoints, the SSE event format, and how to resume interrupted streams.
## Prerequisites
* An existing agent (see [Create an agent](/agentic/guides/create-agent))
* An access token or client credentials
* Understanding of the [task lifecycle](/agentic/task-lifecycle) and [A2A protocol](/agentic/a2a-protocol)
## When to stream
Use streaming when you need real-time updates as the agent processes a message:
* Showing incremental progress to users
* Displaying partial results as they arrive
* Monitoring long-running tasks
For simple request/response interactions, use [message:send](/agentic/guides/send-message) instead.
## Stream a new message
Use `message:stream` to send a message and receive the response as an SSE stream:
```bash theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -N -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:stream" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "Generate a clinical note for this encounter." }]
}
}'
```
The `-N` flag disables cURL's output buffering, which is necessary for receiving SSE events in real time.
## SSE event format
Each event follows the W3C SSE wire format. The server currently sends only `id` and `data` lines. The `event` and `retry` fields are declared for forward compatibility but are not sent:
```
data: {"task":{"id":"task.0192f4c8-...","contextId":"ctx.0192f4c8-...","status":{"state":"TASK_STATE_WORKING","timestamp":"2026-05-19T12:00:00Z"}}}
data: {"artifactUpdate":{"taskId":"task.0192f4c8-...","contextId":"ctx.0192f4c8-...","artifact":{"artifactId":"art.0192f4c8-...","name":"clinical-note","parts":[{"text":"SOAP Note..."}]},"lastChunk":true}}
data: {"statusUpdate":{"taskId":"task.0192f4c8-...","contextId":"ctx.0192f4c8-...","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-05-19T12:00:01Z"}}}
```
### Event types
Each SSE event carries an `A2AStreamResponse` with exactly one field:
| Field | Description |
| ---------------- | --------------------------------------------------------------------------------------- |
| `task` | The full task object, sent when the task is created or state changes |
| `message` | A direct message response (no task lifecycle) |
| `statusUpdate` | Incremental status change (no `final` flag; clients infer finality from the task state) |
| `artifactUpdate` | Incremental artifact update with `lastChunk` flag |
## Subscribe to an existing task
If you have a task ID (from a non-blocking `message:send` or after a stream dropped), use `tasks:subscribe` to stream updates:
```bash theme={null}
TASK_ID=""
curl -N -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/tasks/${TASK_ID}:subscribe" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "A2A-Version: 1.0"
```
## Resumption with Last-Event-ID
The server writes event IDs but does not yet read the `Last-Event-ID` request header, so resumption without gaps is not implemented. The section below describes the intended design for when this feature ships.
If an SSE stream drops, the intended resumption mechanism is to send the `Last-Event-ID` header with the most recent event ID you received:
```bash theme={null}
LAST_EVENT_ID="event-123"
curl -N -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:stream" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-H "Last-Event-ID: ${LAST_EVENT_ID}" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "Continue generating the note." }]
}
}'
```
The server will replay events from that point forward once the feature is implemented. This will work with both `message:stream` and `tasks:subscribe`.
## Cancel a task
Cancel an in-flight task:
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/tasks/${TASK_ID}:cancel" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "A2A-Version: 1.0" \
-H "Content-Type: application/json" \
-d '{}'
```
The task transitions to `TASK_STATE_CANCELED`.
## JSON-RPC streaming binding
You can also stream via the JSON-RPC binding by sending a `message/stream` method to the base A2A endpoint:
```bash theme={null}
curl -N -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"jsonrpc": "2.0",
"id": "msg-001",
"method": "SendStreamingMessage",
"params": {
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "Generate a clinical note." }]
}
}
}'
```
The SSE events carry JSON-RPC response envelopes instead of raw `A2AStreamResponse` objects.
## Next steps
* Read about the [task lifecycle](/agentic/task-lifecycle) for state transitions
* Learn how to [send a message](/agentic/guides/send-message) without streaming
* Understand the [A2A protocol](/agentic/a2a-protocol) bindings
# Submit feedback on tasks
Source: https://docs.corti.ai/agentic/guides/submit-feedback
Learn how to submit, list, and delete feedback for tasks and messages using the feedback API.
This guide shows you how to submit, list, and delete feedback for tasks and messages. Feedback is useful for building thumbs-up/down UIs, case review workflows, and automated evaluation pipelines.
## Prerequisites
* An existing task (see [Send a message](/agentic/guides/send-message))
* An access token or client credentials
* A context ID and task ID
## Submit feedback
```bash theme={null}
# Replace these with your values
CONTEXT_ID=""
TASK_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/tasks/${TASK_ID}/feedback" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"rating": {
"scale": "binary",
"value": 1
},
"labels": ["correct", "helpful"],
"reason": "The response accurately identified the ICD-10 code."
}'
```
## Rating scales
| Scale | Values | Description |
| -------------- | ---------------------------- | --------------------------- |
| `binary` | 0 (negative) or 1 (positive) | Available now |
| `likert5` | 1-5 | Planned (not yet available) |
| `continuous01` | 0.0-1.0 | Planned (not yet available) |
## Labels
Labels provide structured observations about the result. Positive and negative labels can be combined to represent mixed feedback:
**Positive labels**: `correct`, `complete`, `helpful`, `wellPresented`, `efficient`
**Negative labels**: `incorrect`, `missingInformation`, `irrelevant`, `misunderstoodRequest`, `unsupportedClaim`, `unsafeOrInappropriate`, `poorlyPresented`, `tooVerbose`
**Neutral**: `other` (requires a `reason`)
A maximum of five labels may be submitted per feedback entry. Duplicate labels are rejected. The `other` label requires a `reason` field.
## Targeting a specific message
By default, feedback applies to the task as a whole. To target a specific message in the task's history, include the `target` field with the message ID:
```bash theme={null}
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/tasks/${TASK_ID}/feedback" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"rating": {"scale": "binary", "value": 0},
"labels": ["incorrect"],
"reason": "The ICD-10 code was wrong for this specific response.",
"target": {"messageId": "msg.0192f4c8-5f8d-7e61-924d-6fb09b5ead73"}
}'
```
## Metadata
Include provenance and correlation information using the `metadata` field:
```json theme={null}
{
"metadata": {
"collectionMethod": "thumbs",
"clientReference": "session-abc-123",
"actor": {"externalId": "user-pseudonymous-id"}
}
}
```
| Field | Description |
| ------------------ | -------------------------------------------------------- |
| `collectionMethod` | How the feedback was collected (e.g. `thumbs`, `survey`) |
| `clientReference` | Customer-defined reference for correlation |
| `actor.externalId` | Pseudonymous identifier for the submitter |
The `actor.externalId` must not contain names, emails, national identifiers, or medical record numbers. Use a pseudonymous identifier.
## List feedback
```bash theme={null}
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/tasks/${TASK_ID}/feedback" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
The response includes all feedback entries for the task, scoped to the authenticated user via row-level security, newest first. The response is wrapped in a `feedbacks` array:
```json theme={null}
{
"feedbacks": [
{
"id": "fb.0192f4c8-...",
"taskId": "task.0192f4c8-...",
"rating": {"scale": "binary", "value": 1},
"normalizedScore": 1.0,
"labels": ["correct", "helpful"],
"reason": "The response accurately identified the ICD-10 code.",
"createdAt": "2026-05-19T12:00:00Z"
}
]
}
```
Feedback is scoped to the authenticated user. The response contains only the caller's feedback, not all feedback for the task.
## Delete feedback
Delete all feedback you submitted for a task. The DELETE endpoint targets the task as a whole — there is no per-entry feedback ID in the path:
```bash theme={null}
curl -X DELETE "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/tasks/${TASK_ID}/feedback" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
DELETE removes **all** feedback you submitted for the task, not a single entry. The operation is idempotent: deleting when there is no feedback returns `204`.
## Use cases
* **Thumbs UI**: Collect binary feedback after each task response
* **Case review**: Use labels like `correct`, `incorrect`, `missingInformation` for structured review
* **Automated evaluation**: Submit feedback programmatically with metadata for tracking
## Next steps
* Learn how to [work with contexts](/agentic/guides/work-with-contexts) to find task IDs
* Read about the [task lifecycle](/agentic/task-lifecycle) for task states
* Learn how to [export traces](/agentic/guides/export-traces) for debugging
# Browse the connector registry
Source: https://docs.corti.ai/agentic/guides/use-registry
Learn how to list and inspect pre-built registry connectors and get their configuration schemas.
This guide shows you how to browse the registry of pre-built connectors maintained by Corti and partners. You will learn how to search for connectors, inspect their details, and use them in your agents.
## Prerequisites
* An access token or client credentials
* Understanding of [connectors](/agentic/connectors)
## List registry connectors
```bash theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/registry/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
### Searching
The `q` free-text search parameter is in private preview — accepted by the API but silently ignored.
Use the `q` parameter for free-text search over connector names and descriptions:
```bash theme={null}
QUERY="coding"
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/registry/connectors?q=${QUERY}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
### Pagination
The registry list endpoint supports `pageSize` and `pageToken` query parameters. The response includes `nextPageToken` (opaque cursor for the next page) and `totalSize` (not currently populated by the server — treat as absent).
```bash theme={null}
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/registry/connectors?pageSize=20" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Get a registry connector
```bash theme={null}
CONNECTOR_ID="coding-expert"
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/registry/connectors/${CONNECTOR_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
The response includes:
The `version`, `provider`, `capabilities`, `tags`, and `documentationUrl` fields are in private preview — declared in the schema but not yet populated. Only `id`, `type`, `name`, `title`, `description`, and `configSchema` are returned.
| Field | Description |
| ------------------ | ---------------------------------------------------------------- |
| `id` | Stable, namespaced identifier (use as the `name` when attaching) |
| `type` | Connector kind this entry provisions |
| `name` | Programmatic name |
| `title` | Human-readable display name |
| `description` | Description (may contain CommonMark) |
| `version` | Latest published version |
| `provider` | Publishing organization |
| `capabilities` | Connector capabilities |
| `tags` | Keywords for search and filtering |
| `configSchema` | JSON Schema describing accepted `config` |
| `documentationUrl` | Documentation URL |
| `icons` | Icon URLs for the connector (e.g. logo) |
| `websiteUrl` | Website URL for the connector provider |
## Using a registry connector
When you find a registry connector you want to use, attach it to an agent by referencing its `id` as the `name` field:
```bash theme={null}
AGENT_ID=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/connectors" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"type": "registry",
"name": "coding-expert",
"config": {"codingSystem": "icd-10"}
}'
```
The `config` object is validated against the registry connector's `configSchema`. If your config doesn't match the schema, the request will be rejected.
## Available registry connectors
For a catalog of available registry connectors with detailed documentation, see the [Registry Connectors](/agentic/registry/overview) section.
## Next steps
* Learn how to [manage connectors](/agentic/guides/manage-connectors) on your agents
* Read about [connector auth](/agentic/guides/connector-auth) for outbound connectors
* Browse the [registry connector catalog](/agentic/registry/overview)
# View agent usage
Source: https://docs.corti.ai/agentic/guides/view-usage
Learn how to retrieve usage metrics for an agent, including granularity, date ranges, and bucket structure.
This guide shows you how to retrieve usage metrics for an agent using the v2 Agentic API. You will learn about granularity, date ranges, and how to interpret the response.
## Prerequisites
* An existing agent with at least one invocation
* An access token or client credentials
## Get usage
```bash theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/usage" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Query parameters
| Parameter | Type | Description |
| ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from` | date-time | Inclusive start of the range (UTC). Defaults to 30 days ago. |
| `to` | date-time | Exclusive end of the range (UTC). Defaults to now. |
| `granularity` | enum | Bucket size: `minute`, `hour`, `day` (default), or `week`. Only `day` is currently honored; other values are accepted but produce daily buckets (the server always returns `day`). |
```bash theme={null}
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/usage?from=2026-05-19T00:00:00Z&to=2026-05-21T00:00:00Z&granularity=day" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Response structure
The response contains range-wide `totals` and an array of `buckets`:
```json theme={null}
{
"granularity": "day",
"from": "2026-05-19T00:00:00Z",
"to": "2026-05-21T00:00:00Z",
"totals": {
"invocations": 15,
"uniqueContexts": 6
},
"buckets": [
{
"periodStart": "2026-05-19T00:00:00Z",
"periodEnd": "2026-05-20T00:00:00Z",
"invocations": 12,
"uniqueContexts": 5
},
{
"periodStart": "2026-05-20T00:00:00Z",
"periodEnd": "2026-05-21T00:00:00Z",
"invocations": 3,
"uniqueContexts": 2
}
]
}
```
## Interpreting metrics
| Metric | Description |
| ---------------- | ------------------------------------------------- |
| `invocations` | Number of agent invocations in the period |
| `uniqueContexts` | Number of distinct contexts invoked in the period |
For per-task token and credit accounting, see the `$usage` field in the [task metadata](/agentic/task-lifecycle#task-metadata-and-usage).
## Next steps
* Learn how to [export traces](/agentic/guides/export-traces) for detailed observability
* Read about the [task lifecycle](/agentic/task-lifecycle) for per-task usage data
* Learn how to [submit feedback](/agentic/guides/submit-feedback) on task results
# Work with contexts
Source: https://docs.corti.ai/agentic/guides/work-with-contexts
Learn how to list, inspect, and delete contexts, and how to retrieve tasks within a context.
This guide shows you how to manage contexts using the v2 Agentic API. Contexts are first-class resources with their own endpoints for listing, inspection, and deletion.
## Prerequisites
* An existing agent with at least one sent message (see [Send a message](/agentic/guides/send-message))
* Understanding of [context and memory](/agentic/context-memory)
## List contexts
The `GET /v2/agentic/contexts` listing endpoint is in private preview — it returns an empty list and ignores all filters. Use `GET /v2/agentic/contexts/{contextId}` to retrieve a specific context.
```bash theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
### Filtering
| Parameter | Description |
| --------- | ----------------------------------------------- |
| `agentId` | Restrict to contexts owned by a specific agent |
| `from` | Inclusive lower bound on `createdAt` (RFC 3339) |
| `to` | Exclusive upper bound on `createdAt` (RFC 3339) |
```bash theme={null}
AGENT_ID=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts?agentId=${AGENT_ID}&from=2026-05-19T00:00:00Z" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
The response is paginated with `contexts`, `nextPageToken`, and `totalSize` fields.
## Get a context
```bash theme={null}
CONTEXT_ID=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
Use the `historyLength` query parameter to cap the number of history messages returned per task:
```bash theme={null}
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}?historyLength=10" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
The response includes the context's metadata and its `tasks` array (oldest first). Each task carries its full message `history`.
```json theme={null}
{
"id": "ctx.0192f4c8-...",
"agentId": "agt.0192f4c8-...",
"taskCount": 1,
"createdAt": "2026-05-19T12:00:00Z",
"updatedAt": "2026-05-19T12:00:01Z",
"expiresAt": null,
"tasks": [
{
"id": "task.0192f4c8-...",
"contextId": "ctx.0192f4c8-...",
"status": {"state": "TASK_STATE_COMPLETED", "timestamp": "2026-05-19T12:00:01Z"},
"history": [
{"role": "ROLE_USER", "messageId": "msg-...", "parts": [{"text": "Code this encounter."}]},
{"role": "ROLE_AGENT", "messageId": "msg-...", "parts": [{"text": "J45.901"}]}
]
}
]
}
```
## Delete a context
```bash theme={null}
curl -X DELETE "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
Deleting a context is irreversible. All associated tasks, messages, and artifacts are permanently removed.
## List tasks in a context
```bash theme={null}
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/tasks" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Get a specific task in a context
```bash theme={null}
TASK_ID=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/tasks/${TASK_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Get an artifact
Retrieve a specific artifact produced by a task:
```bash theme={null}
ARTIFACT_ID=""
curl "https://api.${ENVIRONMENT}.corti.app/v2/agentic/contexts/${CONTEXT_ID}/tasks/${TASK_ID}/artifacts/${ARTIFACT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}"
```
## Next steps
* Learn how to [export traces](/agentic/guides/export-traces) for observability
* Read about [context and memory](/agentic/context-memory) concepts
* Learn how to [submit feedback](/agentic/guides/submit-feedback) on task results
# Overview of the Corti Agentic Framework
Source: https://docs.corti.ai/agentic/overview
Learn what the Agentic Framework is, what problems it solves, and what you can build with it.
The Corti Agentic Framework is a modular AI system for building advanced agents that perform complex tasks without months of complex architecture work. An LLM agent is not a chatbot answering everything from internal knowledge. The language model is used for reasoning and planning: understanding a request, breaking it down, and deciding which tools and data sources are best suited to handle each part.
The Agentic Framework supports use cases across the industry spectrum, from chat-based assistants to automating data entry and powering decision support workflows.
## What problems it solves
Modern LLMs are powerful, but on their own they are insufficient and unsafe for production use. The Agentic Framework addresses two fundamental gaps:
### LLMs do not have reliable access to domain data
LLMs cannot be trusted to rely on internal knowledge alone. In production, responses must be grounded in validated reference sources, real-time system data, and customer-owned systems and APIs. Without access to these sources at runtime, models are forced to infer or guess, which is unacceptable in specialised settings.
The Agentic Framework addresses this by enabling agents to retrieve information directly from trusted external tools through [connectors](/agentic/connectors). Instead of hallucinating answers, agents look things up, verify context, and base their outputs on authoritative data.
### LLMs cannot safely act on the world
Production workflows require more than generating text. They involve interacting with real systems: querying databases, drafting and updating documentation, preparing structured outputs, and triggering downstream processes.
The framework provides a controlled execution layer that allows agents to plan actions, invoke tools, and coordinate multi-step workflows while remaining within clearly defined safety boundaries. Where necessary, agents can pause execution, request human approval, and resume only once explicit consent is given.
## What you can build with it
Using the Agentic Framework, teams can build:
* **User-facing assistants**: documentation editing, guideline and reference lookup, coding and administrative support
* **Programmatic agent endpoints**: embedded into existing software, triggered by events, APIs, or workflows
* **Customer-embedded agents**: customers bring their own tools and systems; agents combine Corti, third-party, and customer capabilities
All of these share the same underlying agent runtime, safety model, and connector layer.
## Built for industry by design
Industry applications are not general-purpose, and the framework reflects that reality.
Typed inputs and outputs, explicit tool schemas, and guardrails around action-taking ensure safe operation in production environments.Every decision and tool call is observable with replayable traces and structured logs for transparency, compliance, and quality assurance.Fine-tuned reasoning layers optimized for industry language, workflows, and compliance needs.A unified connector model enables agents to call registry tools, MCP servers, other agents, remote A2A agents, and custom schema tools.Maintain persistent, context-aware conversations and manage multiple active contexts without losing information throughout the session.Access a library of pre-built connectors maintained by Corti and partners: specialized tools that connect to data sources and services to execute complex tasks.Plug directly into EHRs, decision support systems, and knowledge bases with minimal setup.Pass relevant context with each query, including structured data formats, enabling connectors to work with rich, domain-specific information.
## v2 at a glance
The v2 API introduces several key changes:
* **Unified connectors**: Experts, MCP servers, and sub-agents are all replaced by a single [connectors](/agentic/connectors) model with five types: `registry`, `mcp`, `agent`, `a2a`, and `schema`
* **Clean CRUD verbs**: Consistent REST patterns for agents, connectors, contexts, and feedback
* **A2A v1.0 only**: The [A2A protocol](/agentic/a2a-protocol) is upgraded to v1.0; v0.3 is deprecated but still available on the v1 API surface
* **First-class metadata**: Agents have `visibility` (private, unlisted, public), `lifecycle` (ephemeral, persistent), `model`, and `labels`
* **New endpoints**: Contexts, connectors, registry, usage, artifacts, feedback, traces, and agent cards all have dedicated API surfaces
* **Prefixed UUIDv7 IDs**: All resource IDs use type-prefixed UUIDv7 format (e.g. `agt.`, `ctx.`, `task.`, `msg.`, `con.`)
Migrating from v1? See the [v1-to-v2 migration guide](/agentic/guides/migrate-v1-to-v2) for a detailed mapping of changes.
## Who it's for
The Agentic Framework is built for teams working on industry software:
* **Software companies** embedding intelligent automation directly into their products
* **Enterprise customers** building internal, AI-powered workflows
* **Advanced engineering teams** that need flexibility, control, and strong safety guarantees without building bespoke agent infrastructure from scratch
The platform is designed to make it easy to go from demo to production-grade AI systems that operate safely in real-world environments.
## Next steps
Create your first agent and send a message in minutes.Learn the fundamental building blocks of the Agentic Framework.
# Quickstart
Source: https://docs.corti.ai/agentic/quickstart
Create your first agent, attach a connector, and send a message in minutes.
This guide walks you through creating your first agent with a registry connector, sending a message, and receiving a response. By the end, you will have a working end-to-end agent.
After completing this quickstart, you will have a working agent and know where to go next based on your use case.
## Prerequisites
* API access credentials (client ID and client secret, or an access token)
* Development environment set up
* Basic understanding of REST APIs
If you haven't set up authentication, follow the [Creating Clients](/authentication/creating_clients) and [authentication quickstart](/authentication/quickstart) guides first.
Start by creating a project in the [Corti Console](https://console.corti.app). This gives you a workspace and access to manage your clients and credentials.
Use the Agentic API to create your first agent. You need an access token (obtained using your client credentials) and your tenant name.
The example below creates a persistent agent with a registry connector (`memory-expert`) attached.
```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const ENVIRONMENT = "";
const TENANT = "";
const client = new CortiClient({
tenantName: TENANT,
environment: ENVIRONMENT,
auth: {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
},
});
const myAgent = await client.agents.create({
name: "My First Agent",
description: "A simple agent to get started with the Corti Agentic Framework",
lifecycle: "persistent",
connectors: [
{ type: "registry", name: "memory-expert" },
],
});
```
```csharp title="C# .NET" theme={null}
using Corti;
// Replace these with your values
const string CLIENT_ID = "";
const string CLIENT_SECRET = "";
const string ENVIRONMENT = "";
const string TENANT = "";
var client = new CortiClient(
tenantName: TENANT,
environment: ENVIRONMENT,
auth: CortiClientAuth.ClientCredentials(clientId: CLIENT_ID, clientSecret: CLIENT_SECRET)
);
var myAgent = await client.Agents.CreateAsync(
new AgentsCreateRequest
{
Name = "My First Agent",
Description = "A simple agent to get started with the Corti Agentic Framework",
Lifecycle = AgentsLifecycle.Persistent,
Connectors = new[]
{
new CommonRegistryConnectorCreate
{
Type = "registry",
Name = "memory-expert",
},
},
}
);
```
```python title="Python" expandable theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/agentic/agents",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"name": "My First Agent",
"description": "A simple agent to get started with the Corti Agentic Framework",
"lifecycle": "persistent",
"connectors": [
{"type": "registry", "name": "memory-expert"},
],
},
)
response.raise_for_status()
agent = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"name": "My First Agent",
"description": "A simple agent to get started with the Corti Agentic Framework",
"lifecycle": "persistent",
"connectors": [
{"type": "registry", "name": "memory-expert"}
]
}'
```
The response includes the agent's `id` (a prefixed UUIDv7 like `agt.0192f4c8-...`). Save this ID for the next step.
Send a message to your agent using the A2A HTTP+JSON binding. The response is either a `Task` (for longer work) or a `Message` (for quick responses).
```ts title="JavaScript" theme={null}
const agentResponse = await client.agents.messageSend(myAgent.id, {
message: {
role: "ROLE_USER",
parts: [{
text: "Hello there. This is my first message.",
}],
messageId: crypto.randomUUID(),
},
});
if (agentResponse.task) {
console.log(agentResponse.task.status.state);
console.log(agentResponse.task.history?.at(-1)?.parts?.[0]?.text);
} else if (agentResponse.message) {
console.log(agentResponse.message.parts[0].text);
}
```
```csharp title="C# .NET" theme={null}
var agentResponse = await client.Agents.MessageSendAsync(
myAgent.Id,
new A2ASendMessageRequest
{
Message = new CommonMessage
{
Role = CommonRole.RoleUser,
Parts = new[]
{
new CommonPart
{
Text = "Hello there. This is my first message.",
}
},
MessageId = Guid.NewGuid().ToString(),
},
}
);
if (agentResponse.Task != null)
{
Console.WriteLine(agentResponse.Task.Status.State);
}
else if (agentResponse.Message != null)
{
Console.WriteLine(agentResponse.Message.Parts[0].Text);
}
```
```python title="Python" expandable theme={null}
import uuid
import requests
# Replace these with your values
AGENT_ID = ""
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/agentic/agents/{AGENT_ID}/a2a/message:send",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
"A2A-Version": "1.0",
},
json={
"message": {
"role": "ROLE_USER",
"parts": [
{"text": "Hello there. This is my first message."}
],
"messageId": str(uuid.uuid4()),
}
},
)
response.raise_for_status()
result = response.json()
if "task" in result:
task = result["task"]
print(task["status"]["state"])
if task.get("history"):
print(task["history"][-1]["parts"][0]["text"])
elif "message" in result:
print(result["message"]["parts"][0]["text"])
```
```bash title="cURL" theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:send" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "Hello there. This is my first message." }]
}
}'
```
Check the response to confirm your agent processed the message:
* If the response contains a `task`, check `task.status.state`. A `TASK_STATE_COMPLETED` state means the task finished successfully. The agent's response is in `task.history` (the last `ROLE_AGENT` message).
* If the response contains a `message`, the agent responded directly. The text is in `message.parts[0].text`.
The response includes `task.metadata.$usage.credits`, reporting the credits used for the request. See [Tracking credit consumption](/get_started/tracking-credit-consumption) to attribute this to your own customers or features.
## Next steps
Learn the fundamental building blocks: agents, connectors, tasks, messages, and contexts.Understand the five connector types and how to attach them to agents.Go deeper into message sending: blocking vs. non-blocking, DataParts, error handling.Stream agent responses in real time via SSE.
# Clinical Trials
Source: https://docs.corti.ai/agentic/registry/clinicaltrials-gov
Search for clinical trials, study protocols, eligibility criteria, and recruitment status
```json theme={null}
{
"type": "registry",
"name": "clinical-trials-expert"
}
```
The **Clinical Trials** connector lets agents search for clinical trials, find relevant studies for specific medical conditions, and retrieve detailed protocol information including eligibility criteria and recruitment status.
Use this connector when you need to surface ongoing or completed clinical research relevant to a patient's condition or to a research workflow.
## Capabilities
The Clinical Trials connector can:
* Search clinical trial registries for studies matching a condition or intervention
* Retrieve study titles, locations, and recruitment status
* Provide eligibility criteria and protocol details
* Synthesize concise, factual summaries grounded in registry data
## Use cases
* Finding relevant clinical trials for a patient's condition
* Research and study discovery
* Reviewing protocols and eligibility criteria during clinical decision support
* Surfacing recruiting trials for referral workflows
## Detailed information
The Clinical Trials connector retrieves study information through a dedicated MCP server. It is designed to operate within a multi-agent system: it follows agent runtime instructions exactly, plans the fewest necessary tool calls, and grounds every output in the data returned by its tools rather than speculating.
Outputs are structured (Markdown headings, bullet points) so they remain both human-readable and easy for downstream agents to parse. The connector returns short, factual summaries (including study title, condition, location, recruitment status, and eligibility criteria where available) and never provides medical advice.
# DrugBank
Source: https://docs.corti.ai/agentic/registry/drugbank
Look up detailed drug information, medication profiles, and drug-drug interactions from DrugBank
```json theme={null}
{
"type": "registry",
"name": "drugbank-expert"
}
```
The **DrugBank** connector retrieves structured drug information from the DrugBank API. It is suited for product concept lookups, product details, drug profiles, and drug-drug interaction queries, and returns source-grounded results without speculation.
Use this connector when you need authoritative, identifier-grounded medication data (for example pharmacology, dosing, or interaction details) to support a prescribing or review workflow.
## Authentication
The DrugBank connector connects to the DrugBank MCP server using **bearer-token authentication**, which means **you must supply a DrugBank API key** when registering or invoking this connector. The API key is issued by DrugBank and is provided to the MCP server as a bearer token at runtime.
See [Configure connector authentication](/agentic/guides/connector-auth) for the full list of supported auth modes and how to wire credentials through to a connector. If you do not yet have a DrugBank API key, request one from DrugBank before enabling this connector.
## Capabilities
The DrugBank connector can:
* Find product concepts and retrieve product concept profiles
* Look up drug profiles and product details by name
* Retrieve detailed drug-drug interaction information
* Return identifiers and key fields preserving the original DrugBank wording when quoting
## Use cases
* Drug-drug interaction checks during medication review
* Medication information and monograph lookups
* Pharmacological research and reference workflows
* Clinical decision support for prescribing
## Detailed information
The DrugBank connector connects to DrugBank's MCP server and exposes tools such as `find_product_concepts`, `get_product_concept_profile`, `get_drug_profile`, `find_product_by_name`, `get_product_details`, and `get_drug_interaction_details`.
Operating as a connector within the Corti Agentic Framework, it follows agent runtime instructions precisely and avoids speculative medical interpretation. If DrugBank returns no results or errors, the connector reports that explicitly rather than guessing.
# Medical Calculator
Source: https://docs.corti.ai/agentic/registry/medical-calculator
Perform clinical calculations such as BMI, HbA1c, glucose conversions, and other medical formulas
```json theme={null}
{
"type": "registry",
"name": "medical-calculator-expert"
}
```
The **Medical Calculator** connector performs clinical and physiological calculations such as BMI, HbA1c conversions, glucose conversions, and related metrics. Use this connector whenever a workflow needs precise, formula-based medical computations.
The Medical Calculator returns clear, concise results suitable for direct display or for further reasoning by another connector, removing the risk of manual calculation errors.
## Capabilities
The Medical Calculator connector can:
* Compute BMI, HbA1c, eAG, glucose conversions, and other physiological metrics
* Run multiple calculations in a single request and return them as a clean list
* Skip calculations when required inputs are missing and report what was missing
* Return human-readable, plain-text results without unnecessary metadata
## Use cases
* Inline clinical metric calculation during a conversation or document generation
* Lab value conversion (for example HbA1c ↔ eAG, or glucose unit conversion)
* Quick BMI or risk-metric lookups in clinical decision support flows
* Combining several calculations into a single, clinician-friendly summary
## Output format
When one calculation is requested, the connector returns a short single line, for example:
```
BMI: 24.8 (normal range)
```
When multiple calculations are requested, the connector returns a clean list:
```
BMI: 24.8 (normal range)
HbA1c: 6.1%
eAG: 128 mg/dL
```
If a value cannot be calculated, the connector notes the reason briefly, for example:
```
HbA1c: cannot calculate (missing fructosamine)
```
## Detailed information
The Medical Calculator is a connector that does not interact with end users directly. It reads input values from the agent runtime's `data_part`, runs the requested formulas via its tools, and returns results grounded in the actual computed values: no extra commentary, no speculative medical advice.
# Medical Coding
Source: https://docs.corti.ai/agentic/registry/medical-coding
Assign medical diagnosis and procedure codes from clinical notes using AI-assisted coding
```json theme={null}
{
"type": "registry",
"name": "coding-expert"
}
```
The **Medical Coding** connector assigns diagnosis and procedure codes from clinical text using a structured, AI-assisted workflow. It can identify codes relevant to an admission even from incomplete data, and selects the most appropriate code system at runtime.
Medical Coding is essential for billing, claims, CDI, and revenue-cycle workflows where accurate, guideline-compliant codes are required.
## Capabilities
The Medical Coding connector can:
* Predict candidate codes from a full clinical note
* Search a code system from clinical text fragments
* Explore the structure of a code system to drill into candidate codes
* Retrieve coding guidelines that govern a given code or scenario
* Verify a final code sequence against the code system before returning it
## Workflows
Every code assignment follows one of the structured workflows below. `explore()`, `guidelines()`, and `verify()` are always called. They are never skipped.
* **SEARCH-FIRST** (from clinical text): `search()` → `explore()` → `guidelines()` → `verify()`
* **PREDICT-FIRST** (from a full clinical note): `predict()` → `explore()` → `guidelines()` → `verify()`
* **GUIDELINES-FIRST** (unfamiliar conventions): `guidelines()` → `search()` → `explore()` → `guidelines()` → `verify()`
* **VERIFY-AND-NARROW** (refine predictions): `predict()` → `explore()` + `guidelines()` → `verify()` → `predict(filter=include/exclude)` → `verify()`
* **EXPLORE-FIRST** (browse code system): `explore()` → `explore()` drill down → `guidelines()` → `verify()`
The connector never relies on `search()` alone. It can return overly specific codes or miss applicable coding rules. Codes already present in the agent runtime's `tool_data_part` are preferred; MCP tools are only invoked if suitable codes are absent or insufficient.
## Use cases
* Automated coding for billing and claims
* Clinical documentation improvement (CDI) and audit support
* Code validation and verification against guidelines
* Revenue-cycle management
## Detailed information
The Medical Coding connector is wired to a coding MCP server with an extended call timeout to accommodate prediction and verification workflows. The general `coding-expert` calls `list_code_systems()` first and selects from any supported code system at runtime.
For details on the supported code systems and use cases, see the [Medical Coding tab](/coding/overview).
## Code-system-specific variants
In addition to the general `coding-expert`, the registry exposes pre-configured variants that lock the connector to a single code system. Use these when you know which system you want to code in and don't need the connector to discover code systems at runtime.
| Key | Code system |
| -------------------------- | ---------------------------------------------------------------------------------------- |
| `coding-expert-icd-10-cm` | ICD-10-CM, US diagnosis coding |
| `coding-expert-icd-10-int` | ICD-10 WHO, international diagnosis coding (uses `icd-10-who` for search/explore/verify) |
| `coding-expert-icd-10-pcs` | ICD-10-PCS, US inpatient procedure coding |
| `coding-expert-icd-10-uk` | ICD-10-UK, UK diagnosis coding |
Each variant shares the same workflows and capabilities documented above.
# Memory
Source: https://docs.corti.ai/agentic/registry/memory
Recall and analyze content from large contexts and files inside a single request
```json theme={null}
{
"type": "registry",
"name": "memory-expert"
}
```
Memory is a **core toolbox connector**, it is foundational to most agent workflows where the agent runtime needs to reason over large inputs without bloating the prompt.
The **Memory** connector lets agents recall and analyze content from large contexts (long documents, attached files, transcripts, or other bulky inputs) that would not fit cleanly inside the agent runtime's working prompt. The agent runtime hands the bulky context to the Memory connector and asks targeted questions; the Memory connector searches and analyzes that content and returns the relevant facts.
The Memory connector does **not** persist information across sessions, conversations, or context boundaries. It only operates over the context that has already been provided to the agent in the current request. If you need durable, cross-session memory, that has to be implemented at the application layer.
Use the Memory connector when a request involves a large body of text or files that the agent runtime should not load into its own prompt (for example a long discharge summary, a multi-page guideline, or an attached transcript) and you only need a few targeted facts back.
## Capabilities
The Memory connector can:
* Search large bodies of context for information matching a query
* Analyze long documents or files and return targeted facts
* Surface relevant excerpts so the agent runtime can ground its synthesis
* Reduce the size of the agent runtime's working prompt by offloading bulky lookups
## Use cases
* Querying a long clinical document or attached file for specific facts
* Pulling targeted passages out of a large transcript or note
* Summarizing or extracting from inputs that are too large to keep in the agent runtime prompt
* Providing analyzed excerpts as evidence for a downstream connector
## How to invoke
Send the relevant query or high-level objective as the `text` parameter. Do not pass new raw facts to the Memory connector. Provide only the question or objective so it can search and analyze the existing in-request context itself.
For questions that can be answered directly from the agent runtime's own prompt without consulting bulky context, you can answer directly without invoking the Memory connector.
# Registry connectors
Source: https://docs.corti.ai/agentic/registry/overview
A registry of prebuilt connectors available in the Corti Agentic Framework
The Corti Agentic Framework ships with a **registry of prebuilt connectors** that the agent runtime can compose into multi-step workflows. Each connector is a discrete capability, ranging from in-context memory recall, to reference lookup, to medical coding, to structured interviewing.
Browse the list below to see what is available out of the box. You can also [bring your own connector](/agentic/connectors) by exposing an MCP server and registering it with Corti.
Want to inspect the live registry programmatically? Use the [registry connector API](/agentic/guides/use-registry) to retrieve the full list of available connectors, their descriptions, and configuration schemas.
## Core toolbox
These connectors are foundational to most agent workflows and should be available to the agent runtime by default.
Recall and analyze content from large contexts and files within a single request. Core toolbox connector, keep available across most workflows.
## Knowledge and reference
Get medication guidance including dosing, interactions, contraindications, and prescribing considerations from POSOS.
Look up detailed drug information, medication profiles, and drug-drug interactions from DrugBank.
Search PubMed for scientific articles, abstracts, and citations from biomedical literature.
Search for clinical trials, study protocols, eligibility criteria, and recruitment status.
Search the web and retrieve up-to-date information from online sources.
## Medical coding
Assign medical diagnosis and procedure codes from clinical notes using AI-assisted coding across all supported coding systems.
Assign ICD-10-CM diagnosis codes (US standard) from clinical notes.
Assign ICD-10-WHO international diagnosis codes from clinical notes.
Assign ICD-10-PCS inpatient procedure codes (US standard) from clinical notes.
Assign ICD-10-UK diagnosis codes (UK standard) from clinical notes.
## Computation and structured workflows
Perform clinical calculations such as BMI, HbA1c, glucose conversions, and other medical formulas.
Guide users through structured questionnaires and clinical interviews step by step.
Need a connector that isn't listed here? You can [register your own MCP server](/agentic/connectors) as a custom connector, or [contact us](mailto:help@corti.ai) to discuss new integrations.
# POSOS
Source: https://docs.corti.ai/agentic/registry/posos
Get medication guidance including dosing, interactions, contraindications, and prescribing considerations from POSOS
```json theme={null}
{
"type": "registry",
"name": "posos-expert"
}
```
The **POSOS** connector is a primary source of truth for medication intelligence and prescription decision support, backed by POSOS (a medication database and clinical decision platform). It should be invoked early and frequently to ground synthesis in structured, patient-contextualized medication guidance and to reduce alert fatigue by focusing on clinically relevant signals.
POSOS is not limited to direct medication questions. It can also fact-check, validate, or anchor broader reasoning that relies on pharmacology, dosing, contraindications, interactions, organ-impairment considerations, or pregnancy/lactation considerations.
## Capabilities
The POSOS connector can:
* Retrieve medication database entities and structured drug information
* Surface interaction, contraindication, and high-risk warning signals
* Provide dosing guidance and dose adjustments (for example renal or hepatic impairment)
* Flag pregnancy and lactation considerations and monitoring requirements
* Return interoperability-friendly structured outputs aligned with SNOMED/FHIR concepts where available, with traceability back to POSOS entities
## Use cases
* Prescription decision support embedded in EHR or clinician-facing workflows
* Reducing alert fatigue by prioritizing clinically relevant medication signals
* Patient-contextualized prescribing: indication, current meds, organ impairment, pregnancy/lactation
* Validating medication-related assumptions in broader clinical reasoning
## Detailed information
The POSOS connector is a clinical decision support and medication knowledge retrieval connector. It treats POSOS (its medication database and CDSS outputs) as the primary source of truth for retrieved content, prefers entity- or signal-level retrieval (drug, interaction, contraindication, dosing adjustment, alert rationale) over broad summaries, and stays concise, clinically accurate, and neutral.
Outputs reference POSOS entities and signals clearly so the agent runtime can trace the source (drug entity, interaction signal, alert category, patient-context parameters). The connector connects to POSOS via OAuth 2.0. See [Configure connector authentication](/agentic/guides/connector-auth) for details.
# PubMed
Source: https://docs.corti.ai/agentic/registry/pubmed
Search PubMed for scientific articles, abstracts, and citations from biomedical literature
```json theme={null}
{
"type": "registry",
"name": "pubmed-expert"
}
```
The **PubMed** connector retrieves PubMed citations, abstracts, and related-article results via NCBI Entrez E-utilities. It is suited for literature searches, author searches, and fetching abstracts with formatted summaries, returning source-grounded results without speculation.
PubMed is the go-to connector for finding peer-reviewed biomedical literature, clinical studies, and citations to support evidence-based reasoning.
## Capabilities
The PubMed connector can:
* Search PubMed for citations matching a query
* Retrieve abstracts and metadata for specific publications
* Find related articles for a given citation
* Look up publications by author
## Use cases
* Literature reviews and evidence retrieval
* Pulling abstracts to ground a clinical synthesis
* Finding studies relevant to a diagnosis, treatment, or intervention
* Author and publication discovery
## Detailed information
The PubMed connector connects to an MCP server that exposes tools including `search_pubmed`, `get_pubmed_abstract`, `get_related_articles`, and `find_by_author`.
It operates as a focused retrieval connector: it follows agent runtime instructions precisely, avoids speculative medical interpretation, and returns concise structured outputs grounded in tool responses. Results include identifiers (PMID/PMCID when available) and preserve the original wording from PubMed when quoting. If PubMed returns no results or errors, the connector reports that explicitly.
# Interviewing
Source: https://docs.corti.ai/agentic/registry/questionnaire-interviewing
Guide users through structured questionnaires and clinical interviews step by step
```json theme={null}
{
"type": "registry",
"name": "interviewing-expert"
}
```
The **Interviewing** connector manages questionnaire-style interviews with the user. Use this connector whenever the user is answering or progressing through a questionnaire, always pass the questionnaire `data_part` (and its ID) when available so the connector can follow the correct structure and logic.
When invoking this connector, set the `text` argument to the raw content of what the user just said or answered, with no extra commentary, prompts, or instructions.
## Capabilities
The Interviewing connector can:
* Drive structured interviews that follow a provided questionnaire definition
* Guide the conversation question-by-question and adapt based on responses
* Track answers and resolve conditional flow (`next`, `defaultNext`, `conditions`)
* Return structured `questionnaire_response` artifacts the agent runtime can consume
## Use cases
* Patient intake and history-taking
* Clinical assessments and screening questionnaires
* Research and survey data collection
* Any structured, multi-step information-gathering workflow
## Documentation
### Questionnaire
* `questionnaireId`: `string` (required)
* `version`: `string | number` (required)
* `startQuestion` : `string` (required, but will be optional soon; ID of a question)
* `questions`: `Question[]` – list of unique IDs (required)
* `title`, `description`, `meta`: optional
### Question (Discriminated by `type`)
Common fields for **all** question types:
* `id`: `string` (required, unique)
* `type`: one of the literals below (required)
* `boolean`
* `text_short`
* `text_long`
* `number`
* `date_time` (with `mode`: `date` | `time` | `datetime`)
* `scale` (with `min`, `max`, optional `step`, optional `labels[]`)
* `single_choice` (with `options[]`)
* `multi_choice` (with `options[]`, optional `maxSelections`)
* `text`: `string` (required)
* `guideline`: `string` (optional, instructs model to provide guidelines to the end user, can be used to augment question text)
* `facets`: `string` (optional, hints for the LLM on how to populate and interpret user answers)
* `required`: `boolean` (optional; if `true`, agent must collect a valid answer)
* `conditions`: `Condition[]` (optional; controls visibility/flow)
* `defaultNext`: `string` (optional; ID of the next question if no option-level `next` applies)
* `meta`: `object` (optional)
### Options (for choice questions)
* `value`: `string | number | boolean`
* `label`: `string`
* `guideline`: `string` (optional)
* `next`: `string` (optional, overrides `defaultNext` when chosen)
### Conditions
**Operators:** `=`, `!=`, `<`, `<=`, `>`, `>=`, `contains`, `not_contains`, `in`, `not_in`, `exists`, `not_exists`
* `question`: `string` – the **source** question ID to evaluate
* `operator`: as above
* `value`: required for all operators **except** `exists`/`not_exists`
## Example requests
### 1. Initial Request
User provides an incomplete or ambiguous answer (starts the flow):
```json theme={null}
{
"message": {
"role": "ROLE_USER",
"messageId": "",
"parts": [
{
"text": "Answer to the questionnaire: I'm ok satisfied"
},
{
"data": {
"type": "questionnaire",
"questionnaire": {
"questionnaireId": "sleep-survey-v1",
"version": "1.0",
"startQuestion": "question-1",
"questions": [
{
"id": "question-1",
"type": "scale",
"text": "How satisfied are you?",
"min": 1,
"max": 5
},
{
"id": "question-2",
"type": "scale",
"text": "How many hours of sleep did you have?",
"min": 0,
"max": 12
},
{
"id": "question-3",
"type": "text_short",
"text": "How well rested do you feel?"
},
{
"id": "question-4",
"type": "single_choice",
"text": "How many interruptions did you have?",
"options": [
{"value": "none", "label": "None at all"},
{"value": "few", "label": "A few"},
{"value": "lots", "label": "Lots"}
]
}
]
}
}
}
]
}
}
```
**Agent response**
```json theme={null}
{
"task": {
"id": "",
"contextId": "",
"status": {
"state": "TASK_STATE_COMPLETED",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "Thanks, how many hours of sleep did you have?"
}
],
"messageId": "", // For client use to track messages
"taskId": "",
"contextId": ""
}
},
"artifacts": [{
"artifactId": "",
"parts": [
{
"data": {
"answers": {
"question-1": 3
},
"is_completed": false,
"next_question_id": "question-2",
"questionnaire_id": "sleep-survey-v1",
"version": "1.0"
},
"metadata": {
"type": "questionnaire_response"
}
}
]
}],
"history": [
// ...
],
"metadata": {
// contains metadata related to task execution
// not relevant for purposes of input/output
}
}
}
```
### 2. Second Request
User clarifies and completes the missing information:
```json theme={null}
{
"message": {
"role": "ROLE_USER",
"messageId": "",
"contextId": "",
"parts": [
{
"text": "I had 7 hours of sleep"
}
]
}
}
```
Agent response
```json theme={null}
{
"task": {
"id": "",
"contextId": "",
"status": {
"state": "TASK_STATE_COMPLETED",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "Thanks, how many interruptions did you have while sleeping?"
}
],
"messageId": "", // For client use to track messages
"taskId": "",
"contextId": ""
}
},
"artifacts": [{
"artifactId": "",
"parts": [
{
"data": {
"answers": {
"question-1": 3,
"question-2": 7
},
"is_completed": false,
"next_question_id": "question-3",
"questionnaire_id": "sleep-survey-v1",
"version": "1.0"
},
"metadata": {
"type": "questionnaire_response"
}
}
]
}],
"history": [
// ...
],
"metadata": {
// contains metadata related to task execution
// not relevant for purposes of input/output
}
}
}
```
### 3. Final Request
User clarifies and completes the missing information:
```json theme={null}
{
"message": {
"role": "ROLE_USER",
"messageId": "",
"contextId": "",
"parts": [
{
"text": "I feel well rested and was not interrupted"
}
]
}
}
```
**Agent response (final result):**
```json theme={null}
{
"task": {
"id": "",
"contextId": "",
"status": {
"state": "TASK_STATE_COMPLETED"
},
"artifacts": [{
"artifactId": "",
"parts": [
{
"data": {
"type": "questionnaire_response",
"answers": {
"question-1": 3,
"question-2": 7,
"question-3": "Well rested",
"question-4": "none"
},
"is_completed": true,
"next_question_id": null,
"questionnaire_id": "sleep-survey-v1",
"version": "1.0"
},
"metadata": {
"type": "questionnaire_response"
}
}
]
}],
"history": [
//...
],
"metadata": {}
}
}
```
# Web Search
Source: https://docs.corti.ai/agentic/registry/web-search
Search the web and retrieve up-to-date information from online sources
```json theme={null}
{
"type": "registry",
"name": "web-search-expert"
}
```
The **Web Search** connector lets agents search the web, read specific URLs, and gather up-to-date external information. It finds relevant online sources, extracts their content, and returns concise, evidence-based summaries.
Web Search is essential when you need information that may not be available in a static knowledge base, for example recent news, fresh research, or dynamic content that changes frequently.
## Capabilities
The Web Search connector can:
* Search the web for sources relevant to a query
* Read specific URLs and extract their content
* Synthesize concise, source-grounded summaries
* Filter results to or from specific domains and tune search depth (basic vs. advanced)
## Use cases
* Surfacing recent medical research and publications
* Verifying claims against multiple online sources
* Pulling current news, policy updates, or product information
* Retrieving information not yet indexed in static reference databases
## Configuration
The Web Search connector exposes optional search configuration including `include_domains`, `exclude_domains`, `max_results`, and `search_depth` (`basic` or `advanced`). Use these to constrain results to trusted domains or to trade off latency for thoroughness.
## Detailed information
The Web Search connector is a focused research connector: it does not interact with end users directly, but instead follows structured agent runtime instructions and returns factual, well-organized findings grounded entirely in the data its tools return. Outputs use Markdown structure so downstream agents and humans can both consume them easily.
# SDKs and integrations
Source: https://docs.corti.ai/agentic/sdks-integrations
Official SDKs and community integrations for the Agentic Framework.
The Agentic Framework provides official SDKs and supports community integrations to help you build quickly.
## Official Corti SDKs
Official Agentic Framework SDK for Node and browser environments. JavaScript SDK docs →
Official Agentic Framework SDK for .NET applications. C# .NET SDK docs →
Adapter for integrating Corti A2A agents with the Vercel AI SDK. Use `useChat` and streaming patterns to build chat UIs. AI SDK Adapter docs →
## Official A2A Project SDKs
The A2A project maintains open source SDKs in multiple languages. These SDKs implement the A2A v1.0 protocol that the Agentic Framework uses:
Build A2A-compliant agents and servers in Python. a2a-python →
Official JavaScript/TypeScript SDK for A2A. a2a-js →
Build A2A-compliant agents and services in Java. a2a-java →
Implement A2A agents and servers in Go. a2a-go →
Build A2A-compatible agents in .NET ecosystems. a2a-dotnet →
## Other libraries
* **shadcn/ui component library for chatbots**: [`ai-elements` on npm](https://www.npmjs.com/package/ai-elements)
* **A2A Inspector**: [a2a-inspector on GitHub](https://github.com/a2aproject/a2a-inspector)
* **Awesome A2A**: [awesome-a2a on GitHub](https://github.com/ai-boost/awesome-a2a)
# Task lifecycle
Source: https://docs.corti.ai/agentic/task-lifecycle
Learn how A2A tasks move through states, how streaming events deliver updates, and how to resume interrupted streams.
A task is a stateful unit of work performed by an agent. When you send a message to an agent, the response is either a direct `Message` (for quick operations) or a `Task` (for longer-running work). Tasks have a defined lifecycle, can produce artifacts, and can be streamed in real time.
## What is a task
A task represents work that an agent performs in response to a message. It has:
* A unique prefixed UUIDv7 identifier (`task.0192f4c8-...`)
* A `contextId` linking it to a [context](/agentic/context-memory) for conversation continuity
* A `status` with a current state and timestamp
* A `history` array of messages exchanged during the task
* An `artifacts` array of outputs produced by the task
* `metadata` including token and credit accounting under `$usage`
The agent decides whether to respond with a `Task` or a `Message`. Quick operations (short completions, classifications) often return a `Message` directly. Longer workflows return a `Task` that you monitor for completion.
## Task states
A task moves through the following states:
| State | Description |
| --------------------------- | --------------------------------------------------------------------------------------------- |
| `TASK_STATE_SUBMITTED` | The task has been accepted by the agent but processing has not started |
| `TASK_STATE_WORKING` | The agent is actively processing the task |
| `TASK_STATE_COMPLETED` | The task finished successfully; artifacts are available |
| `TASK_STATE_FAILED` | The task failed; check the status message for error details |
| `TASK_STATE_CANCELED` | The task was canceled by the client |
| `TASK_STATE_INPUT_REQUIRED` | The agent needs additional input before it can continue |
| `TASK_STATE_AUTH_REQUIRED` | An MCP connector requires authentication; the task pauses until auth is provided |
| `TASK_STATE_REJECTED` | The task was rejected before processing (insufficient credits or invalid agent configuration) |
`TASK_STATE_REJECTED` and `TASK_STATE_AUTH_REQUIRED` are only visible on the JSON-RPC binding. On the REST (HTTP+JSON) binding, both are mapped to `TASK_STATE_FAILED` with a descriptive status message.
### Terminal vs non-terminal states
| Category | States |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Terminal** (immutable, cannot be canceled or resumed) | `TASK_STATE_COMPLETED`, `TASK_STATE_FAILED`, `TASK_STATE_CANCELED`, `TASK_STATE_REJECTED` |
| **Non-terminal** (can be resumed or canceled) | `TASK_STATE_SUBMITTED`, `TASK_STATE_WORKING`, `TASK_STATE_INPUT_REQUIRED`, `TASK_STATE_AUTH_REQUIRED` |
Canceling a task in a terminal state returns `409 Conflict`.
### State transitions
A task transitions through states as follows:
* **`TASK_STATE_SUBMITTED`** to **`TASK_STATE_WORKING`**: The agent begins processing.
* **`TASK_STATE_SUBMITTED`** to **`TASK_STATE_REJECTED`**: The task was rejected before processing (insufficient credits or invalid configuration).
* **`TASK_STATE_SUBMITTED`** to **`TASK_STATE_AUTH_REQUIRED`**: An MCP connector requires authentication before the agent can proceed.
* **`TASK_STATE_WORKING`** to **`TASK_STATE_COMPLETED`**: The task finishes successfully.
* **`TASK_STATE_WORKING`** to **`TASK_STATE_FAILED`**: An error occurred during processing.
* **`TASK_STATE_WORKING`** to **`TASK_STATE_INPUT_REQUIRED`**: The agent paused and needs user input.
* **`TASK_STATE_WORKING`** to **`TASK_STATE_AUTH_REQUIRED`**: An MCP connector encountered an auth error during execution.
* **`TASK_STATE_AUTH_REQUIRED`** to **`TASK_STATE_WORKING`**: Auth is resolved and the agent resumes.
* **`TASK_STATE_INPUT_REQUIRED`** to **`TASK_STATE_WORKING`**: The user provides input and the agent resumes.
* Any non-terminal state to **`TASK_STATE_CANCELED`**: The client cancels the task via `tasks/{id}:cancel`.
A task in `TASK_STATE_INPUT_REQUIRED` or `TASK_STATE_AUTH_REQUIRED` state is not terminated. Send a new message with the required input (for `TASK_STATE_INPUT_REQUIRED`) or resolve the connector authentication (for `TASK_STATE_AUTH_REQUIRED`) to resume processing. The SSE stream closes when either state is reached; you must send a new message or re-subscribe to continue receiving updates.
## Task history and messages
The `history` array contains all messages exchanged during the task, oldest first. Each message has:
* `messageId`: A prefixed UUIDv7 (e.g. `msg.0192f4c8-...`)
* `role`: Either `ROLE_USER` (sent by the client) or `ROLE_AGENT` (sent by the agent)
* `parts`: An ordered list of content parts (text, file, or data)
* `metadata`: Free-form metadata, including Corti's `$timestamp` for timing
You can control how much history the agent considers by setting `configuration.historyLength` on the [message send request](/agentic/guides/send-message).
## Artifacts
Artifacts are the tangible outputs a task produces. Each artifact has:
* `artifactId`: A prefixed UUIDv7 (e.g. `art.0192f4c8-...`)
* `name`: An optional human-readable name
* `parts`: Content parts containing the artifact data
Artifacts are closely tied to the task lifecycle. In production scenarios, artifacts typically correspond to business outputs like notes, coding suggestions, or extracted facts.
You can retrieve a specific artifact via:
```
GET /v2/agentic/contexts/{contextId}/tasks/{taskId}/artifacts/{artifactId}
```
## Streaming events
When you stream a message via `message:stream` or subscribe to an existing task via `tasks/{id}:subscribe`, the server sends Server-Sent Events (SSE). Each event carries an `A2AStreamResponse` with exactly one of the following fields:
| Field | Description |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `task` | The full task object, sent when the task is first created or when its state changes |
| `message` | A direct message response (no task lifecycle) |
| `statusUpdate` | An incremental status change with `taskId`, `contextId`, `status`, and `metadata` (no `final` flag; clients infer finality from the task state) |
| `artifactUpdate` | An incremental artifact update with `taskId`, `contextId`, `artifact`, and `lastChunk` |
### SSE event format
Each SSE frame follows the W3C SSE wire format:
| Field | Description |
| ------- | -------------------------------------------------------------------------------------------------- |
| `data` | The JSON-encoded payload (an `A2AStreamResponse`) |
| `event` | Event type (declared for forward compatibility; not currently sent by the server) |
| `id` | Opaque event ID (the server writes event IDs but does not yet read `Last-Event-ID` for resumption) |
| `retry` | Reconnection interval (declared for forward compatibility; not currently sent by the server) |
### Example SSE event
```json theme={null}
data: {"task":{"id":"task.0192f4c8-...","contextId":"ctx.0192f4c8-...","status":{"state":"TASK_STATE_WORKING","timestamp":"2026-05-19T12:00:00Z"}}}
data: {"artifactUpdate":{"taskId":"task.0192f4c8-...","contextId":"ctx.0192f4c8-...","artifact":{"artifactId":"art.0192f4c8-...","name":"icd10-result","parts":[{"text":"J45.909"}]},"lastChunk":true}}
data: {"statusUpdate":{"taskId":"task.0192f4c8-...","contextId":"ctx.0192f4c8-...","status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-05-19T12:00:01Z"}}}
```
## Resumption with Last-Event-ID
The server writes event IDs but does not yet read the `Last-Event-ID` request header, so resumption without gaps is not implemented. The section below describes the intended design for when this feature ships.
The intended resumption mechanism is to send the `Last-Event-ID` header with the most recent event ID you received. The server will replay events from that point forward once implemented.
This will work with both `message:stream` and `tasks/{id}:subscribe`. See [Stream responses](/agentic/guides/stream-responses) for implementation details.
## Task metadata and usage
Each task carries `metadata` with Corti's first-party keys prefixed with `$`. The `$usage` object provides token and credit accounting:
| Field | Description |
| -------------------------- | ----------------------------------------------------------------- |
| `model` | The model identifier that served the request |
| `inputTokens` | Prompt tokens consumed |
| `outputTokens` | Completion tokens produced |
| `cachedInputTokens` | Subset of `inputTokens` served from the prompt cache (a discount) |
| `cacheCreationInputTokens` | Input tokens written to the prompt cache (cache-write surcharge) |
| `totalTokens` | Total tokens billed (`inputTokens` + `outputTokens`) |
| `credits` | Corti billing credits charged for the task |
```json theme={null}
{
"$usage": {
"model": "corti-default",
"inputTokens": 100,
"outputTokens": 20,
"cachedInputTokens": 64,
"cacheCreationInputTokens": 0,
"totalTokens": 120,
"credits": 1.2
}
}
```
You can use this data for cost tracking, budget monitoring, and evaluating cache effectiveness across your agents.
## Next steps
* Learn how to [send a message](/agentic/guides/send-message) and handle task or message responses
* Learn how to [stream responses](/agentic/guides/stream-responses) via SSE
* Read about [contexts](/agentic/context-memory) to understand how tasks are grouped
# A2A Protocol (Agent-to-Agent)
Source: https://docs.corti.ai/agentic/v1/a2a-protocol
Learn about the Agent-to-Agent protocol for inter-agent communication
### What is the A2A Protocol
The **Agent-to-Agent (A2A)** protocol is an open standard that enables secure, framework-agnostic communication between autonomous AI agents. Instead of building bespoke integrations whenever you want agents to collaborate, A2A gives Corti-Agentic and other systems a **common language** agents can use to discover, talk to, and delegate work to one another.
For the full technical specification, see the official A2A project docs at [a2a-protocol.org](https://a2a-protocol.org/latest/).
The v1 API implements **A2A protocol version 0.3**, not the latest (v1.0) specification. The link above shows the latest spec, which includes features not available in the v0.3 implementation. For the v0.3-specific behavior, refer to the v1 API reference and the migration guide's [A2A v0.3 to v1.0](/agentic/guides/migrate-v1-to-v2#a2a-v03-to-v10) section.
Originally developed by Google and now stewarded under the Linux Foundation, A2A solves a core problem in multi-agent systems: interoperability across ecosystems, languages, and vendors. It lets you connect agents built in Python, JavaScript, Java, Go, .NET, or other languages and have them cooperate on complex workflows without exposing internal agent state or proprietary logic.
### Why Corti-Agentic Uses A2A
We chose A2A because it:
* **Standardizes agent communication.** Agents can talk to each other without siloed, point-to-point integrations. That makes composite workflows easier to build and maintain.
* **Supports real workflows.** A2A includes discovery, task negotiation, and streaming updates, so agents can coordinate long-running or multi-step jobs.
* **Preserves security and opacity.** Agents exchange structured messages without sharing internal memory or tools. That protects intellectual property and keeps interactions predictable.
* **Leverages open tooling.** There are open source SDKs in multiple languages and example implementations you can reuse.
In Corti-Agentic, A2A is the backbone for agent collaboration. Whether you’re orchestrating specialist agents, chaining reasoning tasks, or integrating external agent services, A2A gives you a robust, open foundation you don’t have to reinvent.
### V1 API Bindings
The v1 API exposes two A2A v0.3 bindings:
| Binding | Endpoint | Methods |
| ---------------- | ------------------------------------ | ------------------------------------------------------- |
| HTTP+JSON (REST) | `POST /agents/{id}/v1/message:send` | Send a message |
| HTTP+JSON (REST) | `GET /agents/{id}/v1/tasks/{taskId}` | Get task status and history |
| JSON-RPC v0.3 | `POST /agents/{id}/v1` | `message/send`, `tasks/get`, and other JSON-RPC methods |
Both bindings share the same underlying handler—the REST endpoints are a convenience wrapper around the same logic as the JSON-RPC binding.
The v1 agent card advertises `streaming: true` in its capabilities, but the v1 API surface does **not** provide a streaming endpoint. Streaming (`message:stream` and `tasks:subscribe` SSE endpoints) is only available in the v2 API. Use polling via `GET /agents/{id}/v1/tasks/{taskId}` to track task progress in v1.
### Open Source SDKs and Tooling
For links to Corti’s official SDK and the official A2A project SDKs (Python, JavaScript/TypeScript, Java, Go, and .NET), see **[SDKs & Integrations](/agentic/v1/sdks-integrations)**.
Please [contact us](mailto:help@corti.ai) if you need more information about the Corti Agentic Framework.
# System Architecture
Source: https://docs.corti.ai/agentic/v1/architecture
Learn about the Agentic Framework system architecture
The Corti Agentic Framework adopts a **multi-agent architecture** to power development of healthcare AI solutions. As compared to a monolithic LLM, the Corti Agentic Framework allows for improved specialization and protocol-based composition.
## Architecture Components
The architecture consists of three core components working together:
* **[Orchestrator](/agentic/v1/orchestrator)** — The central coordinator that receives user requests and delegates tasks to specialized Experts via the A2A protocol.
* **[Experts](/agentic/v1/experts)** — Specialized sub-agents that perform domain-specific work, potentially calling external services through MCP.
* **[Memory](/agentic/v1/context-memory)** — Maintains persistent context and state, enabling the Orchestrator to make informed decisions and ensuring continuity across conversations.
Together, this architecture enables complex workflows through protocol-based composition while maintaining strict data isolation and stateless reasoning agents.
## Interaction mechanisms in Corti
The A2A Protocol supports various interaction patterns to accommodate different needs for responsiveness and persistence. Corti builds on these patterns so you can choose the right interaction model for your product:
* **Request/Response (Polling)**: Used for many synchronous Corti APIs where you send input and wait for a single response. For long‑running Corti tasks, your client can poll the task endpoint for status and results.
* **Streaming with Server-Sent Events (SSE)**: Used by Corti for real-time experiences (for example, ambient notes or live guidance). Your client opens an SSE stream to receive incremental tokens, events, or status updates over an open HTTP connection.
The v1 API does not provide a streaming endpoint, even though the agent card advertises `streaming: true`. Streaming (`message:stream` and `tasks:subscribe` SSE endpoints) is only available in the v2 API. In v1, use polling via `GET /agents/{id}/v1/tasks/{taskId}` to track task progress. The SSE pattern described here is available in the v2 API.
Please [contact us](mailto:help@corti.ai) if you need more information about the Corti Agentic Framework.
# Beginners' Guide to Agents
Source: https://docs.corti.ai/agentic/v1/beginners-guide
How LLM agents work in the Corti Agentic Framework
In healthcare, an **LLM agent** is not a chatbot trying to answer everything on its own. The language model is used primarily for reasoning and planning, understanding a request, breaking it down, and deciding which experts, tools, or data sources are best suited to handle each part of the task.
Instead of relying on internal knowledge, agents retrieve information from trusted external knowledge bases, clinical systems, and customer-owned data at runtime. When appropriate, they can also take controlled actions, such as writing structured data back to an EHR, triggering downstream workflows, or sending information to other systems.
The **Corti Agentic Framework** is the healthcare-grade platform that makes this possible in production. It provides the orchestration layer that allows agents to delegate work to specialized experts, operate within strict safety and governance boundaries, and remain fully auditable. This enables AI systems that can reason, look things up, and act, without guessing or bypassing clinical control.
# Context & Memory
Source: https://docs.corti.ai/agentic/v1/context-memory
Learn how context and memory work in the Corti Agentic Framework
A **context** in the Corti Agentic Framework makes use of memory from previous text and data in the conversation so far—think of it as a thread that maintains conversation history. Understanding how context works is essential for building effective integrations that maintain continuity across multiple messages.
## What is Context?
A `Context` (identified by a server-generated `contextId`) is a logical grouping of related `Messages`, `Tasks`, and `Artifacts`, providing context across a multi-turn "conversation". It enables you to associate multiple tasks and agents with a single patient encounter, call, or workflow, ensuring continuity and proper scoping of shared knowledge throughout.
When you send a message **without** a `contextId`, the server generates one automatically. You can also supply your own `contextId` to continue an existing conversation—pass the same `contextId` you received from a previous response to maintain continuity across messages.
### Data Isolation and Scoping
**Contexts provide data isolation across users**: Data can **never** leak across users. Each `contextId` creates a conversation scope where messages, tasks, and artifacts are isolated from other users' contexts. This ensures:
* **Privacy and security**: Patient data from one encounter cannot accidentally be exposed to another encounter
* **Data integrity**: Information from different workflows remains properly separated
* **Compliance**: You can confidently scope sensitive data to specific contexts without risk of cross-contamination
Contexts are scoped **per user**, not per agent. Any agent belonging to the same authenticated user can read any of that user's contexts. Do not rely on agent-level context isolation—if you need to restrict which agents can access which contexts, enforce that in your application layer.
When you need to share information across contexts, you must explicitly pass it via `DataPart` objects in your messages—there is no automatic data sharing between contexts.
## Using Context for Automatic Memory Management
The simplest way to use context is to let the framework automatically manage conversation memory:
### Workflow Pattern
1. **First message**: Send your message **without** a `contextId`. The server will create a new context automatically.
2. **Response**: The server's response includes the newly created `contextId`.
3. **Subsequent messages**: Include that `contextId` in your requests. Memory from previous messages in that context is automatically managed and available to the agent.
When you include a `contextId` in your request, the agent has access to all previous messages, artifacts, and state within **that specific context only**. Data from other contexts is completely isolated and inaccessible. This enables natural, continuous conversations without manually passing history, while maintaining strict data boundaries between different encounters or workflows.
### Standalone Requests
If you don't want automatic memory management, always send messages **without** a `contextId`. Each message will then be treated as a standalone request without access to prior conversation history. This is useful for:
* One-off queries that don't depend on prior context
* Testing and debugging individual requests
* Scenarios where you want explicit control over what context is included
## Passing Additional Context with Each Request
In addition to automatic memory management via `contextId`, you can pass additional context in each request by including `DataPart` objects in your message. This is useful when you want to provide specific structured data, summaries, or other context that should be considered for that particular request.
```json theme={null}
{
"message": {
"role": "user",
"parts": [
{
"kind": "text",
"text": "Generate a summary of this patient encounter"
},
{
"kind": "data",
"data": {
"patientId": "pat_12345",
"encounterDate": "2025-12-15",
"chiefComplaint": "Chest pain",
"vitalSigns": {
"bloodPressure": "120/80",
"heartRate": 72,
"temperature": 98.6
}
}
}
],
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"kind": "message",
"contextId": "ctx_abc123"
}
}
```
This approach allows you to:
* Provide structured data (patient records, clinical facts, etc.) alongside text
* Include summaries or distilled information from external sources
* Pass metadata or configuration that should be considered for this specific request
* Combine automatic memory (via `contextId`) with explicit context (via `DataPart`)
## How Memory Works
The Corti Agentic Framework provides an optional memory system that agents can use to search for relevant past content using semantic similarity.
Memory is **not** an automatic RAG pipeline. There is no automatic indexing of all content, no automatic semantic search on every message, and no automatic injection of retrieved context into the agent's prompt. Memory retrieval requires a memory MCP connector to be attached to the agent, and the LLM must decide to call the `search_memory` tool.
### Background Embedding
When an embedding client is configured (a deployment dependency, not guaranteed), user messages are embedded in the background for potential future retrieval. Agent responses are not embedded. DataParts carrying internal metadata markers (such as `$memoryType` or `$targetExpert`) are skipped.
### Explicit Retrieval via MCP Tool
If a memory MCP connector is attached to an agent, the agent's tool list includes a `search_memory` tool. The LLM decides whether to call this tool during processing—it is not called automatically. When the LLM calls `search_memory`, it performs semantic search across the context's embedded content and receives the results as a tool response, which it can then incorporate into its reasoning.
### What This Means in Practice
* Memory retrieval is **opt-in**: you need a memory MCP connector and the LLM must choose to call the tool
* Not all content is indexed: only user messages are embedded (when an embedding client is configured)
* There is no automatic context injection: the LLM receives tool results and incorporates them into its reasoning
* If no embedding client is configured or no memory connector is attached, the memory system is inactive
## Context vs. Reference Task IDs
The framework provides two mechanisms for linking related work:
* **`contextId`** – Groups multiple related `Messages`, `Tasks`, and `Artifacts` together (think of it as the encounter/call/workflow bucket). This provides automatic memory management and is sufficient for most use cases.
* **`referenceTaskIds`** – An optional list of specific past `Task` IDs within the same context that should be treated as explicit inputs or background. Note that `referenceTaskIds` are scoped to a context—they reference tasks within the same `contextId`.
**In most situations, you can ignore `referenceTaskIds`** since the automatic memory provided by `contextId` is sufficient. Only use `referenceTaskIds` when you need to explicitly direct the agent to pay attention to specific tasks or artifacts within the context, such as in complex multi-step workflows where you want to ensure certain outputs are prioritized.
```json theme={null}
{
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "Summarize the findings from the previous tasks." }],
"messageId": "550e8400-e29b-41d4-a716-446655440001",
"kind": "message",
"contextId": "ctx_abc123",
"referenceTaskIds": ["task-uuid-1", "task-uuid-2"]
}
}
```
## Message:send Configuration
The `message:send` endpoint accepts an optional `configuration` object alongside the `message` object. This controls blocking behavior, history length, output modes, and push notifications.
```json theme={null}
{
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "What was the patient's chief complaint?" }],
"messageId": "550e8400-e29b-41d4-a716-446655440002",
"kind": "message",
"contextId": "ctx_abc123"
},
"configuration": {
"blocking": true,
"historyLength": 10,
"acceptedOutputModes": ["text/plain"]
}
}
```
### `blocking`
Controls whether the server waits for the task to complete before returning a response.
* **`true`** (default): The server waits for the task to complete and returns the final result.
* **`false`**: The server returns immediately after the first task event (non-blocking mode). Use this for long-running tasks where you want to poll for results later via the get-task endpoint.
### `historyLength`
Controls how many previous messages are included in the response.
The default `historyLength` is **0** — no history is returned in the response. If you want conversation history included in the response, you must explicitly set `historyLength` to a positive value.
### `acceptedOutputModes`
An array of strings specifying the output MIME types the client accepts (for example, `["text/plain"]`).
### `pushNotificationConfig`
Optional configuration for push notifications. The agent card advertises `pushNotifications: false`, so this feature may not be fully implemented in all deployments.
## Context Management Endpoints
The v1 API provides two endpoints for managing contexts programmatically:
### List tasks in a context
```
GET /agents/{id}/v1/contexts/{contextId}?limit=50&offset=0
```
Returns the tasks belonging to a context, with `limit` and `offset` pagination. The agent ID in the path is not used for scoping—any agent belonging to the same authenticated user can read any of that user's contexts.
### Delete a context
```
DELETE /agents/{id}/v1/contexts/{contextId}
```
Deletes a context and scrubs all associated data. Returns 204 on success.
## Context Retention
Contexts are retained for a limited time. The current time-to-live (TTL) is approximately **30 days**, though this may vary by deployment configuration. After the TTL expires, the context and its associated data are automatically cleaned up.
The exact TTL depends on deployment configuration and may change. Contact your Corti representative for the current value in your environment.
## Context and Interaction IDs
If you're using contexts alongside Corti's internal interaction representation (for example, when integrating with Corti Assistant or other Corti products that use `interactionId`), note that **these two concepts are currently not linked**.
* **`contextId`** (from the Agentic Framework) and **`interactionId`** (from Corti's internal systems) are separate concepts that you will need to map yourself in your application.
* There is no automatic association between a Corti `interactionId` and an Agentic Framework `contextId`.
**Recommended approach:**
* **Use a fresh context per interaction**: When working with a Corti interaction, create a new `contextId` for that interaction. This keeps data properly scoped and isolated per interaction.
* Store the mapping between your `interactionId` and `contextId`(s) in your own application state or metadata.
* If you need to share data across multiple contexts within the same interaction, explicitly pass it via `DataPart` objects.
We're looking into ways to make the relationship between interactions and contexts more ergonomic if this is relevant to your use case. For now, maintaining your own mapping and using one context per interaction is the recommended pattern.
For more details on how context relates to other core concepts, see [Core Concepts](/agentic/v1/core-concepts).
Please [contact us](mailto:help@corti.ai) if you need more information about context and memory in the Corti Agentic Framework.
# Core Concepts
Source: https://docs.corti.ai/agentic/v1/core-concepts
Learn the fundamental building blocks of the Corti Agentic Framework
This page adds Corti-specific detail on top of the core A2A concepts. We have tried to adhere as closely as possible to the intended A2A protocol specification — for the canonical definition of these concepts, see the A2A documentation on [Core Concepts and Components in A2A](https://a2a-protocol.org/latest/topics/key-concepts).
The Corti Agentic Framework uses a set of core concepts that define how Corti agents, tools, and external systems interact. Understanding these building blocks is essential for developing on the Corti platform and for integrating your own systems using the A2A Protocol.
## Core Actors
At Corti, these actors typically map to concrete products and integrations:
* **User**: A clinician, contact-center agent, knowledge worker, or an automated service in your environment. The user initiates a request (for example, “summarize this consultation” or “triage this patient”) that requires assistance from one or more Corti-powered agents.
* **A2A Client (Client Agent)**: The application that calls Corti. This is your application/server. The client initiates communication using the A2A Protocol and orchestrates how results are used in your product.
* **A2A Server (Remote Agent)**: A Corti agent or agentic system that exposes an HTTP endpoint implementing the A2A Protocol. It receives requests from clients, processes tasks, and returns results or status updates.
## Fundamental Communication Elements
The following elements are fundamental to A2A communication and how Corti uses them:
A JSON metadata document describing an agent's identity, capabilities, endpoint, skills, and authentication requirements.
**Key Purpose:** Enables Corti and your applications to discover agents and understand how to call them securely and effectively.
A stateful unit of work initiated by an agent, with a unique ID and defined lifecycle.
**Key Purpose:** Powers long‑running operations in Corti (for example, document generation or multi‑step workflows) and enables tracking and collaboration.
A single turn of communication between a client and an agent, containing content and a role ("user" or "agent").
**Key Purpose:** Carries instructions, clinical context, user questions, and agent responses between your application, Corti Assistant, and remote agents.
The fundamental content container (for example, TextPart, FilePart, DataPart) used within Messages and Artifacts.
**Key Purpose:** Lets Corti exchange text, audio transcripts, structured JSON, and files in a consistent way across agents and tools.
A tangible output generated by an agent during a task (for example, a document, image, or structured data).
**Key Purpose:** Represents concrete Corti results such as SOAP notes, call summaries, recommendations, or other structured outputs.
A server-generated identifier (`contextId`) that logically groups multiple related `Task` objects, providing context across a series of interactions.
**Key Purpose:** Enables you to associate multiple tasks and agents with a single patient encounter, call, or workflow, ensuring continuity and proper scoping of shared knowledge throughout an interaction.
## Agent Cards in Corti
The Agent Card is a JSON document that serves as a digital business card for initial discovery and interaction setup. It provides essential metadata about an agent. Clients parse this information to determine if an agent is suitable for a given task, how to structure requests, and how to communicate securely. Key information includes identity, service endpoint (URL), A2A capabilities, authentication requirements, and a list of skills.
Within Corti, Agent Cards are how you:
* Discover first‑party Corti agents and their capabilities.
* Register and describe your own remote agents so Corti workflows can call them.
* Declare authentication and compliance requirements up front, before any PHI or sensitive data is exchanged.
## Messages and Parts in Corti
A message represents a single turn of communication between a client and an agent. It includes a role ("user" or "agent") and a unique `messageId`. It contains one or more Part objects, which are granular containers for the actual content. This design allows A2A to be modality independent and lets Corti mix clinical text, transcripts, and structured data safely in a single exchange.
The primary part kinds are:
* `TextPart`: Contains plain textual content, such as instructions, questions, or generated notes.
* `DataPart`: Carries structured JSON data. This is useful for clinical facts, workflow parameters, EHR identifiers, or any machine‑readable information you exchange with Corti.
* `FilePart`: Represents a file (for example, a PDF discharge letter or an audio recording). It can be transmitted either inline (Base64 encoded) or through a URI. It includes metadata like "name" and "mimeType". File parts are converted to data-part references—the LLM receives metadata (URL, size, filename, media type) but raw byte contents are not passed through inline. URL-based file parts work fully.
## Artifacts in Corti
An artifact represents a tangible output or a concrete result generated by a remote agent during task processing. Unlike general messages, artifacts are the actual deliverables. An artifact has a unique `artifactId`, a human-readable name, and consists of one or more part objects. Artifacts are closely tied to the task lifecycle and can be streamed incrementally to the client.
In Corti, artifacts typically correspond to business outputs such as:
* Clinical notes (for example, SOAP notes, discharge summaries).
* Extracted clinical facts or coding suggestions.
* Generated documents, checklists, or other workflow‑specific artifacts.
## Agent response: Task or Message
The agent response can be a new `Task` (when the agent needs to perform a long-running operation) or a `Message` (when the agent can respond immediately).
On the Corti platform this means:
* For quick operations (for example, a short completion or a classification), your agent often responds with a `Message`.
* For longer workflows (for example, generating a full clinical document, coordinating multiple tools, or waiting on downstream systems), your agent responds with a `Task` that you can monitor and later retrieve artifacts from.
## Task States
Tasks progress through a defined lifecycle. The v1 API uses the following task states:
| State | Description |
| ---------------- | ------------------------------------------------------------------------------- |
| `submitted` | Task has been accepted by the server |
| `working` | Agent is actively processing the task |
| `input-required` | Agent needs additional input from the user to continue |
| `completed` | Task finished successfully; artifacts are available |
| `canceled` | Task was canceled |
| `failed` | Task failed (for example, max iterations reached without a finishing tool call) |
| `rejected` | Task was rejected (for example, insufficient credits for orchestrator runs) |
| `auth-required` | Authentication is required to proceed (for example, MCP server returned 401) |
| `unknown` | Task state is unknown |
## Conversation History
The `message:send` endpoint does **not** return conversation history by default. The `historyLength` field in the `configuration` object defaults to **0**, meaning the response's `history` array will be empty. To include previous messages in the response, set `historyLength` to a positive value in the `configuration` object.
See [Context & Memory](/agentic/v1/context-memory#message-send-configuration) for details on the `configuration` object.
# Experts
Source: https://docs.corti.ai/agentic/v1/experts
Learn about Experts available for use with the AI Agent
An **Expert** is an LLM-powered capability that an AI agent can utilize. Experts are designed to complete small, discrete tasks efficiently, enabling the Orchestrator to compose complex workflows by chaining multiple experts together.
## Expert Registry
Corti maintains a **registry of experts** that includes both first-party experts built by Corti and third-party integrations. You can browse the available experts in the [Available Experts overview](/agentic/registry/overview), or discover them programmatically through the Expert Registry API endpoint `GET /agents/registry/experts`, which returns information about all available experts including their capabilities, descriptions, and configuration requirements.
The registry includes experts for various healthcare use cases such as:
* Clinical reference lookups
* Medical coding
* Document generation
* Data extraction
* And more
### Common registry experts
A minimal sample of frequently-used experts:
| Key | Purpose |
| --------------------------- | ------------------------------------------------------------------- |
| `memory-expert` | Recall and analyze content from large in-request contexts and files |
| `coding-expert` | Assign diagnosis and procedure codes from notes |
| `medical-calculator-expert` | Compute BMI, HbA1c, glucose conversions, etc. |
| `drugbank-expert` | Drug information and interaction lookups |
| `posos-expert` | Medication guidance and prescribing decision support |
| `pubmed-expert` | PubMed literature search and abstracts |
| `clinical-trials-expert` | Search clinical trial registries |
| `web-search-expert` | Search and retrieve up-to-date web content |
| `interviewing-expert` | Drive structured questionnaire interviews |
See the [Available Experts overview](/agentic/registry/overview) for the full list, including all coding-expert variants and per-expert configuration details.
## Bring Your Own Expert
You can create custom experts by exposing an MCP (Model Context Protocol) server. When you register your MCP server, Corti wraps it in a custom LLM agent with a system prompt that you can control. This allows you to:
* Integrate your own tools and data sources
* Create domain-specific experts tailored to your workflows
* Maintain control over the expert's behavior through custom system prompts
* Leverage Corti's orchestration and memory management while using your own tools
### Expert Configuration
When creating a custom expert, you provide configuration that includes:
* **Expert metadata**: ID, name, and description
* **System prompt**: Controls how the LLM agent behaves and reasons about tasks
* **MCP server configuration**: Details about your MCP server including transport type, authorization, and connection details (see [MCP Authentication](/agentic/v1/mcp-authentication) for details)
```json Expert Configuration expandable theme={null}
{
"name": "My Agent",
"description": "An agent with a custom ECG interpreter expert.",
"experts": [
{
"type": "new",
"name": "ecg_interpreter",
"description": "Interprets 12 lead ECGs.",
"systemPrompt": "You are an expert ECG interpreter.",
"mcpServers": [
{
"name": "ECG API Svc",
"transportType": "streamable_http",
"authorizationType": "none",
"url": "https://api.ecg.com/x"
}
]
}
]
}
```
The spec-correct value for inline expert creation is "type": "new". The server also accepts "type": "expert" as an alias, but "new" is the canonical value.
### MCP Server Requirements
Your MCP server must:
* Implement the [Model Context Protocol](https://modelcontextprotocol.io/) specification
* Expose tools via the standard MCP `tools/list` and `tools/call` endpoints
* Handle authentication
Once registered, your custom expert becomes available to the Orchestrator and can be used alongside Corti's built-in experts in multi-expert workflows.
## Multi-Agent Composition
This feature is coming soon.
We're working on exposing A2A (Agent-to-Agent) endpoints that will allow you to attach multiple agents together, enabling more sophisticated multi-agent workflows. This will provide:
* Direct agent-to-agent communication using the A2A protocol
* Composition of complex workflows across multiple agents
* Fine-grained control over agent interactions and data flow
For now, the Orchestrator handles expert composition automatically. When A2A endpoints are available, you'll be able to build custom agent networks while still leveraging Corti's orchestration capabilities.
## Direct Expert Calls
This feature is coming soon.
We're also working on enabling direct calls to experts, allowing you to use them directly in your workflows rather than only through agents. This will provide:
* Direct API access to individual experts
* Integration of experts into custom workflows
* More flexible composition patterns beyond agent-based orchestration
**While AI chat is a useful mechanism, it's not the only option!**
The Corti Agentic Framework is API-first, enabling synchronous or async usage across a range of modalities: scheduled batch jobs, clinical event triggers, UI widgets, and direct EHR system calls.
[Let us know](mailto:help@corti.ai) what types of use cases you're exploring, from doctor-facing chat bots to system-facing automation backends.
Please [contact us](mailto:help@corti.ai) if you need more information about Experts or creating custom experts in the Corti Agentic Framework.
# FAQ
Source: https://docs.corti.ai/agentic/v1/faq
Frequently asked questions about the Corti Agentic Framework
Common questions and answers to help you get the most out of the Corti Agentic Framework and the underlying A2A-based APIs.
The **Orchestrator** is the central coordinator of the Corti Agentic Framework. It receives user requests, reasons about what needs to be done, and delegates work to specialized Experts. The Orchestrator doesn't perform specialized work itself—instead, it plans, selects appropriate Experts, and coordinates their activities to accomplish complex workflows.
An **Expert** is a specialized sub-agent that performs domain-specific tasks. Experts are designed to complete small, discrete tasks efficiently, such as clinical reference lookups, medical coding, or document generation. The Orchestrator composes complex workflows by chaining multiple Experts together.
In summary: the Orchestrator coordinates and delegates; Experts execute specialized work.
For more details, see [Orchestrator](/agentic/v1/orchestrator) and [Experts](/agentic/v1/experts).
**A2A (Agent-to-Agent)** is the protocol used for accessing the Corti API and for communication between agents. It's the standard protocol that your application uses to interact with Corti agents, send messages, receive tasks, and manage the agent lifecycle. A2A enables secure, framework-agnostic communication between autonomous AI agents.
**MCP (Model Context Protocol)** is the way to connect additional Experts. When you create custom Experts by exposing an MCP server, Corti wraps it in a custom LLM agent. MCP handles agent-to-tool interactions, allowing Experts to interact with external systems and resources.
In the Corti Agentic Framework: A2A handles agent-to-agent communication (including your API calls to Corti), while MCP handles agent-to-tool interactions for Expert integrations.
For more information, see [A2A Protocol](/agentic/v1/a2a-protocol) and [MCP Protocol](/agentic/v1/mcp-protocol).
The Corti agent can return either a **Task** or a **Message** depending on the processing. If the agent completes the work synchronously, it returns a `Message`. If it creates a stateful unit of work, it returns a `Task` with a unique ID and lifecycle that you can monitor.
Tasks are used for:
* Long-running operations (for example, generating a full clinical document)
* Multi-step workflows that coordinate multiple Experts
* Operations that may need to wait on downstream systems
* Any work that benefits from tracking and monitoring
Messages (with immediate responses) are returned for quick operations like simple classifications or completions that can be resolved immediately without asynchronous processing.
For task states and their meanings, see [Core Concepts](/agentic/v1/core-concepts#task-states).
Use **`TextPart`** for messages that will be directly exposed to the Orchestrator and the LLM. TextPart content is immediately available for reasoning and response generation.
Use **`DataPart`** for structured JSON data that will be stored in memory first and accessed through more indirect manipulation. DataPart content is automatically indexed and stored in the context's memory, enabling semantic retrieval when needed. DataPart is JSON-only and is useful for structured data like patient records, clinical facts, workflow parameters, or EHR identifiers.
You can combine both in a single message: use TextPart for the primary instruction or question, and DataPart to provide structured context that will be semantically retrieved when relevant.
For more details, see [Core Concepts](/agentic/v1/core-concepts) and [Context & Memory](/agentic/v1/context-memory).
Both `Message` and `Artifact` use the same underlying `Part` primitives, but they serve different roles:
* **`Message` (with `role: "agent"`)**
* Represents a **single turn of communication** from the agent to the client.
* Best for ephemeral conversational output, intermediate reasoning, clarifications, or status updates.
* Typically tied to a particular task step but not necessarily considered a durable business deliverable.
* **`Artifact`**
* Represents a **tangible, durable output** of a task (for example, a SOAP note, coding suggestions, a structured fact bundle, or a generated document).
* Has its own `artifactId`, name/metadata, and lifecycle; can be streamed, versioned, and reused by later tasks.
* Is what downstream systems, UIs, or audits usually consume as the final result.
A useful mental model is: **Messages are how agents "talk"; Artifacts are what they "produce".** You might see several `agent` messages during a task (status, intermediate commentary), but only a small number of artifacts that represent the completed work.
The Corti Agentic Framework provides automatic memory management through contexts. When you send your first message without a `contextId`, the server generates one and returns it in the response. You can also supply your own `contextId` to continue an existing conversation—include that `contextId` in subsequent messages to maintain conversation continuity.
You can also pass additional context in each request using `DataPart` objects to include structured data, summaries, or other specific context alongside the automatic memory.
For comprehensive guidance on context and memory management, see [Context & Memory](/agentic/v1/context-memory).
No, you cannot share data between different contexts. Contexts provide data isolation—data cannot leak across users. Each `contextId` creates a conversation scope where messages, tasks, and artifacts are isolated from other users' contexts.
Contexts are scoped per user, not per agent. Any agent belonging to the same authenticated user can read any of that user's contexts. If you need to restrict which agents can access which contexts, enforce that in your application layer.
If you need to share information across contexts, you must explicitly pass it via `DataPart` objects in your messages—there is no automatic data sharing between contexts.
For more details, see [Context & Memory](/agentic/v1/context-memory).
The current time-to-live (TTL) for context memory is approximately **30 days**, though this may vary by deployment configuration. After this period, the context and its associated memory are automatically cleaned up. Contact your Corti representative for the current value in your environment.
For more information about context lifecycle and memory management, see [Context & Memory](/agentic/v1/context-memory).
The Orchestrator analyzes incoming requests and uses reasoning to determine which Expert(s) are needed to fulfill the task. It considers the nature of the request, the available Experts, and their capabilities.
You can control Expert selection by writing additional system prompts, both in the Orchestrator configuration and in individual Expert configurations. System prompts guide how the Orchestrator reasons about task decomposition and Expert selection, and how Experts interpret and execute their assigned work.
The Orchestrator can compose multiple Experts together, calling them in sequence or parallel as needed to accomplish complex workflows. For more information, see [Orchestrator](/agentic/v1/orchestrator) and [Experts](/agentic/v1/experts).
Please [contact us](mailto:help@corti.ai) if you need more information about the Corti Agentic Framework.
# MCP Authentication
Source: https://docs.corti.ai/agentic/v1/mcp-authentication
Learn how to authenticate MCP server calls in the Agentic Framework
This document covers how to register MCP servers and how to pass authentication data in A2A message DataParts so MCP tools can be registered.
## MCP server registration
Each MCP server record includes an `authorizationType` field that controls how the Agent API authenticates when registering tools and calling that server. DataParts provide credentials at runtime but do not change the configured authorization type.
### authorizationType = none
**Meaning**: MCP server is callable without authentication.
**Behavior**: No Authorization header or OAuth flow is used. Auth DataParts for this server are ignored.
**Registration example:**
```json theme={null}
{
"name": "medical-calculator",
"transportType": "streamable_http",
"authorizationType": "none",
"url": "http://mcp-server-medical-calculator.agents:80/mcp"
}
```
### authorizationType = inherit
**Meaning**: Reuse the incoming Agent API bearer token.
**Behavior**: Uses the token from the request `Authorization` header. The API request must include a valid bearer token or the request fails with `missing_inherited_token`.
The inherit type is also used internally by the Memory MCP server. Internal MCP servers are filtered from API responses, so you won't see them when listing an agent's MCP servers. This is why inherit exists as an option even if you don't use it directly.
**DataPart override**: If a token DataPart is supplied for this server name, that token is used instead of the inherited token.
**Registration example:**
```json theme={null}
{
"name": "medical-coding",
"transportType": "streamable_http",
"authorizationType": "inherit",
"url": "http://mcp-server-medical-coding.agents/mcp"
}
```
### authorizationType = bearer
**Meaning**: MCP server expects a bearer token.
**Behavior**: Uses the token from a matching DataPart (type=token). If the token is missing or invalid, the MCP server typically returns 401 and the task becomes `auth-required`.
**Registration example:**
```json theme={null}
{
"name": "medical-coding",
"transportType": "streamable_http",
"authorizationType": "bearer",
"url": "http://mcp-server-medical-coding.agents/mcp"
}
```
### authorizationType = oauth2.0
**Meaning**: MCP server expects OAuth client credentials.
**Behavior**: Uses `client_id` and `client_secret` from a matching DataPart (type=credentials) and performs a client\_credentials flow. Supported for `streamable_http` transport only; `sse` is not supported.
**Registration example:**
```json theme={null}
{
"name": "medical-coding",
"transportType": "streamable_http",
"authorizationType": "oauth2.0",
"url": "http://mcp-server-medical-coding.agents/mcp"
}
```
## Authorization via message DataParts
Authentication is supplied as an A2A DataPart with `kind: "data"` and the auth payload under `data`. The following fields are used:
* `type`: `token` or `credentials` (case-insensitive)
* `mcp_name`: MCP server name as registered (case-sensitive, trimmed)
* `token`: required when `type=token`
* `client_id` and `client_secret`: required when `type=credentials`
### Token example (for authorizationType=bearer or inherit override)
```json theme={null}
{
"kind": "data",
"data": {
"type": "token",
"mcp_name": "crm-mcp",
"token": "eyJhbGciOi..."
}
}
```
### Credentials example (for authorizationType=oauth2.0)
```json theme={null}
{
"kind": "data",
"data": {
"type": "credentials",
"mcp_name": "crm-mcp",
"client_id": "abc",
"client_secret": "def"
}
}
```
## Processing rules and errors
* `type` is normalized to lowercase; only `token` and `credentials` are extracted
* DataParts do not change the MCP server `authorizationType`—make sure the DataPart type matches the server configuration
* Unknown or invalid auth DataParts are left in the message as normal parts
* `mcp_name` must be unique per message; duplicates return `mcp_auth_duplicate_name`
* Missing fields return:
* `mcp_auth_missing_name`
* `mcp_auth_missing_token`
* `mcp_auth_missing_credentials`
* If `mcp_name` does not match any configured server, the DataPart is ignored
## When DataParts are used
* MCP tools are registered when a new thread is created (the first message). Include auth DataParts on that first message
* Later messages on the same thread do not re-register tools, so auth DataParts will be ignored for MCP registration
* In the API flow, extracted auth DataParts are removed from the message before it is stored or sent to reasoning
For more information about the A2A protocol and DataParts, see [A2A Protocol](/agentic/v1/a2a-protocol).
For general information about MCP, see [MCP Protocol](/agentic/v1/mcp-protocol).
Please [contact us](mailto:help@corti.ai) if you need more information about the Corti Agentic Framework.
# MCP Protocol (Model Context Protocol)
Source: https://docs.corti.ai/agentic/v1/mcp-protocol
Learn about the Model Context Protocol for tool integration
The Model Context Protocol (MCP) provides a standardized way for agents to interact with tools and external resources in the Corti Agentic Framework.
## What is MCP?
The Model Context Protocol (MCP) is a standardized protocol for exposing tools and resources to AI agents.
## When to Use MCP
MCP is ideal when you need to expose tools, APIs, or data sources to agents in a standardized way.
## How MCP Complements A2A
While A2A handles agent-to-agent communication, MCP handles agent-to-tool interactions, creating a complete communication framework.
## MCP in the Agentic Ecosystem
MCP plays a crucial role in the agentic ecosystem by enabling agents to interact with external systems and resources.
MCP handles tool execution, while A2A handles agent-to-agent communication. See [A2A Protocol](/agentic/v1/a2a-protocol) for more on inter-agent communication. For information about authenticating MCP server calls, see [MCP Authentication](/agentic/v1/mcp-authentication).
For the complete MCP specification, visit [modelcontextprotocol.io](https://modelcontextprotocol.io/specification/2025-06-18).
Please [contact us](mailto:help@corti.ai) if you need more information about the Corti Agentic Framework.
# Orchestrator
Source: https://docs.corti.ai/agentic/v1/orchestrator
Learn about the Orchestration Agent at the center of the Agentic Framework
The **Orchestrator** is the central intelligence layer of the Corti Agentic Framework. It serves as the primary interface between users and the multi-agent system, coordinating the flow of conversations and tasks.
## What the Orchestrator Does
The Orchestrator reasons about incoming requests and determines how to fulfill them by coordinating with specialized [Experts](/agentic/v1/experts). Its core responsibilities include:
* **Reasoning and planning**: Analyzes user requests and determines the necessary steps to complete them
* **Expert selection**: Decides which Expert(s) to call, in what order, and with what data
* **Task decomposition**: Breaks complex requests into discrete tasks that can be handled by individual Experts
* **Response generation**: Aggregates results from Experts and typically generates the final response to the user
* **Context management**: Has full access to the [context](/agentic/v1/context-memory), while Experts typically only have scoped access to relevant portions
* **Safety enforcement**: Enforces guardrails, type validation, and policy-driven constraints to ensure safe operation in clinical environments
The Orchestrator does not perform specialized work itself—instead, it delegates to appropriate Experts and coordinates their activities to accomplish complex workflows.
## Safety Mechanisms
### Max Iterations
The agent loop runs a maximum of 5 iterations by default (configurable per expert). After hitting the limit, it enters `forceFinish` mode, which makes up to 2 additional LLM calls restricted to terminal tools (`complete_tool`, `input_required_tool`, `task_failure_tool`). If those also fail to produce a terminal result, the task fails with "Max attempts reached without a finishing tool call."
### Credit Pre-flight Check
For orchestrator runs, the agent performs a pre-flight credit check before processing. If the metering service reports insufficient credits, the task is immediately rejected with state `rejected` and a message about insufficient credits.
### Expert Invocation Progress
When the orchestrator calls an expert tool, it emits a `Working` status update with the text "Calling expert: \". These intermediate status messages are part of the task's history but are not user-facing. If you are polling for task status, you may see these intermediate working states.
***
For more information about how the Orchestrator fits into the overall architecture, see [Architecture](/agentic/v1/architecture). To understand how context and memory work, see [Context & Memory](/agentic/v1/context-memory).
Please [contact us](mailto:help@corti.ai) if you need more information about the Orchestrator in the Corti Agentic Framework.
# Overview of the Corti Agentic Framework
Source: https://docs.corti.ai/agentic/v1/overview
AI for every healthcare app
The Corti Agentic Framework is a modular artificial intelligence system for software developers to build advanced AI agents that perform high-quality clinical and operational tasks, without having to spend months on complex architecture work.
The AI Agent is designed to support use cases across the healthcare spectrum, from chat-based assistants for doctors to automating EHR data entry and powering clinical decision support workflows.
## What Problems It Solves
Modern LLMs are powerful, but on their own they are insufficient and unsafe for clinical use.
The Corti Agent Platform addresses two fundamental gaps:
### 1. LLMs Do Not Have Reliable Access to Clinical Data
LLMs cannot be trusted to rely on internal knowledge alone. In healthcare, responses must be grounded in clinically validated reference sources, real-time patient and system data, and customer-owned systems and APIs. Without access to these sources at runtime, models are forced to infer or guess, which is unacceptable in clinical settings.
The Corti Agentic Framework addresses this by enabling agents to retrieve information directly from trusted external tools as part of their reasoning process. Instead of hallucinating answers, agents are designed to look things up, verify context, and base their outputs on authoritative data.
### 2. LLMs Cannot Safely Act on the World
Clinical workflows require more than generating text. They involve interacting with real systems: querying EHRs, drafting and updating documentation, preparing prescriptions, and triggering downstream processes.
The framework provides a controlled execution layer that allows agents to plan actions, invoke tools, and coordinate multi-step workflows while remaining within clearly defined safety boundaries. Where necessary, agents can pause execution, request human approval, and resume only once explicit consent is given. This ensures that automation enhances clinical workflows without bypassing governance or control.
***
## What You Can Build With It
Using the Corti Agent Platform, teams can build:
* **Clinician-facing assistants**
* Documentation editing
* Guideline and reference lookup
* Coding and administrative support
* **Programmatic agent endpoints**
* Embedded into existing clinical software
* Triggered by events, APIs, or workflows
* **Customer-embedded agents**
* Customers bring their own tools and systems
* Agents combine Corti, third-party, and customer capabilities
All of these share the same underlying agent runtime, safety model, and orchestration layer.
***
## Built for Healthcare by Design
Healthcare is not a general-purpose domain, and this platform reflects that reality.
**Key design principles include:**
Typed inputs and outputs, explicit tool schemas, and guardrails around action-taking ensure safe operation in clinical environments.Every decision and tool call is observable with replayable traces and structured logs for transparency, compliance, and quality assurance.Fine-tuned reasoning layers optimized for healthcare language, workflows, and compliance needs.Corti Agentic Framework uses a state-of-the-art multi-agent architecture to enable greater scale, accuracy, and resilience in AI-driven workflows.Maintain persistent, context-aware conversations and manage multiple active contexts (threads) without losing information throughout the session.Access a library of prebuilt Experts: specialized agents that connect to data sources, tools, and services to execute clinical and operational tasks.Plug directly into EHRs, clinical decision support systems, and medical knowledge bases with minimal setup.Pass relevant context with each query, including structured data formats like FHIR resources, enabling Experts to work with rich, domain-specific information.
***
## Who It’s For
The Corti Agent Platform is built for teams working on healthcare software.
It is intended for:
* **Healthcare software companies** embedding intelligent automation directly into their products
* **Enterprise customers** building internal, AI-powered clinical workflows
* **Advanced engineering teams** that need flexibility, control, and strong safety guarantees without building bespoke agent infrastructure from scratch
The platform is not limited to simple prompt-based chatbots. It is designed to make it easy to go from demo to **production-grade clinical AI systems** that operate safely in real-world healthcare environments.
***
## Agents vs. Workflows
Understanding the difference between agents and workflows helps you choose the right approach for your use case:
**Agents** are autonomous systems that can think, reason, and adapt to new situations. They use AI to understand context, make decisions dynamically, and take actions based on the task at hand—even when encountering scenarios they haven't seen before. Like a chef who can create a meal based on what's available, agents excel at handling unpredictable, open-ended tasks that require flexibility and judgment.
**Workflows** are structured, step-by-step processes that follow predefined paths. They execute tasks in a fixed order, like following a recipe or checklist. Workflows are ideal for repeatable processes that require consistency and compliance, such as automated approval processes or scheduled maintenance tasks.
For workflow-oriented needs, you can leverage our toolkit of other APIs to orchestrate well-defined, repeatable flows throughout your solution.
In the Corti Agentic Framework, agents leverage the Orchestrator to compose experts dynamically, adapting their approach based on the situation. Workflows, on the other hand, provide deterministic execution paths for tasks with well-defined steps and requirements—supported by our robust library of workflow APIs and integrations.
## V1 API Notes
### Usage Endpoint
The v1 API provides a usage endpoint for per-agent metrics:
```
GET /agents/{id}/usage?from=YYYY-MM-DD&to=YYYY-MM-DD
```
Returns per-day invocation metrics and unique context counts. The `from` and `to` query parameters accept dates in `YYYY-MM-DD` format, defaulting to a 30-day window.
### Agent Type Field
The v1 API accepts an optional `agentType` field on agent creation with enum values `expert`, `orchestrator`, and `interviewing-expert`. However, this field is **silently ignored** — the server always creates an orchestrator-type agent regardless of the value provided. Specialization comes from the system prompt and connected experts, not from `agentType`. This field is removed in v2.
### Description Field
The `description` field is required on agent creation per the v1 OpenAPI spec. A missing `description` key triggers a validation error (422), but an empty string (`"description": ""`) is accepted. In v2, `description` is optional.
Please [contact us](mailto:help@corti.ai) if you need more information about the Corti Agentic Framework.
# Quickstart (v1)
Source: https://docs.corti.ai/agentic/v1/quickstart
Get started with the Corti Agentic Framework (v1 archived documentation)
This is archived v1 documentation. For the current v2 docs, see the [Agentic Framework overview](/agentic/overview) and [v2 quickstart](/agentic/quickstart). For migrating from v1 to v2, see the [migration guide](/agentic/guides/migrate-v1-to-v2).
This guide will walk you through setting up your first agent and getting it running end-to-end.
After completing this quickstart, you'll have a working agent and know where to go next based on your use case.
### Prerequisites
* API access credentials
* Development environment set up
* Basic understanding of REST APIs
Start by creating a project in the Corti Console. This gives you a workspace and access to manage your clients and credentials. If you haven't set up authentication before, follow the Creating Clients and authentication quickstart guides.
Use the Corti Agentic API to create your first agent. You'll need an access token (obtained using your client credentials) and your tenant name.
```bash theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/agents" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"name": "My First Agent",
"description": "A simple agent to get started with the Corti Agentic Framework"
}'
```
Use your stored credentials to authenticate, then run your agent end-to-end and verify it processes input and returns the expected outputs.
```bash theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/agents/${AGENT_ID}/v1/message:send" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "Hello there. This is my first message." }],
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"kind": "message"
}
}'
```
### Next Steps
Depending on your use case:
* **Building custom agents**: See [Core Concepts](/agentic/v1/core-concepts)
* **Integrating with existing systems**: See [SDKs & Integrations](/agentic/v1/sdks-integrations)
* **Understanding the architecture**: See [Architecture Overview](/agentic/v1/architecture)
* **Working with protocols**: See [A2A Protocol](/agentic/v1/a2a-protocol) and [MCP Protocol](/agentic/v1/mcp-protocol)
Please [contact us](mailto:help@corti.ai) if you need more information about the Corti Agentic Framework.
# SDKs & Integrations
Source: https://docs.corti.ai/agentic/v1/sdks-integrations
Official SDKs and integration options for the Corti Agentic Framework
The Corti Agentic Framework provides official SDKs and supports community integrations to help you build quickly.
## Official Corti SDKs
Official Corti Agentic SDK for Node and browser environments. JavaScript SDK docs →
Official Corti Agentic SDK for .NET applications. C# .NET SDK docs →
Adapter for integrating Corti A2A agents with the Vercel AI SDK. Use `useChat` and streaming patterns to build chat UIs. AI SDK Adapter docs →
## Official A2A Project SDKs
These are community/open-source SDKs maintained by the A2A project. Check each repository for current maturity and compatibility status.
Build A2A-compliant agents and servers in Python. a2a-python →
Official JavaScript/TypeScript SDK for A2A. a2a-js →
Build A2A-compliant agents and services in Java. a2a-java →
Implement A2A agents and servers in Go. a2a-go →
Build A2A-compatible agents in .NET ecosystems. a2a-dotnet →
## Other libraries
* **shadcn/ui component library for chatbots**: [`ai-elements` on npm](https://www.npmjs.com/package/ai-elements)
* **A2A Inspector**: [a2a-inspector on GitHub](https://github.com/a2aproject/a2a-inspector)
* **Awesome A2A**: [awesome-a2a on GitHub](https://github.com/ai-boost/awesome-a2a)
# Authenticate user and get access token
Source: https://docs.corti.ai/api-reference/admin/auth/authenticate-user-and-get-access-token
/api-reference/admin/admin-openapi.yml post /auth/token
Authenticate using email and password to receive an access token.
This `Admin API` is separate from the `Corti API` used for speech recognition, text generation, and agentic workflows:
Authentication and scope for the `Admin API` uses email-and-password to obtain a bearer token via `/auth/token`. This token is only used for API administration.
Please [contact us](https://help.corti.app) if you have interest in this functionality or further questions.
# Create a new customer
Source: https://docs.corti.ai/api-reference/admin/customers/create-a-new-customer
/api-reference/admin/admin-openapi.yml post /projects/{projectId}/customers
# Delete a customer
Source: https://docs.corti.ai/api-reference/admin/customers/delete-a-customer
/api-reference/admin/admin-openapi.yml delete /projects/{projectId}/customers/{customerId}
# Get quotas for a customer
Source: https://docs.corti.ai/api-reference/admin/customers/get-quotas-for-a-customer
/api-reference/admin/admin-openapi.yml get /projects/{projectId}/customers/{customerId}/quotas
# List customers for a project
Source: https://docs.corti.ai/api-reference/admin/customers/list-customers-for-a-project
/api-reference/admin/admin-openapi.yml get /projects/{projectId}/customers
# Update a customer
Source: https://docs.corti.ai/api-reference/admin/customers/update-a-customer
/api-reference/admin/admin-openapi.yml patch /projects/{projectId}/customers/{customerId}
Update specific fields of a customer. Only provided fields will be updated.
# Get quotas for all tenants within a project
Source: https://docs.corti.ai/api-reference/admin/projects/get-quotas-for-all-tenants-within-a-project
/api-reference/admin/admin-openapi.yml get /projects/{projectId}/quotas
# Create a new user and add it to the customer
Source: https://docs.corti.ai/api-reference/admin/users/create-a-new-user-and-add-it-to-the-customer
/api-reference/admin/admin-openapi.yml post /projects/{projectId}/customers/{customerId}/users
# Delete a user
Source: https://docs.corti.ai/api-reference/admin/users/delete-a-user
/api-reference/admin/admin-openapi.yml delete /projects/{projectId}/customers/{customerId}/users/{userId}/consumption
# Get usage consumption for a user within a time window
Source: https://docs.corti.ai/api-reference/admin/users/get-usage-consumption-for-a-user-within-a-time-window
/api-reference/admin/admin-openapi.yml get /projects/{projectId}/customers/{customerId}/users/{userId}/consumption
# List users for a customer
Source: https://docs.corti.ai/api-reference/admin/users/list-users-for-a-customer
/api-reference/admin/admin-openapi.yml get /projects/{projectId}/customers/{customerId}/users
# Update a user
Source: https://docs.corti.ai/api-reference/admin/users/update-a-user
/api-reference/admin/admin-openapi.yml patch /projects/{projectId}/customers/{customerId}/users/{userId}
# Generate Codes
Source: https://docs.corti.ai/api-reference/codes-legacy/generate-codes
/api-reference/auto-generated-openapi.yml post /interactions/{id}/codes/
`Limited Access - Contact us for more information`
Generate codes within the context of an interaction. This endpoint is only accessible within specific customer tenants. It is not available in the public API.
For stateless code prediction based on input text string or documentId, please refer to the [Predict Codes](/api-reference/codes/predict-codes) API, or [contact us](https://help.corti.app) for more information.
# List Codes
Source: https://docs.corti.ai/api-reference/codes-legacy/list-codes
/api-reference/auto-generated-openapi.yml get /interactions/{id}/codes/
`Limited Access - Contact us for more information`
List predicted codes within the context of an interaction. This endpoint is only accessible within specific customer tenants. It is not available in the public API.
For stateless code prediction based on input text string or documentId, please refer to the [Predict Codes](/api-reference/codes/predict-codes) API, or [contact us](https://help.corti.app) for more information.
# Select Codes
Source: https://docs.corti.ai/api-reference/codes-legacy/select-codes
/api-reference/auto-generated-openapi.yml put /interactions/{id}/codes/
`Limited Access - Contact us for more information`
Select predicted codes within the context of an interaction. This endpoint is only accessible within specific customer tenants. It is not available in the public API.
For stateless code prediction based on input text string or documentId, please refer to the [Predict Codes](/api-reference/codes/predict-codes) API, or [contact us](https://help.corti.app) for more information.
# Predict Codes
Source: https://docs.corti.ai/api-reference/codes/predict-codes
/api-reference/auto-generated-openapi.yml post /tools/coding/
Predict medical codes from provided context. This is a stateless endpoint, designed to predict ICD-10-CM, ICD-10-PCS, ICD-10 (international), ICD-10-UK, CIM-10-FR, ICD-10-GM, OPCS-4, OPS, CCAM and CPT codes based on input text string or documentId.
More than one code system may be defined in a single request.
Code prediction requests have two possible values for context: - `text`: One set of code prediction results will be returned based on all input text defined. - `documentId`: Code prediction will be based on that defined document only.
The response includes two sets of results: - `Codes`: Codes predicted by the model. - `Candidates`: Lower-confidence codes the model considered potentially relevant but excluded from the predicted set.
All predicted code results are based on input context defined in the request only (not other external data or assets associated with an interaction).
# Create message (Anthropic)
Source: https://docs.corti.ai/api-reference/corti-models/anthropic/create-message-anthropic
/api-reference/corti-models/corti-models-openapi.yml post /messages
Sends a structured list of input messages and generates the next message in the conversation. This is the Anthropic Messages API.
Reasoning behavior mirrors the Chat Completions API: `corti-s1` and `corti-s1-mini` generate chain-of-thought reasoning, while `-instant` variants do not. However, the Anthropic Messages API response does not expose reasoning as a separate field — content is returned as text blocks only.
# Create chat completion
Source: https://docs.corti.ai/api-reference/corti-models/chat/create-chat-completion
/api-reference/corti-models/corti-models-openapi.yml post /chat/completions
Creates a model response for the given conversation. This is the OpenAI Chat Completions API.
Some models return chain-of-thought reasoning in the `reasoning` field of the response message. The `-instant` model variants do not produce reasoning on this endpoint.
# Create completion
Source: https://docs.corti.ai/api-reference/corti-models/completions/create-completion
/api-reference/corti-models/corti-models-openapi.yml post /completions
Creates a text completion for the given prompt. Supports streaming via SSE.
# Create embeddings
Source: https://docs.corti.ai/api-reference/corti-models/embeddings/create-embeddings
/api-reference/corti-models/corti-models-openapi.yml post /embeddings
Creates embeddings for the given input text.
# List models
Source: https://docs.corti.ai/api-reference/corti-models/models/list-models
/api-reference/corti-models/corti-models-openapi.yml get /models
Returns the list of available models.
# Create response
Source: https://docs.corti.ai/api-reference/corti-models/responses/create-response
/api-reference/corti-models/corti-models-openapi.yml post /responses
Creates a model response for the given input. This is the OpenAI Responses API. Supports `instructions` (system message equivalent), streaming, tool calling, JSON mode, and multi-turn conversations.
When present, reasoning is returned as a structured output item with `type: "reasoning"`, separate from the message output.
# Delete Document
Source: https://docs.corti.ai/api-reference/documents-classic/delete-document
/api-reference/auto-generated-openapi.yml delete /interactions/{id}/documents/{documentId}
# Generate Document
Source: https://docs.corti.ai/api-reference/documents-classic/generate-document
/api-reference/auto-generated-openapi.yml post /interactions/{id}/documents/
This endpoint offers different ways to generate a document. Find guides to document generation [here](/textgen/documents-standard).
# Get Document
Source: https://docs.corti.ai/api-reference/documents-classic/get-document
/api-reference/auto-generated-openapi.yml get /interactions/{id}/documents/{documentId}
Get Document.
# List Documents
Source: https://docs.corti.ai/api-reference/documents-classic/list-documents
/api-reference/auto-generated-openapi.yml get /interactions/{id}/documents/
List Documents
# Update Document
Source: https://docs.corti.ai/api-reference/documents-classic/update-document
/api-reference/auto-generated-openapi.yml patch /interactions/{id}/documents/{documentId}
# Errors
Source: https://docs.corti.ai/api-reference/errors
Error messages and solutions from Corti's API
Corti uses standard HTTP status codes. `4xx` errors indicate a problem with the request; `5xx` errors indicate a problem on Corti's side. Error responses are in JSON and differ between the SDKs and the REST API. The tabs below cover each.
Some Corti products handle errors and authentication differently than what is described below:
* **Admin API** — Separate from the `Corti API` used for speech to text, text generation, and agentic workflows. See the [Administration API](/about/admin-api) reference for details. Please [contact us](mailto:help@corti.ai) if you have interest in this functionality or further questions.
* **Agents API** — The JSON format of the error response is different. See the [Agents API](/agentic/overview) docs for more info.
* **Embedded Assistant** — Authentication is handled differently. See the [Embedded Assistant Authentication](https://docs.corti.ai/assistant/authentication) docs for those methods.
The SDK throws typed error classes on any non-2xx response or internal failure. Each class exposes a different set of attributes. See [Error Handling](/sdk/js/overview#error-handling) for full details.
## Error Classes
| Class | Thrown when | Attributes |
| --------------- | ----------------------------------------------------------------- | ---------------------------------------------- |
| `CortiError` | API returned a non-2xx HTTP response (4xx / 5xx) | `message`, `statusCode`, `body`, `rawResponse` |
| `CortiSDKError` | SDK infrastructure error (e.g. localStorage unavailable) | `message`, `code`, `cause` |
| `ParseError` | Input validation failed (e.g. missing PKCE verifier, invalid JWT) | `message`, `errors` |
| `JsonError` | Response body could not be parsed as JSON | `message`, `errors` |
## HTTP Status Codes
| Error code | When it occurs | How to resolve |
| ------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `bad_request` | The request body or query parameters are malformed or missing required fields. | Double check the fields submitted to be sure of correct format and necessary fields. |
Token endpoint errors are returned as JSON in the response body. Corti API errors are returned in the `WWW-Authenticate` response header with no body. The header is not accessible from the exception directly; use `.WithRawResponse()` on the request if you need it.
| Error code | When it occurs | How to resolve |
| --------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `invalid_client` | Client authentication failed. There is a problem with the client ID. | Verify your `client_id` is correct. |
| `unauthorized_client` | Client authentication failed. There is a problem with the client secret. | Verify your `client_secret` is correct. |
| `invalid_token` | The bearer token is invalid or has expired. Check the `WWW-Authenticate` header for `error_description`. | Re-authenticate to obtain a fresh token and retry the request. |
| When it occurs | How to resolve |
| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Accessing a resource (interaction, recording, transcript, document) that belongs to a different client. | Verify the resource ID was created by the authenticated client. |
| Error | When it occurs | How to resolve |
| ------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `Invalid '{resource_name}' '{resource_id}'` | The requested resource does not exist. | Verify the ID and parent resource ID is correct and was created under the authenticated tenant. |
Attempting to access a resource that belongs to a different OAuth client also returns a 404, not a 403. This is intentional. Returning a 403 would confirm that the resource exists, which is a security risk.
You have exceeded the rate limit for the API. The SDK automatically retries with exponential backoff. The default retry limit is 2. Override per request:
```js theme={null}
const response = await client.interactions.create(..., {
maxRetries: 0, // disable retries for this request
});
```
If the error persists, reduce request frequency or contact [support](mailto:help@corti.ai) to discuss your rate limit requirements.
An unexpected error occurred on the Corti side. Double check the request format and retry the request. If the issue persists, contact [support](mailto:help@corti.ai).
The service is temporarily unavailable. Retry with exponential backoff. Monitor the [status page](https://status.corti.ai).
## WebSocket APIs
WebSocket errors from `/streams` and `/transcribe` are surfaced as plain `Error` objects whose `message` is the server status code (e.g. `CONFIG_DENIED`, `CONFIG_MISSING`, `CONFIG_TIMEOUT`).
Every `CONFIG_*` server message also carries a `reason` field with human-readable details behind the failure. Whether you can read it depends on the mode you connected in:
* **Default mode** (`client.stream.connect({ id, configuration })` / `client.transcribe.connect({ configuration })`): the SDK handles the handshake-phase and `CONFIG_*` messages internally. It rejects `connect()` with just the bare code. The `reason` is not exposed.
* **`awaitConfiguration: false`** or **[manual mode](/sdk/js/websockets#connecting-without-configuration)**: the raw message, including `msg.reason`, flows through `socket.on('message', …)`.
> You sent a config message, but it's invalid.
| `reason` from server | Likely cause | Fix |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `invalid language code: must be a valid BCP 47 language tag` | `transcription.primaryLanguage` is not a supported code. | Use a [supported language code](https://docs.corti.ai/stt/languages). |
| `invalid output locale: language should not be empty` | `mode.type: "facts"` without `mode.outputLocale`. | Add `outputLocale`. |
| `invalid output locale: invalid language code: must be a valid BCP 47 language tag` | `mode.type: "facts"` with invalid `mode.outputLocale` code. | Use a [supported language code](https://docs.corti.ai/stt/languages) |
| `unknown mode` | `mode.type` is not `"transcription"` or `"facts"`. | Use one of the two supported values. |
| `invalid participants` | Participant `role` outside `doctor` / `patient` / `multiple`. | Use one of the three supported roles. |
| `invalid audio format: MIME type not allowed...` | `audioFormat` MIME type is not in the supported list. | Use a [supported MIME type](https://docs.corti.ai/stt/audio) or omit the field. |
> You sent a non-config frame before any valid `config` message arrived.
Only reachable when you opt out of the [automatic handshake](/sdk/js/websockets#connecting). Default mode sends config for you.
| `reason` from server | Cause | Fix |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `No valid configuration provided for interaction` | A non-config frame reached the server before a config message. Typically sendAudio() / sendFlush() / sendEnd() in manual mode, or a raw socket.send() queued before OPEN in awaitConfiguration: false. | Be sure to call sendConfiguration() immediately after waitForOpen(), or pass configuration to connect() and let the SDK handle the handshake.\` |
> The 10-second handshake window elapsed without any `config` message arriving.
Only reachable in [manual mode](/sdk/js/websockets#connecting-without-configuration). The [default handshake](/sdk/js/websockets#connecting) sends config well within the 10s budget.
| `reason` from server | Cause | Fix |
| ------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------- |
| Manual mode: no configuration provided within 10s | Server's 10s handshake timer expired. | Call `sendConfiguration()` immediately after `waitForOpen()`. |
The SDK throws typed exceptions on any non-2xx response. Each exception extends `CortiClientApiException`, which exposes `Message`, `StatusCode`, and `Body`. See [Error Handling](/sdk/dotnet/overview#error-handling) for full details.
## Exceptions
| Exception class | Thrown when | Attributes |
| ------------------------- | ------------------------- | ------------------------------- |
| `CortiClientApiException` | Any non-2xx HTTP response | `Message`, `StatusCode`, `Body` |
Each status code throws a typed subclass with a more specific `Body`. See the accordions below for details.
## HTTP Status Codes
| Exception class | Error code | When it occurs | How to resolve |
| ----------------- | ------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `BadRequestError` | `bad_request` | The request body or query parameters are malformed or missing required fields. | Double check the fields submitted to be sure of correct format and necessary fields. |
Token endpoint errors are returned as JSON in the response body. Corti API errors are returned in the `WWW-Authenticate` response header with no body. The header is not accessible from the exception directly; use `.WithRawResponse()` on the request if you need it.
| Exception class | Error code | When it occurs | How to resolve |
| ------------------- | --------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------- |
| `UnauthorizedError` | `invalid_client` | Client authentication failed. There is a problem with the client ID. | Verify your `client_id` is correct. |
| `UnauthorizedError` | `unauthorized_client` | Client authentication failed. There is a problem with the client secret. | Verify your `client_secret` is correct. |
| `UnauthorizedError` | `invalid_token` | The bearer token is invalid or has expired. | Re-authenticate to obtain a fresh token and retry the request. |
| Exception class | When it occurs | How to resolve |
| ---------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `ForbiddenError` | Accessing a resource (interaction, recording, transcript, document) that belongs to a different client. | Verify the resource ID was created by the authenticated client. |
| Exception class | Error code | When it occurs | How to resolve |
| --------------- | ------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `NotFoundError` | `Invalid '{resource_name}' '{resource_id}'` | The requested resource does not exist. | Verify the ID and parent resource ID is correct and was created under the authenticated tenant. |
Attempting to access a resource that belongs to a different OAuth client also returns a 404, not a 403. This is intentional. Returning a 403 would confirm that the resource exists, which is a security risk.
| Exception class | When it occurs | How to resolve |
| -------------------------- | -------------------------- | --------------------------------------------------------------------------------- |
| `UnprocessableEntityError` | Request validation failed. | Check `Body` for `Code`, `Description`, `HowToFix`, `Detail`, and `Cause` fields. |
| Exception class | When it occurs | How to resolve |
| ------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `CortiClientApiException` | Rate limit exceeded after retries exhausted (default: 2 retries with backoff). | Reduce request frequency or contact [support](mailto:help@corti.ai) to discuss your rate limit requirements. |
| Exception class | When it occurs | How to resolve |
| --------------------- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `InternalServerError` | An unexpected error occurred on the Corti side. `Body` contains `Detail`, `Type`, `Status`, and `RequestId`. | Double check your request format and retry the request. If the issue persists, contact [support](mailto:help@corti.ai). |
| Exception class | When it occurs | How to resolve |
| ----------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `BadGatewayError` | A gateway error occurred. `Body` contains `Detail`, `Type`, `Status`, and `RequestId`. | Retry the request. If the issue persists, contact [support](mailto:help@corti.ai). |
| Exception class | When it occurs | How to resolve |
| ------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |
| `CortiClientApiException` | The service is temporarily unavailable. | Retry with exponential backoff. Monitor the [status page](https://status.corti.ai). |
| Exception class | When it occurs | How to resolve |
| --------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `GatewayTimeoutError` | The gateway timed out. `Body` contains `Detail`, `Type`, `Status`, and `RequestId`. | Retry the request. If the issue persists, contact [support](mailto:help@corti.ai). |
## WebSocket APIs
WebSocket errors from `/streams` and `/transcribe` surface as exceptions thrown by `ConnectAsync()` (with `Message` set to the server status code, e.g. `CONFIG_DENIED`, `CONFIG_MISSING`, `CONFIG_TIMEOUT`) or as typed events on the socket.
Every `CONFIG_*` server message also carries a `Reason` field with human-readable details behind the failure. Whether you can read it depends on how you connect:
* **Default mode** (`stream.ConnectAsync(new StreamConfig { … })` / `transcribe.ConnectAsync(new TranscribeConfig { … })`): the SDK handles the handshake-phase and `CONFIG_*` messages internally. It throws from `ConnectAsync()` with just the bare code. The `Reason` is not exposed.
* **[Manual mode](/sdk/dotnet/websockets#connecting-without-configuration)** (`ConnectAsync()` with no config): the raw message, including `msg.Reason`, flows through the `StreamConfigStatusMessage` / `TranscribeConfigStatusMessage` event. Subscribe **before** calling `ConnectAsync()` to catch handshake-phase events.
> You sent a config message, but it's invalid.
| `reason` from server | Likely cause | Fix |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `invalid language code: must be a valid BCP 47 language tag` | `transcription.primaryLanguage` is not a syntactically valid BCP-47 tag (e.g. `"english"`, `"en_US"`, empty string). | Use a [supported language code](https://docs.corti.ai/stt/languages). |
| `invalid output locale: language should not be empty` | `mode.type: "facts"` without `mode.outputLocale` (or with an empty value). | Set `mode.outputLocale` to a valid BCP-47 code whenever `mode.type` is `facts`. |
| `invalid output locale: invalid language code: must be a valid BCP 47 language tag` | `mode.type: "facts"` with a syntactically invalid `mode.outputLocale`. | Use a [supported language code](https://docs.corti.ai/stt/languages) for `outputLocale`. |
| `unknown mode` | `mode.type` is not `transcription` or `facts`. | Use one of the two supported values. |
| `invalid participants` | Participant `role` outside `doctor` / `patient` / `multiple`. | Restrict to the three supported roles. |
| `invalid audio format: MIME type not allowed...` | `audioFormat` MIME type is not in the supported list. | Use a [supported MIME type](https://docs.corti.ai/stt/audio) or omit the field. |
> You sent a non-config frame before any valid `config` message arrived.
Only reachable in [manual mode](/sdk/dotnet/websockets#connecting-without-configuration). Default mode sends config for you.
| `Reason` from server | Cause | Fix |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `No valid configuration provided for interaction` | A non-config frame reached the server before a config message. Typically a `Send(byte[], …)` audio call, or a `Send(StreamFlushMessage, …)` / `Send(StreamEndMessage, …)` call before any `Send(StreamConfigMessage, …)`. | Call `Send(new StreamConfigMessage { … }, …)` (or `TranscribeConfigMessage`) as the first frame after `ConnectAsync()` returns, **or** pass the config directly to `ConnectAsync(new StreamConfig { … })` and let the SDK handle the handshake. |
> The 10-second handshake window elapsed without any `config` message arriving.
Only reachable in [manual mode](/sdk/dotnet/websockets#connecting-without-configuration). The default handshake sends config well within the 10s budget.
| `Reason` from server | Cause | Fix |
| -------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `no configuration provided within 10s` | Server's 10s handshake timer expired without receiving a config message. | Call `Send(new StreamConfigMessage { … }, …)` (or `TranscribeConfigMessage`) immediately after `ConnectAsync()` returns, **or** pass the config directly to `ConnectAsync(new StreamConfig { … })` and let the SDK handle the handshake. |
**Resource and API errors** (all endpoints except the OAuth token endpoint) return JSON with `requestid`, `status`, `type`, `detail`, and `validationErrors` fields:
```json theme={null}
{
"requestid": "",
"status": 400,
"type": "",
"detail": "",
"validationErrors": [
{}
]
}
```
**Authentication errors** from the OAuth token endpoint return `error` and `error_description` fields:
```json theme={null}
{
"error": "invalid_client",
"error_description": "Client authentication failed."
}
```
## HTTP Status Codes
| Error code | When it occurs | How to resolve |
| ------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `invalid_request` | The request is missing a required parameter or is otherwise malformed. | Ensure required fields (`grant_type`,`scope`) are present and correctly formatted. |
| `unsupported_grant_type` | The `grant_type` value is not recognised by the server. | This API requires a `grant_type` of `client_credentials`. Corti Assistant requires alternate methods for `grant_type`. |
| `invalid_scope` | The requested scope does not exist or is not permitted for this client. | Use a valid scope. This API requires `openid`. |
Token endpoint errors are returned as JSON in the response body. Corti API errors are returned in the `WWW-Authenticate` response header with no body. Check the header directly if the response body is empty.
| Error code | When it occurs | How to resolve |
| --------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `invalid_client` | Client authentication failed. There is a problem with the client ID. | Verify your `client_id` is correct. |
| `unauthorized_client` | Client authentication failed. There is a problem with the client secret | Verify your `client_secret` is correct. |
| `invalid_token` | The bearer token is invalid or has expired. Check the `WWW-Authenticate` header for `error_description`. | Re-authenticate to obtain a fresh token and retry the request. |
| When it occurs | How to resolve |
| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Accessing a resource (interaction, recording, transcript, document) that belongs to a different client. | Verify the resource ID was created by the authenticated client. |
| Error | When it occurs | How to resolve |
| ------------------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `Invalid '{resource_name}' '{resource_id}'` | The requested resource does not exist. | Verify the ID and parent resource ID is correct and was created under the authenticated tenant. |
| `'{resource_name}' not found` | The requested resource does not exist. | Verify the ID and parent resource ID is correct and was created under the authenticated tenant. |
You have exceeded the rate limit for the API. Retry the request after a delay, using exponential backoff. If the error persists, reduce request frequency or contact [support](mailto:help@corti.ai) to discuss your rate limit requirements.
An unexpected error occurred on the Corti side. Double check your request format and retry the request. If the issue persists, contact [support](mailto:help@corti.ai).
The service is temporarily unavailable. Retry with exponential backoff. Monitor the [status page](https://status.corti.ai).
## WebSocket APIs
WebSocket errors from `/streams` and `/transcribe` arrive as plain JSON messages on the socket: `{ type: "CONFIG_DENIED", reason: "…" }`, `{ type: "CONFIG_MISSING", reason: "…" }`, etc. Without an SDK in the middle, you see every server message verbatim, including the `reason` field on every `CONFIG_*` event.
**The raw protocol is manual mode out of the box.** There's no SDK layer handling the handshake for you. You open the socket, send `{ type: "config", configuration: { … } }` yourself, and listen for `CONFIG_ACCEPTED` before sending audio. See the [Streams API reference](/api-reference/streams) for the full protocol.
**The server does not close the socket on `CONFIG_DENIED` or `CONFIG_MISSING`.** Both codes reject the input but keep the connection open. Each subsequent non-config frame triggers a fresh `CONFIG_MISSING` response, one per frame, until the client closes the socket. SDK consumers don't see this because the SDK calls `close()` on rejection internally. Raw-protocol clients should close the socket themselves once they receive either rejection.
> You sent a config message, but it's invalid.
| `reason` from server | Likely cause | Fix |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `invalid language code: must be a valid BCP 47 language tag` | `transcription.primaryLanguage` is not a syntactically valid BCP-47 tag (e.g. `"english"`, `"en_US"`, empty string). | Use a [supported language code](https://docs.corti.ai/stt/languages). |
| `invalid output locale: language should not be empty` | `mode.type: "facts"` without `mode.outputLocale` (or with an empty value). | Set `mode.outputLocale` to a valid BCP-47 code whenever `mode.type` is `facts`. |
| `invalid output locale: invalid language code: must be a valid BCP 47 language tag` | `mode.type: "facts"` with a syntactically invalid `mode.outputLocale`. | Use a [supported language code](https://docs.corti.ai/stt/languages) for `outputLocale`. |
| `unknown mode` | `mode.type` is not `transcription` or `facts`. | Use one of the two supported values. |
| `invalid participants` | Participant `role` outside `doctor` / `patient` / `multiple`. | Restrict to the three supported roles. |
| `invalid audio format: MIME type not allowed...` | `audioFormat` MIME type is not in the supported list. | Use a [supported MIME type](https://docs.corti.ai/stt/audio) or omit the field. |
> You sent a non-config frame before any valid `config` message arrived.
| `reason` from server | Cause | Fix |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `No valid configuration provided for interaction` | A non-config frame (binary audio, `{ type: "flush" }`, `{ type: "end" }`) reached the server before a `{ type: "config", configuration: { … } }` message. | Send the config message as the first frame after the WebSocket opens. Wait for `CONFIG_ACCEPTED` before sending any other frame. |
> The 10-second handshake window elapsed without any `config` message arriving.
| `reason` from server | Cause | Fix |
| -------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| `no configuration provided within 10s` | Server's 10s handshake timer expired without receiving a config message. | Send `{ type: "config", configuration: { … } }` immediately after the WebSocket opens. |
# Add Facts
Source: https://docs.corti.ai/api-reference/facts/add-facts
/api-reference/auto-generated-openapi.yml post /interactions/{id}/facts/
Adds new facts to an interaction.
# Extract Facts
Source: https://docs.corti.ai/api-reference/facts/extract-facts
/api-reference/auto-generated-openapi.yml post /tools/extract-facts
Extract facts from provided text, without storing them.
# List Fact Groups
Source: https://docs.corti.ai/api-reference/facts/list-fact-groups
/api-reference/auto-generated-openapi.yml get /factgroups/
Returns a list of available fact groups, used to categorize facts associated with an interaction.
# List Facts
Source: https://docs.corti.ai/api-reference/facts/list-facts
/api-reference/auto-generated-openapi.yml get /interactions/{id}/facts/
Retrieves a list of facts for a given interaction.
# Update Fact
Source: https://docs.corti.ai/api-reference/facts/update-fact
/api-reference/auto-generated-openapi.yml patch /interactions/{id}/facts/{factId}
Updates an existing fact associated with a specific interaction.
# Update Facts
Source: https://docs.corti.ai/api-reference/facts/update-facts
/api-reference/auto-generated-openapi.yml patch /interactions/{id}/facts/
Updates multiple facts associated with an interaction.
# Delete document
Source: https://docs.corti.ai/api-reference/guided-documents/delete-document
/api-reference/auto-generated-openapi.yml delete /documents/{documentID}
Deletes the document. This cannot be undone.
# Generate a structured document
Source: https://docs.corti.ai/api-reference/guided-documents/generate-a-structured-document
/api-reference/auto-generated-openapi.yml post /documents/
Generates a structured document using one of three template-supply paths: a stored template reference (optionally with runtime overrides), an ad-hoc assembly of stored sections, or a fully inline dynamic template. Exactly one of `templateRef`, `assemblyTemplate`, or `dynamicTemplate` must be provided.
Context can combine different types or reference an interactionId to automatically fetch existing context to pass to the LLM. Note that discarded facts are not passed to the LLM.
With the exception of the plain `templateRef` path (no overrides), every call creates a new auto-generated template aggregate that snapshots the resolved prompts as a drift-proof receipt, persisted for 30 days.
# Get document
Source: https://docs.corti.ai/api-reference/guided-documents/get-document
/api-reference/auto-generated-openapi.yml get /documents/{documentID}
Returns a previously generated document by ID, including its rendered string output
and structured object.
# List documents
Source: https://docs.corti.ai/api-reference/guided-documents/list-documents
/api-reference/auto-generated-openapi.yml get /documents/
Returns a list of previously generated documents.
Use query parameters to filter by template, interaction, or label.
# Update document
Source: https://docs.corti.ai/api-reference/guided-documents/update-document
/api-reference/auto-generated-openapi.yml patch /documents/{documentID}
Updates the document's `name`, `labels`, or rendered output (`stringDocument` / `structuredDocument`).
Use this to persist edits made to a previously generated document.
# Create section
Source: https://docs.corti.ai/api-reference/guided-sections/create-section
/api-reference/auto-generated-openapi.yml post /documents/sections/
Creates a new section with an initial version. When `publish` is true (default),
the response includes the published version with full inheritance resolution applied
(section inheritance chain walked to fill missing fields).
# Create section version
Source: https://docs.corti.ai/api-reference/guided-sections/create-section-version
/api-reference/auto-generated-openapi.yml post /documents/sections/{sectionID}/versions/
Creates a new section version. Returns raw authored values without inheritance resolution.
# Delete section
Source: https://docs.corti.ai/api-reference/guided-sections/delete-section
/api-reference/auto-generated-openapi.yml delete /documents/sections/{sectionID}
Deletes a section and its versions. Returns 409 if other sections inherit from this section.
# Delete section version
Source: https://docs.corti.ai/api-reference/guided-sections/delete-section-version
/api-reference/auto-generated-openapi.yml delete /documents/sections/{sectionID}/versions/{versionID}
Currently published version cannot be deleted. Last remaining version can be deleted, simply create a new section version again if needed.
# Get section
Source: https://docs.corti.ai/api-reference/guided-sections/get-section
/api-reference/auto-generated-openapi.yml get /documents/sections/{sectionID}
Returns the section with its published version fully resolved (inheritance chain walked
to fill missing fields). To see raw authored values without inheritance, use
GET /documents/sections/{sectionID}/versions/{versionID}.
# Get section version
Source: https://docs.corti.ai/api-reference/guided-sections/get-section-version
/api-reference/auto-generated-openapi.yml get /documents/sections/{sectionID}/versions/{versionID}
Returns raw authored section version without inheritance resolution. To see resolved content, use GET /sections/{sectionID} instead.
# List section versions
Source: https://docs.corti.ai/api-reference/guided-sections/list-section-versions
/api-reference/auto-generated-openapi.yml get /documents/sections/{sectionID}/versions/
Returns raw authored section versions without inheritance resolution. To see resolved content, use GET /sections/{sectionID} instead.
# List sections
Source: https://docs.corti.ai/api-reference/guided-sections/list-sections
/api-reference/auto-generated-openapi.yml get /documents/sections/
Returns a list of sections and their metadata. Fetch a sectionId to get the full generation content.
Use query parameters to filter by language, region, specialty, label, publish status, or source.
# Publish section version
Source: https://docs.corti.ai/api-reference/guided-sections/publish-section-version
/api-reference/auto-generated-openapi.yml post /documents/sections/{sectionID}/versions/{versionID}/publish
Sets this version as the published version of the section.
# Update section metadata
Source: https://docs.corti.ai/api-reference/guided-sections/update-section-metadata
/api-reference/auto-generated-openapi.yml patch /documents/sections/{sectionID}
Updates the section's metadata fields (name, description, languages, regions, specialties, labels).
Generation content (instructions, output schema) is managed through versions and cannot be updated here.
# Create template
Source: https://docs.corti.ai/api-reference/guided-templates/create-template
/api-reference/auto-generated-openapi.yml post /documents/templates/
Creates a new template with an initial version. When `publish` is true (default),
the response includes the published version with full inheritance resolution applied
(template-level and section-level inheritance walked).
# Create template version
Source: https://docs.corti.ai/api-reference/guided-templates/create-template-version
/api-reference/auto-generated-openapi.yml post /documents/templates/{templateID}/versions/
Creates a new template version. Returns raw authored values without inheritance resolution or section expansion.
# Delete template
Source: https://docs.corti.ai/api-reference/guided-templates/delete-template
/api-reference/auto-generated-openapi.yml delete /documents/templates/{templateID}
Deletes a template and its versions. Returns 409 if other templates or sections inherit from this template.
# Delete template version
Source: https://docs.corti.ai/api-reference/guided-templates/delete-template-version
/api-reference/auto-generated-openapi.yml delete /documents/templates/{templateID}/versions/{versionID}
A published version cannot be deleted. When deleting a last remaining version of a template, simply create a new version again if needed.
# Get template
Source: https://docs.corti.ai/api-reference/guided-templates/get-template
/api-reference/auto-generated-openapi.yml get /documents/templates/{templateID}
Returns the template with its published version fully resolved (inheritance walked,
sections expanded with their own inheritance applied). To see raw authored
values without inheritance, use GET /documents/templates/{templateID}/versions/{versionID}.
# Get template version
Source: https://docs.corti.ai/api-reference/guided-templates/get-template-version
/api-reference/auto-generated-openapi.yml get /documents/templates/{templateID}/versions/{versionID}
Returns the raw authored template version without inheritance resolution or section expansion.
To see resolved content, use GET /documents/templates/{templateID} instead.
# List template versions
Source: https://docs.corti.ai/api-reference/guided-templates/list-template-versions
/api-reference/auto-generated-openapi.yml get /documents/templates/{templateID}/versions/
Returns raw authored template versions without inheritance resolution or section expansion.
To see resolved content, use GET /documents/templates/{templateID} instead.
# List templates
Source: https://docs.corti.ai/api-reference/guided-templates/list-templates
/api-reference/auto-generated-openapi.yml get /documents/templates/
Returns a list of templates and their metadata. Fetch a specific templateId to get the expanded sections.
Use query parameters to filter by language, region, specialty, label, publish status, or source.
# Publish template version
Source: https://docs.corti.ai/api-reference/guided-templates/publish-template-version
/api-reference/auto-generated-openapi.yml post /documents/templates/{templateID}/versions/{versionID}/publish
Sets this version as the published version of the template.
# Update template metadata
Source: https://docs.corti.ai/api-reference/guided-templates/update-template-metadata
/api-reference/auto-generated-openapi.yml patch /documents/templates/{templateID}
Updates the template's metadata fields (name, description, languages, regions, specialties, labels).
Generation content (template instructions, section composition) is managed through versions
and cannot be updated here.
# Create Interaction
Source: https://docs.corti.ai/api-reference/interactions/create-interaction
/api-reference/auto-generated-openapi.yml post /interactions/
Creates a new interaction.
# Delete Interaction
Source: https://docs.corti.ai/api-reference/interactions/delete-interaction
/api-reference/auto-generated-openapi.yml delete /interactions/{id}
Deletes an existing interaction.
# Get Existing Interaction
Source: https://docs.corti.ai/api-reference/interactions/get-existing-interaction
/api-reference/auto-generated-openapi.yml get /interactions/{id}
Retrieves a previously recorded interaction by its unique identifier (interaction ID).
# List All Interactions
Source: https://docs.corti.ai/api-reference/interactions/list-all-interactions
/api-reference/auto-generated-openapi.yml get /interactions/
Lists all existing interactions. Results can be filtered by encounter status and patient identifier.
# Update Interaction
Source: https://docs.corti.ai/api-reference/interactions/update-interaction
/api-reference/auto-generated-openapi.yml patch /interactions/{id}
Modifies an existing interaction by updating specific fields without overwriting the entire record.
# List Languages
Source: https://docs.corti.ai/api-reference/languages/list-languages
/api-reference/auto-generated-openapi.yml get /languages/
Returns a list of available languages with their enabled endpoints details.
# Delete Recording
Source: https://docs.corti.ai/api-reference/recordings/delete-recording
/api-reference/auto-generated-openapi.yml delete /interactions/{id}/recordings/{recordingId}
Delete a specific recording for a given interaction.
# Get Recording
Source: https://docs.corti.ai/api-reference/recordings/get-recording
/api-reference/auto-generated-openapi.yml get /interactions/{id}/recordings/{recordingId}
Retrieve a specific recording for a given interaction.
# List Recordings
Source: https://docs.corti.ai/api-reference/recordings/list-recordings
/api-reference/auto-generated-openapi.yml get /interactions/{id}/recordings/
Retrieve a list of recordings for a given interaction.
# Upload Recording
Source: https://docs.corti.ai/api-reference/recordings/upload-recording
/api-reference/auto-generated-openapi.yml post /interactions/{id}/recordings/
Upload a recording for a given interaction. There is a maximum limit of 120 minutes in audio duration and 150 MB in file size.
# Real-time conversational transcript generation and fact extraction (FactsR™)
Source: https://docs.corti.ai/api-reference/streams
WebSocket Secure (WSS) API Documentation for /streams endpoint
## Overview
The WebSocket Secure (WSS) `/streams` API enables real-time, bidirectional communication with the Corti system for interaction streaming. Clients can send and receive structured data, including transcripts and fact updates. Learn more about [FactsR™ here](/textgen/factsr/).
This documentation provides a structured guide for integrating the Corti WSS API for real-time interaction streaming.
This `/streams` endpoint supports real-time ambient documentation interactions and clinical decision support workflows.
* If you are looking for a stateless endpoint that is geared towards front-end dictation workflows you should use the [/transcribe WSS](/api-reference/transcribe)
* If you are looking for asynchronous ambient documentation interactions, then please refer to the [/documents endpoint](/api-reference/documents-classic/generate-document)
***
## 1. Establishing a Connection
Clients must initiate a WebSocket connection using the `wss://` scheme and provide a valid interaction ID in the URL.
When creating an interaction, the 200 response provides a `websocketUrl` for that interaction including the `tenant-name` as url parameter.
The authentication for the WSS streams requires in addition to the `tenant-name` parameter a `token` parameter to pass in the Bearer access token.
### Path Parameters
Unique interaction identifier
### Query Parameters
`eu` or `us`
Specifies the tenant context
Bearer \$token
```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const ACCESS_TOKEN = "";
const client = new CortiClient({
auth: {
accessToken: ACCESS_TOKEN,
},
});
```
```csharp title="C# .NET" theme={null}
using Corti;
// Replace these with your values
const string ACCESS_TOKEN = "";
var client = new CortiClient(
auth: CortiClientAuth.Bearer(accessToken: ACCESS_TOKEN)
);
```
```ts title="JavaScript" theme={null}
const streamSocket = await client.stream.connect({
id: ""
});
```
```csharp title="C# .NET" theme={null}
var stream = await client.CreateStreamApiAsync("");
await stream.ConnectAsync();
```
***
## 2. Handshake Responses
### 101 Switching Protocols
Indicates a successful WebSocket connection.
Upon successful connection, send a `config` message to define the configuration: Specify the input language and expected output preferences.
The config message must be sent within 10 seconds of the web socket being opened to prevent `CONFIG-TIMEOUT`, which will require establishing a new wss connection.
***
## 3. Sending Messages
### Configuration
Declare your `/streams` configuration using the message `"type": "config"` followed by defining the `"configuration": {}`.
Defining the type is required along with `transcription: primaryLanguage` and `mode: type and outputLocale` configuration parameters. The other parameters are optional for use, depending on your need and workflow.
Configuration notes:
* Clients must send a streams configuration message and wait for a response of type `CONFIG_ACCEPTED` before transmitting other data.
* If the configuration is not valid it will return `CONFIG_DENIED`.
* The configuration must be committed within 10 seconds of opening the WebSocket, else it will time-out with `CONFIG_NOT_PROVIDED`.
Define parameters for speech to text processing:
The primary spoken language for transcription. See supported languages codes and more information [here](/stt/languages).
Set to true to enable speaker diarization in mono channel audio
Note the legacy parameter `isDiarization` is still accepted
Set to true to enable transcription of a multi-channel audio stream.
List of participants with roles assigned to a channel
Audio channel number (e.g., 0 or 1)
Label for audio channel participant (e.g., Doctor, patient, or multiple)
Define facts or transcript as desired output, depending on workflow need:
Set as `facts` to receive structured facts output along with transcripts, or `transcription` to only receive transcript output
Output language for extracted `facts` (required for `type: "facts"`). See supported languages codes and more information [here](/stt/languages). (Note: This may be different than the `primaryLanguage` defined for transcript output; see details [here](/textgen/facts_realtime).)
Beta Rate at which fact generation should process and return results (optional for `type: "facts"`). Possible values: (empty), `fixed`, `fast_init`. If no value is set, the default is `fixed` and will trigger fact generation at the standard interval of around 60s. With `fast_init`, fact generation will follow a logarithmic curve, currently with the initial generation at roughly around 10s, then 20s, then 26s and continuously increasing interval length until the default 60s interval is reached.
Note: Increased fact processing rate can result in increased (near) duplicates, quantities and increased credit consumption.
With the optional parameter `retentionPolicy:none` the API will generate and return the transcripts and facts as expected, but the generated output will not be saved to the database. If the configuration option is omitted, then the default retention policy will apply and the output will be stored in the database.
Available options: `none`, `retain`
Define the audio format of the incoming audio stream - optional but recommended.
* When omitted, the server auto-detects the format from the first audio chunk using ffprobe. Supported audio will be processed. Unsupported audio returns an error, but in some cases might error silently.
* If provided (recommended), the provided MIME type must be supported and the audio must match the MIME type. An unsupported MIME type results in `CONFIG_REJECTED`. Audio that differs from the MIME type will return audio validation errors on the socket.
See more about supported formats [here](/stt/audio).
When true, enables audio quality and speech activity events to be sent over the WebSocket. Disabled by default.
The following events are supported:
* Speech quality issue detected / recovered
* Long silence detected / recovered
Don't like how Corti STT outputs certain words, phrases, or acronyms? Define `replacements` to have terms (single words or multi-word phrases) replaced in final text output with your preferred style.
Speech-to-text output to be replaced (case insensitive) Replacement text to be used final transcript A limit of 1,000 items is enforced on configuration validation.
Define words, terms, and phrases to be recognized by Corti speech-to-text. Especially useful for proper nouns (e.g., surnames), but also supportive of words not being recognized consistently.
Ordered list of words to be recognized (case sensitive)
Word to be recognized, defined in it's expected "written form"
A defined `term` is limited to a length of 50 charactersA limit of 1,000 items is enforced on configuration validation
#### Example
```json title="Raw message" theme={null}
{
"type": "config",
"configuration": {
"transcription": {
"primaryLanguage": "en",
"diarize": false,
"isMultichannel": false,
"participants": [
{
"channel": 0,
"role": "multiple"
}
]
},
"mode": {
"type": "facts",
"outputLocale": "en"
},
"retentionPolicy": "retain",
"audioFormat": "audio/ogg",
"audioEvents":{
"enabled": true
},
"replacements": [
{
"find": "BID",
"replace": "twice daily"
}
],
"keyterms": {
"terms": [
{
"term": "Corti"
}
]
}
}
}
```
```ts title="JavaScript" theme={null}
const configuration = {
transcription: {
primaryLanguage: "en",
diarize: false,
isMultichannel: false,
participants: [
{
channel: 0,
role: "multiple"
}
]
},
mode: {
type: "facts",
outputLocale: "en"
},
xCortiRetentionPolicy: "retain",
replacements: [
{
find: "BID",
replace: "twice daily"
}
],
keyterms: {
terms: [{ term: "Corti" }]
}
};
// When configuration is provided, connect() resolves after CONFIG_ACCEPTED (or throws on failure).
const streamSocket = await client.stream.connect({
id: "",
configuration
});
```
```csharp title="C# .NET" theme={null}
var stream = await client.CreateStreamApiAsync("");
// ConnectAsync(configuration) waits for CONFIG_ACCEPTED and throws on rejection.
await stream.ConnectAsync(new StreamConfig
{
Transcription = new StreamConfigTranscription
{
PrimaryLanguage = "en",
diarize = false,
IsMultichannel = false,
Participants = new List
{
new() { Channel = 0, Role = StreamConfigParticipantRole.Multiple },
},
},
Mode = new StreamConfigMode
{
Type = StreamConfigModeType.Facts,
OutputLocale = "en",
},
XCortiRetentionPolicy = StreamConfigXCortiRetentionPolicy.Retain,
Replacements = new List
{
new() { Find = "BID", Replace = "twice daily" },
},
Keyterms = new StreamConfigKeyterms
{
Terms = new List
{
new() { Term = "Corti" },
},
},
});
```
### Sending Audio
Ensure that your configuration was accepted before sending audio, and that the initial audio chunk is not too small as it needs to contain the headers to properly decode the audio.
We recommend sending audio in chunks of 250-500ms. In terms of buffering, the limit is 64000 bytes per chunk.
Audio data should be sent as raw binary without JSON wrapping.
A variety of common audio formats are supported; audio will be passed through a transcoder before speech-to-text processing. Similarly, specification of sample rate, depth or other audio settings is not required at this time.
See more details on supported audio formats [here](/stt/audio).
#### Channels, participants, and speakers
In most workflows, especially **in-person settings**, mono-channel audio should be used. If the microphone is a stereo-microphone, then ensure to set `isMultichannel: false` and audio will be converted to mono-channel, preventing duplicate transcripts from being returned.
In a telehealth workflow, or other **virtual setting**, the virtual audio may be on one channel (e.g., from webRTC) with audio from the microphone of the local client on a separate channel. In this scenario, define `isMultichannel: true` and assign each channel the relevant participant role (e.g., if the doctor is on the local client, then set that to channel 0 with participant defined as `doctor` and the virtual audio for patient on channel `defined as participant`patient\`).
**Diarization** is independent of audio channels and participant roles as it enables speaker separation for mono audio.
With configuration `diarize: true`, transcript segments will be assigned to automatically with first speaker identified being channel 0, second on channel 1, etc. If `diarize:false`, then transcript segments will all be assigned with `speakerId: -1`.
Read more [here](/stt/diarization).
```ts title="JavaScript" theme={null}
streamSocket.sendAudio(chunk); // method doesn't do the chunking
```
```csharp title="C# .NET" theme={null}
await stream.Send(audioChunkBytes); // method doesn't do the chunking
```
### Flush the Audio Buffer
To flush the audio buffer, forcing transcript segments to be returned over the web socket (e.g., when turning off or muting the microphone for the patient to share something private, not to be recorded, during the conversation), send a message -
```json title="Raw message" theme={null}
{
"type": "flush"
}
```
```ts title="JavaScript" theme={null}
streamSocket.sendFlush({ type: "flush" });
```
```csharp title="C# .NET" theme={null}
await stream.Send(new StreamFlushMessage());
```
The server will return text for audio sent before the `flush` message and then respond with messages -
```json theme={null}
{
"type": "flushed"
}
```
```json theme={null}
{
"type": "delta_usage",
"credits": 0.00116
}
```
`Delta usage` represents incremental credit consumption between recording initiation and `flush` events. Delta usage is approximate and may differ slightly from final `usage` sent after `end` message is processed (see [below](/api-reference/streams#usage)). Final, end session usage will be reflected in API billing.
The web socket will remain open after `flush` processing so recording can continue.
Beta FactsR generation (i.e., when working in `configuration.mode: facts`) will be triggered upon `flush` event. Facts will only be returned if the sliding transcript window, including the forced transcript segment after `flush`, contains relevant new information for FactsR.
Note: Too frequent triggering of `flush` can negatively impact FactsR, e.g. partial transcripts, increased (near) duplicates or fact quantities, and credits usage can increase.
Client side considerations:
1 If you rely on a `flush` event to separate data (e.g., for different sections in an EHR template), then be sure to receive the `flushed` event before moving on to the next data field.
2 When using a web browser `MediaRecorder` API, audio is buffered and only emitted at the configured timeslice interval. Therefore, *before* sending a `flush` message, call `MediaRecorder.requestData()` to force any remaining buffered audio on the client to be transmitted to the server. This ensures all audio reaches the server before the `flush` is processed.
### Ending the Session
To end the `/streams` session, send a message -
```json title="Raw message" theme={null}
{
"type": "end"
}
```
```ts title="JavaScript" theme={null}
streamSocket.sendEnd({ type: "end" });
```
```csharp title="C# .NET" theme={null}
await stream.Send(new StreamEndMessage());
```
This will signal the server to send any remaining transcript segments and facts (depending on `mode` configuration). Then, the server will send two messages -
```json theme={null}
{
"type": "usage",
"credits":0.1
}
```
```json theme={null}
{
"type": "ENDED"
}
```
Following the message type `ENDED`, the server will close the web socket.
You can at any time open the WebSocket again by sending the configuration.
***
## 4. Responses
### Configuration
Returned when sending a valid configuration. Response body will include the full configuration object to confirm values applied in the configuration.
Returned when sending a valid configuration.
The resolved configuration, including accepted client-defined values and server-applied defaults for parameters not defined in client configuration.
### Transcripts
Array of transcript segments. Each `transcript` message may carry one or more segments; with diarization, segments for different speakers can appear in the same message and may not be in chronological order (order by `time.start`).
Interaction ID that the transcript segments are associated with
Transcript text segments
Start time of the transcript segment in seconds
End time of the transcript segment in seconds
Indicates whether the transcript text results are final or interim (Note: only final transcripts are supported in Streams workflows)
Speaker identification (Note: value of `-1` is returned when diarization is disabled)
Audio channel number (e.g., 0 or 1)
```json Transcript response theme={null}
{
"type": "transcript",
"data": [
{
"id": "UUID",
"transcript": "Patient presents with fever and cough.",
"time": { "start": 1.71, "end": 11.296 },
"final": true,
"speakerId": -1,
"participant": { "channel": 0 }
}
]
}
```
Transcript output will automatically apply server-side default values documented [here](/stt/formatting). Configuration of output formatting is not supported on the `streams` endpoint at this time as it is with `transcribe`.
### Facts
Unique identifier for the fact
Text description of the fact
Categorization of the fact (e.g., "medical-history")
Deprecated. Response still includes groupID but will always be empty `""`
Default response is `false`. Use [PATCH /facts](/api-reference/facts/update-facts) to set to `true` if you want to keep track that a clinician discarded a fact.
Indicates the source of the fact (e.g., "core", "user"). Default response for LLM-generated is `core`. Use [PATCH /facts](/api-reference/facts/update-facts) if you want to keep track that a clinician edited or added a fact.
Timestamp when the fact was created
Timestamp when the fact was last updated via [PATCH /facts](/api-reference/facts/update-facts). Default response equals `createdAt`.
```json Fact response theme={null}
{
"type": "facts",
"fact": [
{
"id": "UUID",
"text": "Patient has a history of hypertension.",
"group": "medical-history",
"groupId": "" // deprecated,
"isDiscarded": false,
"source": "core",
"createdAt": "2024-02-28T12:34:56Z",
"updatedAt": ""
}
]
}
```
By default, incoming audio and returned data streams are persisted on the server, associated with the interactionId. You may query the interaction to retrieve the stored `recordings`, `transcripts`, and `facts` via the relevant REST endpoints. Audio recordings are saved as .webm format; transcripts and facts as json objects.
Data persistence can be disabled by Corti upon request when needed to support compliance with your applicable regulations and data handling preferences.
```ts expandable theme={null}
socket.on("message", (msg) => {
switch (msg.type) {
case "transcript":
console.log("Transcript:", msg.data);
break;
case "facts":
console.log("Facts:", msg.fact);
break;
case "flushed":
console.log("Flush complete");
break;
case "ENDED":
console.log("Stream ended");
socket.close();
break;
case "usage":
console.log("Credits used:", msg.credits);
break;
case "error":
console.error("Server error:", msg.error);
break;
}
});
```
```csharp title="C# .NET" expandable theme={null}
stream.StreamTranscriptMessage.Subscribe(message =>
{
Console.WriteLine($"Transcript: {message.Data.Text}");
});
stream.StreamFactsMessage.Subscribe(message =>
{
Console.WriteLine("Facts received");
});
stream.StreamFlushedMessage.Subscribe(_ =>
{
Console.WriteLine("Flush complete");
});
stream.StreamEndedMessage.Subscribe(_ =>
{
Console.WriteLine("Stream ended");
});
stream.StreamUsageMessage.Subscribe(message =>
{
Console.WriteLine($"Credits used: {message.Credits}");
});
stream.StreamErrorMessage.Subscribe(message =>
{
Console.Error.WriteLine($"Server error: {message.Error.Title} ({message.Error.Status})");
});
```
### Audio Events
Server message indicating an audio quality or speech activity event
The type of audio quality or speech activity event.
Possible values: `speechQualityIssueDetected`, `speechQualityIssueRecovered`, `longSilenceDetected`, `longSilenceRecovered`
Audio channel identifier
Start time of the event in milliseconds
```json Audio Event response theme={null}
{
"type": "audioEvent",
"data": {
"event": "speechQualityIssueDetected",
"channel": 0,
"startTimeMs": 4200
}
}
```
### Flushed
Returned by server, after processing `flush` event from client, to return transcript segments
```json theme={null}
{
"type":"flushed"
}
```
### Usage
Returned by server, after processing `flush` event from client, to convey amount of credits consumed since recording started. Delta usage is approximate and may differ slightly from final `usage` sent after `end` message is processed.
```json theme={null}
{
"type": "delta_usage",
"credits": 0.00116
}
```
Returned by server, after processing `end` event from client, to convey amount of credits consumed
```json theme={null}
{
"type":"usage",
"credits":0.1
}
```
### Ended
Returned by server, after processing `end` event from client, before closing the web socket
```json theme={null}
{
"type":"ENDED"
}
```
***
## 5. Error Handling
For the full catalog of error codes, real `reason` strings, and SDK / REST-specific behavior, see the [Errors reference](/api-reference/errors).
In case of an invalid or missing interaction ID, the server will return an error before opening the WebSocket.
In case of an invalid configuration, the server will return one of the following errors:
Returned when sending an invalid configuration.
Possible errors: `CONFIG_DENIED`, `CONFIG_NOT_PROVIDED`, `CONFIG_ALREADY_RECEIVED`, `CONFIG_MISSING`
The reason the configuration is invalid.
The interaction ID.
Once configuration has been accepted and the session is running, you may encounter runtime or application-level errors. These are sent as JSON objects with the following structure:
```json theme={null}
{
"type": "error",
"error": {
"requestid": "",
"details": "error details"
}
}
```
```ts title="JavaScript" expandable theme={null}
try {
const socket = await client.stream.connect({
id: interactionId,
configuration: {
transcription: { primaryLanguage: "en", participants: [{ channel: 0, role: "doctor" }] },
mode: { type: "facts", outputLocale: "en" },
},
});
socket.on("error", (err) => {
// Network errors and reconnect failures
console.error("Socket error:", err.message);
});
socket.on("message", (msg) => {
if (msg.type === "error") {
// Server-sent runtime error (e.g. audio format issue)
console.error("Server error:", msg.error);
}
});
} catch (err) {
// CONFIG_DENIED, CONFIG_TIMEOUT, CONFIG_MISSING, or connection failure
console.error("Connect failed:", err.message);
}
```
```csharp title="C# .NET" expandable theme={null}
var stream = await client.CreateStreamApiAsync(interactionId);
stream.ExceptionOccurred.Subscribe(ex =>
{
// Network errors, reconnect failures
Console.Error.WriteLine($"Socket error: {ex.Message}");
});
stream.StreamErrorMessage.Subscribe(message =>
{
// Server-sent runtime error (e.g. audio format issue)
Console.Error.WriteLine($"Server error: {message.Error.Title} ({message.Error.Status})");
});
try
{
// ConnectAsync waits for CONFIG_ACCEPTED and throws if configuration is denied
await stream.ConnectAsync(new StreamConfig
{
Transcription = new StreamConfigTranscription
{
PrimaryLanguage = "en",
Participants = new[] { new StreamConfigParticipant { Channel = 0, Role = StreamConfigParticipantRole.Doctor } },
},
Mode = new StreamConfigMode { Type = StreamConfigModeType.Facts, OutputLocale = "en" },
});
}
catch (InvalidOperationException ex)
{
// CONFIG_DENIED, CONFIG_TIMEOUT, CONFIG_MISSING, or connection failure
Console.Error.WriteLine($"Connect failed: {ex.Message}");
throw;
}
```
# Get Template
Source: https://docs.corti.ai/api-reference/templates-classic/get-template
/api-reference/auto-generated-openapi.yml get /templates/{key}
Retrieves template by key.
# List Template Sections
Source: https://docs.corti.ai/api-reference/templates-classic/list-template-sections
/api-reference/auto-generated-openapi.yml get /templateSections/
Retrieves a list of template sections with optional filters for organization and language.
# List Templates
Source: https://docs.corti.ai/api-reference/templates-classic/list-templates
/api-reference/auto-generated-openapi.yml get /templates/
Retrieves a list of templates with optional filters for organization, language, and status.
# Real-time stateless dictation
Source: https://docs.corti.ai/api-reference/transcribe
WebSocket Secure (WSS) API Documentation for /transcribe endpoint
## Overview
The WebSocket Secure (WSS) `/transcribe` API enables real-time, bidirectional communication with the Corti system for stateless speech to text. Clients can send and receive structured data, including transcripts and detected commands.
This documentation provides a comprehensive guide for integrating these capabilities.
This `/transcribe` endpoint supports real-time stateless dictation.
* If you are looking for real-time ambient documentation interactions, you should use the [/streams WSS](/api-reference/streams)
* If you are looking for transcript generation based on a pre-recorded audio file, then please refer to the [/transcripts endpoint](/api-reference/transcripts/create-transcript)
***
## 1. Establishing a Connection
Clients must initiate a WebSocket connection using the `wss://` scheme.
The authentication for the WSS streams requires in addition to the `tenant-name` parameter a `token` parameter to pass in the Bearer access token.
### Query Parameters
`eu` or `us`
Specifies the tenant context
Bearer \$token
```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const ACCESS_TOKEN = "";
const client = new CortiClient({
auth: {
accessToken: ACCESS_TOKEN,
},
});
```
```csharp title="C# .NET" theme={null}
using Corti;
// Replace these with your values
const string ACCESS_TOKEN = "";
var client = new CortiClient(
auth: CortiClientAuth.Bearer(accessToken: ACCESS_TOKEN)
);
```
```bash title="cURL" theme={null}
# Replace these with your values
ACCESS_TOKEN=""
ENVIRONMENT=""
TENANT=""
curl --request GET \
--url "wss://api.${ENVIRONMENT}.corti.app/audio-bridge/v2/transcribe?tenant-name=${TENANT}&token=Bearer%20${ACCESS_TOKEN}"
```
```ts title="JavaScript" theme={null}
const transcribeSocket = await client.transcribe.connect();
```
```csharp title="C# .NET" theme={null}
var transcribe = await client.CreateTranscribeApiAsync();
await transcribe.ConnectAsync();
```
***
## 2. Handshake Response
### 101 Switching Protocols
Indicates a successful WebSocket connection.
Upon successful connection, send a `config` message to define the configuration: Specify the input language and expected output preferences.
The config message must be sent within 10 seconds of the web socket being opened to prevent `CONFIG-TIMEOUT`, which will require establishing a new wss connection.
***
## 3. Sending Messages
### Configuration
Declare your `/transcribe` configuration using the message `"type": "config"` followed by defining the `"configuration": {...}`.
Defining the type is required along with the `primaryLanguage` configuration parameter. The other parameters are optional for use, depending on your need and workflow.
Configuration Notes:
* The configuration must be committed within 10 seconds of opening the WebSocket, else it will time-out with `CONFIG_TIMEOUT`.
* Clients must wait for a response of type `CONFIG_ACCEPTED` before transmitting other data.
* If the configuration is not valid it will return `CONFIG_DENIED`.
The locale of the primary spoken language.
See supported languages codes and more information [here](/stt/languages).
When true, interim (preview) transcript results (`"isFinal"=false`) will be returned with lower latency than final transcript results.
When true, converts spoken punctuation such as `period` or `slash` into `.`or `/`.
Read more about supported punctuation [here](/stt/punctuation).
When true, automatically punctuates and capitalizes in the final transcript.
Spoken and Automatic Punctuation are mutually exclusive - only one should be set to true in a given configuration request. If both are included and set to `true`, then `spokenPunctuation` will take precedence and override `automaticPunctuation`.
Define dictation commands to be registered and detected during audio recording.
Read more about commands, with examples, [here](/stt/commands).
Unique value to identify the command. This, along with the command phrase, will be returned by the API when the command is recognized during dictation.
One or more word sequence(s) that can be spoken to trigger the command. At least one phrase is required per command.
Placeholders that can (optionally) be added in `phrases` to provide flexibility and extensibility for triggering commands.
Define the variable used in command phrase here.
Possible values:
* `enum` - use to define a list of values that can be recognized for a given command phrase
* `wildcard` - use for any free text utterance to be recognized in a command phrase
When using `wildcard` type, the phrase must include a literal trigger word before the variable, and multiple wildcard variables in one phrase must be separated by a non-empty literal string.
Required when `type` is `"enum"`. List of values that should be recognized for the defined variable. Not used (and ignored) when `type` is `"wildcard"`.
Define preferences for output formatting using the `enum` options described below. Formatting configuration is optional, and one or more fields can be defined individually. When a property is not provided for a given field, then the values listed as `default` will be applied automatically.
Read more about formatting options, including how localization is handled per language, and detailed examples, [here](/stt/formatting).
* Default option: `locale:long`
* Alternative options: `locale:medium`, `locale:short`, `iso`, `as_dictated`
* Default option: `locale`
* Alternative options: `h24`, `h12`
* Default option: `numerals_above_nine`
* Alternative options: `numerals`, `as_dictated`
Localization of numbers applied automatically for thousands and decimal separators
* Default option: `abbreviated`
* Alternative options: `as_dictated`
[Click here](/stt/formatting#units-and-measurements) to see a full list of supported units and measurements
* Default option: `numerals`
* Alternative options: `as_dictated`
* Default option: `numerals_above_nine`
* Alternative options: `numerals`, `as_dictated`
Define the audio format of the incoming audio stream - optional but recommended.
* When omitted, the server auto-detects the format from the first audio chunk using ffprobe. Supported audio will be processed. Unsupported return an error but might in some cases error silently.
* If provided (recommended), the provided MIME type must be supported and the audio must match the MIME type. An unsupported MIME type results in `CONFIG_REJECTED`. Audio that differs from the MIME type will return audio validation errors on the socket.
See more about supported formats [here](/stt/audio).
Enables audio quality and speech activity events to be sent over the WebSocket. The following events are supported:
* Speech quality issue detected / recovered
* Long silence detected / recovered
When true, enables audio quality and speech activity events to be sent over the WebSocket. Disabled by default.
Don't like how Corti STT outputs certain words, phrases, or acronyms? Define `replacements` to have terms (single words or multi-word phrases) replaced in final text output with your preferred style.
Speech-to-text output to be replaced (case insensitive) Replacement text to be used final transcript A limit of 1,000 items is enforced on configuration validation.
Define words, terms, and phrases to be recognized by Corti speech-to-text. Especially useful for proper nouns (e.g., surnames), but also supportive of words not being recognized consistently.
Ordered list of words to be recognized (case sensitive)
Word to be recognized, defined in it's expected "written form"
A defined `term` is limited to a length of 50 charactersA limit of 1,000 items is enforced on configuration validation
#### Example
Here is an example configuration for transcription of dictated audio in English with spoken punctuation enabled, two commands defined, and (default) formatting options defined:
```json title="wss:/transcribe configuration example" theme={null}
{
"type": "config",
"configuration":{
"primaryLanguage": "en",
"interimResults": true,
"spokenPunctuation": true,
"commands": [
{
"id": "next_section",
"phrases": ["next section", "go to next section"]
},
{
"id": "insert_template",
"phrases": ["insert my {template_name} template", "insert {template_name} template"],
"variables": [
{
"key": "template_name",
"type": "enum",
"enum": ["soap", "radiology", "referral"]
}
]
},
{
"id": "select_text",
"phrases": ["select {text}"],
"variables": [
{
"key": "text",
"type": "wildcard"
}
]
},
],
"formatting": {
"dates": "locale:long",
"times": "locale",
"numbers": "numerals_above_nine",
"measurements": "abbreviated",
"numericRanges": "numerals",
"ordinals": "numerals_above_nine"
},
"audioFormat": "audio/ogg",
"audioEvents":{
"enabled": true
},
"replacements": [
{
"find": "BID",
"replace": "twice daily"
}
],
"keyterms": {
"terms": [
{
"term": "Corti"
}
]
}
}
}
```
```ts title="JavaScript" theme={null}
const configuration = {
primaryLanguage: "en",
spokenPunctuation: true,
commands: [
{
id: "next_section",
phrases: ["next section", "go to next section"]
},
{
id: "insert_template",
phrases: ["insert my {template_name} template", "insert {template_name} template"],
variables: [
{
key: "template_name",
type: "enum",
enum: ["soap", "radiology", "referral"]
}
]
},
{
id: "select_text",
phrases: ["select {text}"],
variables: [
{
key: "text",
type: "wildcard"
}
]
},
],
audioEvents: {
enabled: true
},
replacements: [
{
find: "BID",
replace: "twice daily"
}
],
keyterms: {
terms: [{ term: "Corti" }]
}
};
const transcribeSocket = await client.transcribe.connect(
{ configuration }
);
```
```csharp title="C# .NET" theme={null}
var transcribe = await client.CreateTranscribeApiAsync();
await transcribe.ConnectAsync(new TranscribeConfig
{
PrimaryLanguage = "en",
SpokenPunctuation = true,
Commands = new List
{
new()
{
Id = "next_section",
Phrases = new List { "next section", "go to next section" },
},
new()
{
Id = "insert_template",
Phrases = new List
{
"insert my {template_name} template",
"insert {template_name} template",
},
Variables = new List
{
new()
{
Key = "template_name",
Type = TranscribeCommandVariableType.Enum,
Enum = new List { "soap", "radiology", "referral" },
},
},
},
new()
{
Id = "select_text",
Phrases = new List { "select {text}" },
Variables = new List
{
new()
{
Key = "text",
Type = TranscribeCommandVariableType.Wildcard,
},
},
},
},
AudioEvents = new TranscribeAudioEventsConfig { Enabled = true },
Replacements = new List
{
new() { Find = "BID", Replace = "twice daily" },
},
Keyterms = new TranscribeConfigKeyterms
{
Terms = new List
{
new() { Term = "Corti" },
},
},
});
```
### Sending Audio
Ensure that your configuration was accepted before sending audio, and that the initial audio chunk is not too small as it needs to contain the headers to properly decode the audio.
We recommend sending audio in chunks of 250-500ms. In terms of buffering, the limit is 64000 bytes per chunk.
Audio data should be sent as raw binary without JSON wrapping.
A variety of common audio formats are supported; audio will be passed through a transcoder before speech-to-text processing. Similarly, specification of sample rate, depth or other audio settings is not required at this time.
See more details on supported audio formats [here](/stt/audio).
```ts title="JavaScript" theme={null}
transcribeSocket.sendAudio(audioChunk); // method doesn't do the chunking
```
```csharp title="C# .NET" theme={null}
await transcribe.Send(audioChunkBytes); // method doesn't do the chunking
```
### Flush the Audio Buffer
To flush the audio buffer, forcing transcript segments and detected commands to be returned over the web socket (e.g., when turning off or muting the microphone in a "hold-to-talk" dictation workflow, or in applications that support mic "go to sleep"), send a message -
```json title="Raw message" theme={null}
{
"type":"flush"
}
```
```ts title="JavaScript" theme={null}
transcribeSocket.sendFlush({ type: "flush" });
```
```csharp title="C# .NET" theme={null}
await transcribe.Send(new TranscribeFlushMessage());
```
The server will return text/commands for audio sent before the `flush` message and then respond with messages -
```json theme={null}
{
"type": "flushed"
}
```
```json theme={null}
{
"type": "delta_usage",
"credits": 0.00116
}
```
`Delta usage` represents incremental credit consumption between recording initiation and `flush` events. Delta usage is approximate and may differ slightly from final `usage` sent after `end` message is processed (see [below](/api-reference/transcribe#usage)). Final, end session usage will be reflected in API billing.
The web socket will remain open after `flush` processing so dictation can continue.
Client side considerations:
1 If you rely on a `flush` event to separate data (e.g., for different sections in an EHR template), then be sure to receive the `flushed` event before moving on to the next data field.
2 When using a web browser `MediaRecorder` API, audio is buffered and only emitted at the configured timeslice interval. Therefore, *before* sending a `flush` message, call `MediaRecorder.requestData()` to force any remaining buffered audio on the client to be transmitted to the server. This ensures all audio reaches the server before the `flush` is processed.
### Ending the Session
To end the `/transcribe` session, send a message -
```json title="Raw message" theme={null}
{
"type": "end"
}
```
```ts title="JavaScript" theme={null}
transcribeSocket.sendEnd({ type: "end" });
```
```csharp title="C# .NET" theme={null}
await transcribe.Send(new TranscribeEndMessage());
```
This will signal the server to send any remaining transcript segments and/or detected commands. Then, the server will send two messages -
```json theme={null}
{
"type": "usage",
"credits": 0.1
}
```
```json theme={null}
{
"type": "ended"
}
```
Following the message type `ended`, the server will close the web socket.
***
## 4. Responses
### Configuration
Returned when sending a valid configuration.
Returned when sending a valid configuration.
The resolved configuration, including accepted client-defined values and server-applied defaults for parameters not defined in client configuration.
### Transcripts
Server message indicated recognized speech to text
Transcript segment with punctuations applied and command phrases removed
The raw transcript without spoken punctuation applied and without command phrases removed
Start time of the transcript segment in seconds
End time of the transcript segment in seconds
If false, then interim transcript result
```json Transcript response theme={null}
{
"type": "transcript",
"data": {
"text": "patient reports mild chest pain.",
"rawTranscriptText": "patient reports mild chest pain period",
"start": 0.0,
"end": 3.2,
"isFinal": true
}
}
```
**[Click here](/stt/best-practices-transcribe)** for detailed guide on how to properly insert transcript segments with proper handling of whitespace, interim vs. final results, and `text` vs. `rawTranscriptText` fields.
### Commands
Server message indicating a recognized Command
To identify the command when it gets detected and returned over the WebSocket
Command variables as key-value pairs
The raw transcript without spoken punctuation applied and without command phrases removed
Start time of the transcript segment in seconds
End time of the transcript segment in seconds
```json Command response theme={null}
{
"type": "command",
"data": {
"id": "insert_template",
"variables": {
"template_name": "radiology"
},
"rawTranscriptText": "insert my radiology template",
"start": 2.3,
"end": 2.9
}
}
```
### Audio Events
Server message indicating an audio quality or speech activity event
The type of audio quality or speech activity event.
Possible values: `speechQualityIssueDetected`, `speechQualityIssueRecovered`, `longSilenceDetected`, `longSilenceRecovered`
Audio channel identifier
Start time of the event in milliseconds
```json Audio Event response theme={null}
{
"type": "audioEvent",
"data": {
"event": "speechQualityIssueDetected",
"channel": 0,
"startTimeMs": 4200
}
}
```
### Flushed
Returned by server, after processing `flush` event from client, to return transcript segments/ detected commands
```json theme={null}
{
"type": "flushed"
}
```
### Usage
Returned by server, after processing `flush` event from client, to convey amount of credits consumed since recording started. Delta usage is approximate and may differ slightly from final `usage` sent after `end` message is processed.
```json theme={null}
{
"type": "delta_usage",
"credits": 0.00116
}
```
Returned by server, after processing `end` event from client, to convey amount of credits consumed
```json theme={null}
{
"type": "usage",
"credits": 0.1
}
```
### Ended
Returned by server, after processing `end` event from client, before closing the web socket
```json theme={null}
{
"type": "ended"
}
```
### Subscribe to messages in SDK
```ts expandable theme={null}
socket.on("message", (msg) => {
switch (msg.type) {
case "transcript":
console.log("Transcript:", msg.data.text, "final:", msg.data.isFinal);
break;
case "command":
console.log("Command:", msg.data.id, msg.data.variables);
break;
case "flushed":
console.log("Flush complete");
break;
case "ended":
console.log("Session ended");
socket.close();
break;
case "usage":
console.log("Credits used:", msg.credits);
break;
case "error":
console.error("Server error:", msg.error);
break;
}
});
```
```csharp title="C# .NET" expandable theme={null}
transcribe.TranscribeTranscriptMessage.Subscribe(message =>
{
Console.WriteLine($"Transcript: {message.Data.Text}");
});
transcribe.TranscribeCommandMessage.Subscribe(message =>
{
Console.WriteLine($"Command: {message.Data.Id}");
});
transcribe.TranscribeFlushedMessage.Subscribe(_ =>
{
Console.WriteLine("Flush complete");
});
transcribe.TranscribeEndedMessage.Subscribe(_ =>
{
Console.WriteLine("Session ended");
});
transcribe.TranscribeUsageMessage.Subscribe(message =>
{
Console.WriteLine($"Credits used: {message.Credits}");
});
transcribe.TranscribeErrorMessage.Subscribe(message =>
{
Console.Error.WriteLine($"Server error: {message.Error.Title} ({message.Error.Status})");
});
```
***
## 5. Error Handling
For the full catalog of error codes, real `reason` strings, and SDK / REST-specific behavior, see the [Errors reference](/api-reference/errors).
Returned when sending an invalid configuration.
Possible errors: `CONFIG_DENIED`, `CONFIG_TIMEOUT`, `CONFIG_ALREADY_RECEIVED`, `CONFIG_MISSING`
The reason the configuration is invalid.
The session ID.
Once configuration has been accepted and the session is running, you may encounter runtime or application-level errors. These are sent as JSON objects with the following structure:
```json theme={null}
{
"type": "error",
"error": {
"requestid": "",
"details": "error details"
}
}
```
### Handle errors in SDK
With the recommended approach (passing configuration as part of `connect`), configuration errors are raised during `connect` (the call fails). Runtime errors are emitted via the error event; you can also inspect the original message in the message handler.
```ts title="JavaScript" expandable theme={null}
try {
const socket = await client.transcribe.connect({
configuration: {
primaryLanguage: "en",
automaticPunctuation: true,
},
});
socket.on("error", (err) => {
// Network errors and reconnect failures
console.error("Socket error:", err.message);
});
socket.on("message", (msg) => {
if (msg.type === "error") {
// Server-sent runtime error
console.error("Server error:", msg.error);
}
});
} catch (err) {
// CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
console.error("Connect failed:", err.message);
}
```
```csharp title="C# .NET" expandable theme={null}
var transcribe = await client.CreateTranscribeApiAsync();
transcribe.ExceptionOccurred.Subscribe(ex =>
{
Console.Error.WriteLine($"Socket error: {ex.Message}");
});
transcribe.TranscribeErrorMessage.Subscribe(message =>
{
Console.Error.WriteLine($"Server error: {message.Error.Title} ({message.Error.Status})");
});
try
{
await transcribe.ConnectAsync(new TranscribeConfig
{
PrimaryLanguage = "en",
AutomaticPunctuation = true,
});
}
catch (InvalidOperationException ex)
{
// CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
Console.Error.WriteLine($"Connect failed: {ex.Message}");
throw;
}
```
# Create Transcript
Source: https://docs.corti.ai/api-reference/transcripts/create-transcript
/api-reference/auto-generated-openapi.yml post /interactions/{id}/transcripts/
Create a transcript from an audio file uploaded to the interaction via `/recordings` endpoint. Each interaction may have more than one audio file and transcript associated with it. Audio files up to 120 minutes in total audio duration and 150 MB in size may be used.
By default, requests will process synchronously for 25 seconds before timeout, upon which processing will continue asynchronously. Set the `async` parameter to true to receive the location header immediately and process the request asynchronously. Read more [here](https://docs.corti.ai/stt/transcripts).
# Delete Transcript
Source: https://docs.corti.ai/api-reference/transcripts/delete-transcript
/api-reference/auto-generated-openapi.yml delete /interactions/{id}/transcripts/{transcriptId}
Deletes a specific transcript associated with an interaction.
# Get Transcript
Source: https://docs.corti.ai/api-reference/transcripts/get-transcript
/api-reference/auto-generated-openapi.yml get /interactions/{id}/transcripts/{transcriptId}
Retrieve a transcript from a specific interaction. Each interaction may have more than one transcript associated with it. Use the List Transcript request (`GET /interactions/{id}/transcripts/`) to see all transcriptIds available for the interaction.
The client can poll this Get Transcript endpoint (`GET /interactions/{id}/transcripts/{transcriptId}/status`) for transcript status changes: - `200 OK` with status `processing`, `completed`, or `failed` - `404 Not Found` if the `interactionId` or `transcriptId` are invalid
Status of `completed` indicates the transcript is finalized. If the transcript is retrieved while status is `processing`, then it will be incomplete.
# Get Transcript Status
Source: https://docs.corti.ai/api-reference/transcripts/get-transcript-status
/api-reference/auto-generated-openapi.yml get /interactions/{id}/transcripts/{transcriptId}/status
Poll for transcript creation status. Status of `completed` indicates the transcript is finalized. If the transcript is retrieved while status is `processing`, then it will be incomplete. Status of `failed` indicate the transcript was not created successfully; please retry.
# List Transcripts
Source: https://docs.corti.ai/api-reference/transcripts/list-transcripts
/api-reference/auto-generated-openapi.yml get /interactions/{id}/transcripts/
Retrieves a list of transcripts for a given interaction.
# Welcome to the Corti API Reference
Source: https://docs.corti.ai/api-reference/welcome
AI platform for healthcare developers
This API Reference provides detailed specifications for integrating with the Corti API, enabling organizations to build bespoke healthcare AI solutions that meet their specific needs.
JavaScript and C# .NET SDKs with quickstart guides
Download the Corti API Postman collection to start building
***
#### Most Popular
Detailed spec for real-time dictation and voice commands
Detailed spec for real-time conversational intelligence
Start here for opening a contextual messaging thread
Detailed spec for creating an interaction and setting appropriate context
Attach an audio file to the interaction
Start or continue your contextual chat and agentic tasks
Convert audio files to text
Create one to many documents for an interaction
Retrieve information about all available experts for use with your agents
OpenAI-compatible chat, responses, and embeddings on EU infrastructure
***
### More resources
| Resource | Description |
| :--------------------------------------------- | :----------------------------------------------------------------------------------- |
| [Release notes](/release-notes/overview/) | Upcoming changes and recent API, language model, and app updates. |
| [Support center ↗](mailto:help@corti.ai) | Help articles and documentation, contact the Corti team, and manage support tickets. |
| [Compliance & Trust ↗](https://trust.corti.ai) | Detailed compliance standards and security certifications. |
[Contact us](mailto:help@corti.ai) if you need more information about the Corti API.
# API Reference
Source: https://docs.corti.ai/assistant/api-reference
Overview of the Embedded API surface, method groups, error model, and common workflows.
This page gives you the structure of the Embedded API and links to the individual method reference pages.
**Applies to all integration methods**
The Embedded API is available through the [Web Component API](/assistant/web-component-api), [Window API](/assistant/window-api), and [PostMessage API](/assistant/postmessage-api). The underlying capabilities and error model are shared, but the invocation shape differs by integration method.
**Examples use the Web Component API shape**
The linked method reference pages use the Web Component or React API shape for consistency. When you use the Window API or PostMessage, the same operation often keeps the same payload but changes how it is invoked.
## How to use this reference
Use the method pages in the sidebar when you need payload details, validation rules, error cases, or return types for a specific method.
The API Reference is grouped into:
* **Application** methods for authentication, app-level configuration, navigation, account state, template lookup, and mobile companion pairing
* **Interaction** methods for interaction setup, session defaults, contextual facts, and recording control
* **Deprecated** methods within those groups when you still need the legacy configuration flow during the deprecation period
If you need configuration behavior, scenarios, or migration help, use these pages alongside the method reference:
* [Configuration Scenarios](/assistant/configuration-scenarios)
* [Config Migration Guide](/assistant/configuration-migration)
* [Scheduled Deprecations](/assistant/deprecation-timeline)
## Method groups
### Application
* [auth()](/assistant/api/auth) - Authenticate the embedded user session
* [configureApp()](/assistant/api/configure-app) - Apply app-level UI, appearance, locale, and network configuration
* [navigate()](/assistant/api/navigate) - Navigate to an internal Assistant route
* [setCredentials()](/assistant/api/set-credentials) - Update the authenticated user's credentials
* [getStatus()](/assistant/api/get-status) - Read current application and interaction state
* [getTemplates()](/assistant/api/get-templates) - Retrieve available templates for the authenticated user
* [showDeviceLinkQR()](/assistant/api/show-device-link-qr) - Display the QR pairing flow for the Corti mobile companion app
* [configure() (Deprecated)](/assistant/api/configure) - Legacy app configuration structure
### Interaction
* [createInteraction()](/assistant/api/create-interaction) - Create a new interaction session
* [setInteractionOptions()](/assistant/api/set-interaction-options) - Apply interaction-level defaults and options
* [addFacts()](/assistant/api/add-facts) - Add contextual facts to the current interaction
* [startRecording()](/assistant/api/start-recording) - Start recording in the current session
* [stopRecording()](/assistant/api/stop-recording) - Stop recording in the current session
* [configureSession() (Deprecated)](/assistant/api/configure-session) - Legacy session-level defaults
## Error codes
All API actions may return errors with the following structure:
```typescript theme={null}
{
message: string,
code: "UNAUTHORIZED" | "NOT_READY" | "NOT_FOUND" | "INVALID_PAYLOAD" | "INTERNAL_ERROR",
details?: unknown,
}
```
| Code | Description | Common causes |
| ----------------- | ------------------------------------------------ | ------------------------------------------------------------------------------- |
| `UNAUTHORIZED` | User is not authenticated or the session expired | `auth()` was not called, or tokens are no longer valid |
| `NOT_READY` | A required precondition is missing | No active interaction, not in recording context, or a required view is not open |
| `NOT_FOUND` | A requested resource does not exist | Invalid interaction ID, user not found, or template not found |
| `INVALID_PAYLOAD` | Request payload validation failed | Missing required fields, invalid formats, unsupported values |
| `INTERNAL_ERROR` | Unexpected client or server failure | Retry the request or contact support if the issue persists |
## Workflows
Workflow examples now live on [API Reference Workflows](/assistant/api-reference-workflows).
Use that page when you want end-to-end examples that combine multiple methods into a realistic integration flow.
## Configuration-specific reference
Configuration-specific lookup data such as supported interface languages, dictation language codes, and string override keys lives in the configuration documentation rather than in the method reference.
Use these pages when you need those details:
* [Supported Values](/assistant/configuration-values)
* [Configuration Scenarios](/assistant/configuration-scenarios)
* [Config Migration Guide](/assistant/configuration-migration)
## Related documentation
* [Web Component API](/assistant/web-component-api) - Package API and integration overview
* [Window API](/assistant/window-api) - Same-origin method invocation
* [PostMessage API](/assistant/postmessage-api) - Cross-frame method invocation
* [Welcome](/assistant/welcome) - Overview of the embedded Assistant
Please [contact us](mailto:help@corti.ai) if you need help with a specific API method or integration pattern.
# API Reference Workflows
Source: https://docs.corti.ai/assistant/api-reference-workflows
End-to-end Embedded API workflow examples that combine multiple methods into realistic integration flows.
Use this page when you want end-to-end examples that combine multiple Embedded API methods into a realistic integration flow.
These workflows complement the individual method reference pages. Use the
method pages when you need payload details, validation rules, or return shapes
for one specific method.
## Initialize and start recording
In the examples below, `api` refers to your Embedded API instance.
* Web Component / vanilla: `const api = document.querySelector("corti-embedded");`
* React: `const api = useCortiEmbeddedApi(cortiRef);`
`navigate({ path: ... })` is used for cross-surface consistency.
* On `@corti/embedded-web` versions earlier than `0.3.0`, use
`navigate("/session/...")`.
* On `@corti/embedded-web@0.3.0` and later, both shapes are accepted.
```typescript theme={null}
try {
// 1. Authenticate
const user = await api.auth({
access_token: "...",
refresh_token: "...",
id_token: "...",
token_type: "Bearer",
});
console.log("Authenticated as:", user.email);
// 2. Configure app-level settings (optional)
await api.configureApp({
locale: { dictationLanguage: "en" },
});
// 3. Configure interaction defaults (optional)
await api.setInteractionOptions({
mode: {
fallback: "virtual",
options: ["in-person", "virtual"],
},
});
// 4. Create interaction
const interaction = await api.createInteraction({
encounter: {
identifier: "enc-123",
status: "planned",
type: "ambulatory",
period: { startedAt: new Date().toISOString() },
title: "Patient Visit",
},
});
// 5. Navigate to the session
await api.navigate({ path: `/session/${interaction.id}` });
// 6. Start recording
await api.startRecording();
} catch (error) {
console.error("Workflow failed:", error.message, error.code);
}
```
## Handle recording startup errors
```typescript theme={null}
async function startRecordingWithRetry() {
try {
await api.startRecording();
} catch (error) {
switch (error.code) {
case "UNAUTHORIZED":
await reauthenticate();
await api.startRecording();
break;
case "NOT_READY":
if (error.message.includes("createInteraction")) {
await api.createInteraction({
encounter: {
identifier: `enc-${Date.now()}`,
status: "planned",
type: "ambulatory",
period: { startedAt: new Date().toISOString() },
},
});
await api.startRecording();
}
break;
default:
console.error("Unexpected error:", error);
}
}
}
```
## Complete session lifecycle
```typescript theme={null}
let currentInteractionId = null;
async function initializeSession() {
await api.auth({
/* tokens */
});
await api.configureApp({
ui: {
aiChat: true,
},
});
await api.setInteractionOptions({
mode: {
fallback: "virtual",
options: ["in-person", "virtual"],
},
spokenLanguage: {
fallback: "en-US",
},
});
}
async function startNewInteraction(encounterData) {
const interaction = await api.createInteraction({
encounter: encounterData,
});
currentInteractionId = interaction.id;
await api.addFacts([
{ text: "Chief complaint: Headache", group: "symptoms" },
]);
await api.navigate({ path: `/session/${interaction.id}` });
await api.startRecording();
return interaction;
}
async function endInteraction() {
await api.stopRecording();
const status = await api.getStatus();
return status.interaction;
}
```
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [auth()](/assistant/api/auth)
* [createInteraction()](/assistant/api/create-interaction)
* [startRecording()](/assistant/api/start-recording)
* [setInteractionOptions()](/assistant/api/set-interaction-options)
# addFacts()
Source: https://docs.corti.ai/assistant/api/add-facts
Reference for the Embedded API addFacts() method.
Use `addFacts()` to attach contextual facts to the current interaction.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
await api.addFacts([
{ text: "Chest pain", group: "other" },
{ text: "Shortness of breath", group: "other" },
{ text: "Fatigue", group: "other" },
]);
```
## Prerequisites
* User must be authenticated.
* An interaction must already exist.
## Input validation
* Web Component / React: pass an array of facts.
* Window API / PostMessage: pass `{ facts: [...] }`.
* `facts[].text`: Required, non-empty string.
* `facts[].group`: Optional string, defaults to `"other"`.
* `facts[].source`: Optional, defaults to `"user"`.
## Possible errors
* `NOT_READY`: No active interaction.
* `UNAUTHORIZED`: User not authenticated.
* `INVALID_PAYLOAD`: Empty facts array or missing text field.
* `INTERNAL_ERROR`: Failed to save facts.
## Returns
`void`
## Patient gender guidance
In languages where patient gender affects phrasing in documentation, provide a clear sentence in the target document language through `addFacts()`.
Common German examples:
```text theme={null}
Das Geschlecht des Patienten ist männlich.
Das Geschlecht des Patienten ist weiblich.
Das Geschlecht des Patienten ist divers.
Das Geschlecht des Patienten ist unbekannt.
```
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [createInteraction()](/assistant/api/create-interaction)
# auth()
Source: https://docs.corti.ai/assistant/api/auth
Reference for the Embedded API auth() method.
Use `auth()` to authenticate the current user session with the embedded Assistant.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
const user = await api.auth({
access_token: string,
refresh_token: string,
id_token: string,
token_type: string,
});
```
## Prerequisites
* None
## Input validation
* All fields (`access_token`, `refresh_token`, `id_token`, `token_type`) are required.
* Tokens must be valid JWT strings.
## Possible errors
* `INVALID_PAYLOAD`: Missing required authentication fields.
* `UNAUTHORIZED`: Invalid or expired tokens.
* `INTERNAL_ERROR`: Authentication service unavailable.
## Returns
```typescript theme={null}
{
id: string,
email: string
}
```
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [createInteraction()](/assistant/api/create-interaction)
# configure() (Deprecated)
Source: https://docs.corti.ai/assistant/api/configure
Reference for the deprecated Embedded API configure() method.
Use `configure()` only if you still depend on the legacy configuration structure.
`configure()` is deprecated in favor of
[configureApp()](/assistant/api/configure-app) and
[setInteractionOptions()](/assistant/api/set-interaction-options). It
continues to work during the deprecation period. See the [Migration
Guide](/assistant/configuration-migration) and [Scheduled
Deprecations](/assistant/deprecation-timeline).
## Usage
```typescript theme={null}
const config = await api.configure({
features: {
interactionTitle: boolean,
aiChat: boolean,
documentFeedback: boolean,
navigation: boolean,
virtualMode: boolean,
syncDocumentAction: boolean,
templateEditor: boolean,
},
appearance: {
primaryColor: string | null,
},
locale: {
interfaceLanguage: string | null,
dictationLanguage: string,
overrides: Record,
},
});
```
## Prerequisites
* None
## Input validation
* `appearance.primaryColor`: Must be valid CSS color or `null`.
* `locale.interfaceLanguage`: Must be a supported interface language or `null`.
* `locale.dictationLanguage`: Must be a supported dictation language.
* `locale.overrides`: Keys must match known override strings.
* `features.*`: Must be boolean values.
* `network.websocketBaseUrl`: Must be a valid URL if provided.
## Possible errors
* `INVALID_PAYLOAD`: Invalid color format, unsupported language code, or non-boolean feature flag.
* `INTERNAL_ERROR`: Failed to apply configuration.
## Returns
```typescript theme={null}
{
features: {
interactionTitle: boolean,
aiChat: boolean,
documentFeedback: boolean,
navigation: boolean,
virtualMode: boolean,
syncDocumentAction: boolean,
templateEditor: boolean,
},
appearance: {
primaryColor: string | null,
},
locale: {
interfaceLanguage: string | null,
dictationLanguage: string,
overrides: Record,
}
}
```
## Defaults
* `features.interactionTitle: true`
* `features.aiChat: true`
* `features.documentFeedback: true`
* `features.navigation: false`
* `features.virtualMode: true`
* `features.syncDocumentAction: false`
* `features.templateEditor: true`
* `appearance.primaryColor: null`
* `locale.interfaceLanguage: null`
* `locale.dictationLanguage: "en"`
* `locale.overrides: {}`
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [configureApp()](/assistant/api/configure-app)
* [setInteractionOptions()](/assistant/api/set-interaction-options)
* [Configuration Guide (Deprecated)](/assistant/configuration)
# configureApp()
Source: https://docs.corti.ai/assistant/api/configure-app
Reference for the Embedded API configureApp() method.
Use `configureApp()` for app-level configuration such as UI visibility, companion app access, appearance, locale, and network settings.
`debug` enables a debug panel intended for development only. Do not enable it
in staging or production.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
`configureApp()` is patchable and may be called multiple times.
Use [Configuration Scenarios](/assistant/configuration-scenarios) for
practical examples and [Supported Values](/assistant/configuration-values) for
supported languages and override keys.
## Payload shape
```typescript theme={null}
type ConfigureAppPayload = {
debug?: boolean;
ui?: {
interactionTitle?: boolean;
aiChat?: boolean;
documentFeedback?: boolean;
navigation?: boolean;
};
companionApp?: {
enabled: boolean;
};
appearance?: {
primaryColor?: string | null;
};
locale?: {
interfaceLanguage?: string;
dictationLanguage?: string | null;
overrides?: Record;
};
network?: {
websocketBaseUrl?: string | null;
};
};
```
## Usage
```typescript theme={null}
await api.configureApp({
debug: false,
ui: {
interactionTitle: true,
aiChat: true,
documentFeedback: true,
navigation: false,
},
companionApp: {
enabled: true,
},
appearance: {
primaryColor: "#000000",
},
locale: {
interfaceLanguage: "da-DK",
dictationLanguage: "da",
overrides: {
"interview.document.syncDocument.label": "Sync Document",
},
},
network: {
websocketBaseUrl: "wss://proxy.example.com",
},
});
```
## Prerequisites
* None
## Input validation
* `debug`, `ui.interactionTitle`, `ui.aiChat`, `ui.documentFeedback`, `ui.navigation`, and `companionApp.enabled`: Must be boolean values when provided.
* `appearance.primaryColor`: Must be a valid CSS color string or `null` when provided.
* `locale.interfaceLanguage`: Must be a supported interface language when provided.
* `locale.dictationLanguage`: Must be a supported dictation language or `null` when provided.
* `locale.overrides`: Keys must match known override strings.
* `network.websocketBaseUrl`: Must be a valid WebSocket URL or `null` when provided. This is mainly for proxy deployments. See the [Proxy guide](/assistant/proxy).
## Possible errors
* `INVALID_PAYLOAD`: Invalid color, URL, language code, or non-boolean UI value.
* `INTERNAL_ERROR`: Failed to apply configuration.
## Returns
This method is documented as a configuration action and does not expose a response payload in the current public reference.
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [Supported Values](/assistant/configuration-values)
* [Configuration Scenarios](/assistant/configuration-scenarios)
* [showDeviceLinkQR()](/assistant/api/show-device-link-qr)
* [Configuration Guide (Deprecated)](/assistant/configuration)
# configureSession() (Deprecated)
Source: https://docs.corti.ai/assistant/api/configure-session
Reference for the deprecated Embedded API configureSession() method.
Use `configureSession()` only if you still depend on the legacy session-configuration structure.
`configureSession()` is deprecated in favor of
[setInteractionOptions()](/assistant/api/set-interaction-options). It
continues to work during the deprecation period. If your integration depends
on `defaultOutputLanguage`, keep using its legacy-compatible behavior during
this period. See the [Migration Guide](/assistant/configuration-migration) and
[Scheduled Deprecations](/assistant/deprecation-timeline).
## Usage
```typescript theme={null}
await api.configureSession({
defaultLanguage: "en",
defaultOutputLanguage: "en",
defaultTemplateKey: "corti-soap",
defaultMode: "virtual",
});
```
```javascript theme={null}
await api.configureSession({
defaultTemplateKey: "corti-soap",
defaultOutputLanguage: "en",
});
```
## Prerequisites
* User must be authenticated.
## Input validation
* `defaultLanguage`: Must be a valid language code.
* `defaultOutputLanguage`: Must be a valid language code.
* `defaultTemplateKey`: Must be a language-agnostic template identifier.
* `defaultMode`: Must be either `"virtual"` or `"in-person"`.
* If either `defaultTemplateKey` or `defaultOutputLanguage` is provided, both must be provided together.
## Possible errors
* `UNAUTHORIZED`: User not authenticated.
* `INVALID_PAYLOAD`: Invalid payload, including when only one of `defaultTemplateKey` or `defaultOutputLanguage` is provided.
* `NOT_FOUND`: No template matches `defaultTemplateKey` plus `defaultOutputLanguage`.
* `INTERNAL_ERROR`: Failed to update session settings.
## Returns
`void`
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [setInteractionOptions()](/assistant/api/set-interaction-options)
* [getTemplates()](/assistant/api/get-templates)
# createInteraction()
Source: https://docs.corti.ai/assistant/api/create-interaction
Reference for the Embedded API createInteraction() method.
Use `createInteraction()` to create a new interaction session before navigating into it or starting recording.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
const interaction = await api.createInteraction({
assignedUserId: null,
encounter: {
identifier: `encounter-${Date.now()}`,
status: "planned",
type: "first_consultation",
period: {
startedAt: new Date().toISOString(),
},
title: "Initial Consultation",
},
});
```
## Prerequisites
* User must be authenticated.
## Input validation
* `encounter.identifier`: Required, non-empty string.
* `encounter.status`: Must be one of `"planned"`, `"in-progress"`, `"completed"`, or `"cancelled"`.
* `encounter.type`: Must be one of the supported encounter types.
* `encounter.period.startedAt`: Required ISO 8601 datetime string.
* `encounter.title`: Optional string.
* `assignedUserId`: Optional string or `null`.
## Possible errors
* `UNAUTHORIZED`: User not authenticated.
* `INVALID_PAYLOAD`: Missing required fields, invalid encounter status or type, or invalid date format.
* `INTERNAL_ERROR`: Failed to create interaction.
## Returns
```typescript theme={null}
{
id: string,
createdAt: string,
status?: string
}
```
## Notes
* Create the interaction before calling [startRecording()](/assistant/api/start-recording).
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [addFacts()](/assistant/api/add-facts)
* [startRecording()](/assistant/api/start-recording)
# getStatus()
Source: https://docs.corti.ai/assistant/api/get-status
Reference for the Embedded API getStatus() method.
Use `getStatus()` to inspect the current application state, including authentication, current URL, interaction details, documents, and facts.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
const status = await api.getStatus();
```
## Prerequisites
* None
## Input validation
* None
## Possible errors
* This action does not typically throw structured error codes.
* In rare cases, partial data may be returned if specific state queries fail.
## Returns
```typescript theme={null}
{
applicationVersion: string,
auth: {
isAuthenticated: boolean,
user: {
id: string,
email: string
} | null
},
currentUrl: string,
isMedicalDevice: boolean,
medicalDevice?: {
buildDate: string,
deviceIdentifier: string,
udi: string
},
interaction: {
id: string,
title: string,
state: "planned" | "ongoing" | "paused" | "disconnected" | "ending" | "parsing" | "ended",
startedAt: string,
endedAt: string | null,
endsAt: string | null,
transcripts: Array,
documents: Array,
facts: Array,
websocketUrl: string
} | null
}
```
## Notes
* This method is useful for debugging and for checking whether prerequisites are already satisfied.
* `applicationVersion` is the application release version.
* `isMedicalDevice` indicates whether the current build is a regulated medical device. When `true`, the `medicalDevice` object is included with the device identifier, build date, and composed UDI. When `false`, `medicalDevice` is omitted.
* `interaction` may be `null` if no interaction is active or if interaction lookup fails.
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [auth()](/assistant/api/auth)
* [createInteraction()](/assistant/api/create-interaction)
# getTemplates()
Source: https://docs.corti.ai/assistant/api/get-templates
Reference for the Embedded API getTemplates() method.
Use `getTemplates()` to retrieve all document templates available to the authenticated user.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
Use this method before `setInteractionOptions()` when you need to verify the
template id and language combination you want to use as the default.
## Usage
```typescript theme={null}
const response = await api.getTemplates();
console.log(response.templates);
```
## Prerequisites
* User must be authenticated.
## Input validation
* None
## Possible errors
* `UNAUTHORIZED`: User not authenticated.
* `INTERNAL_ERROR`: Failed to fetch templates from the server.
## Returns
```typescript theme={null}
{
templates: Array<{
id: string;
name: string;
description?: string;
language: {
code: string;
name: string;
locale?: string;
};
sections: Array<{
id: string;
title: string;
}>;
isCustom: boolean;
}>;
}
```
## Use cases
* Display a template picker.
* Filter templates by language.
* Distinguish between built-in and custom templates.
* Pre-populate template selection based on user preferences or defaults.
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [setInteractionOptions()](/assistant/api/set-interaction-options)
# navigate()
Source: https://docs.corti.ai/assistant/api/navigate
Reference for the Embedded API navigate() method.
Use `navigate()` to move the embedded Assistant to a specific internal route.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
await api.navigate({ path: "/session/interaction-123" });
```
For consistency across transports, prefer object payload form.
Web Component / React compatibility:
* `@corti/embedded-web` earlier than `0.3.0`: only
`api.navigate("/session/interaction-123")` is accepted.
* `@corti/embedded-web@0.3.0` and later: both
`api.navigate("/session/interaction-123")` and
`api.navigate({ path: "/session/interaction-123" })` are accepted.
## Prerequisites
* None
## Input validation
* Window API / PostMessage: payload must be an object with `path`.
* Web Component / React on `@corti/embedded-web@0.3.0` and later: payload can
be an object with `path` (recommended) or a path string.
* Web Component / React on versions earlier than `0.3.0`: payload must be a
path string.
* `path`: Required.
* `path`: Must be a string that starts with `/`.
* `path`: Cannot be a full URL.
* `path`: Must stay within the embedded application.
## Possible errors
* `INVALID_PAYLOAD`:
* Path does not start with `/`.
* Path is a full URL.
* `INTERNAL_ERROR`: Navigation failed.
## Returns
`void`
## Valid path patterns
* `/` - start a new session
* `/session/` - open an existing session
* `/templates` - browse and create templates
* `/settings/preferences` - edit defaults like languages and default session settings
* `/settings/input` - edit dictation input settings
* `/settings/account` - edit general account settings
* `/settings/archive` - view items in and restore from archive
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [createInteraction()](/assistant/api/create-interaction)
# setCredentials()
Source: https://docs.corti.ai/assistant/api/set-credentials
Reference for the Embedded API setCredentials() method.
Use `setCredentials()` to change the credentials of the currently authenticated user.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
await api.setCredentials({ password: "YOUR_NEW_PASSWORD" });
```
## Prerequisites
* User must be authenticated.
## Input validation
* `password`: Required string.
* Password must meet policy requirements:
* Minimum 8 characters.
* At least 1 uppercase letter.
* At least 1 lowercase letter.
* At least 1 number.
* At least 1 special character.
## Possible errors
* `UNAUTHORIZED`: User not authenticated.
* `INVALID_PAYLOAD`: Password does not meet requirements.
* `INTERNAL_ERROR`: Failed to update password.
## Returns
`void`
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [auth()](/assistant/api/auth)
# setInteractionOptions()
Source: https://docs.corti.ai/assistant/api/set-interaction-options
Reference for the Embedded API setInteractionOptions() method.
Use `setInteractionOptions()` for interaction-level defaults such as mode, spoken language, template defaults, personal templates, standard templates, project templates, inline templates, and document actions.
When you set `templates.defaultTemplate`, it can provide a fallback template for
new sessions or force the first generated document to use a specific template.
Fallback templates are used only when the user does not already have their own
default template set.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
`setInteractionOptions()` is a one-shot configuration for the embedded
instance. Set it before the user starts or opens an interaction.
Use [Configuration Scenarios](/assistant/configuration-scenarios) for
practical examples and [Supported Values](/assistant/configuration-values) for
supported languages and other lookup values.
`templates.sources.standard`, `templates.sources.project`, and
`templates.sources.inline` apply only when [Guided Document
Generation](/release-notes/corti-assistant#v12-19-0) is enabled for your
account. If you send these sources before rollout reaches your environment,
the options are inert and do not change behavior. Assistant only dispatches
`embedded.interactionOptionsWarning` with `unsupportedSources`.
## Usage
```typescript theme={null}
await api.setInteractionOptions({
mode: {
fallback: "in-person",
options: ["in-person", "virtual"],
},
spokenLanguage: {
fallback: "da",
options: ["da", "en"],
},
templates: {
sources: {
personal: {
enabled: true,
sectionFields: {
heading: { editable: true },
description: { editable: true },
miscPrompt: { visible: true, editable: true },
outputSchema: { visible: true, editable: true },
},
},
standard: {
enabled: true,
include: {
regions: ["BEL"],
families: ["soap"],
},
exclude: {
regions: ["USA"],
families: ["letter"],
},
},
project: {
enabled: true,
include: {
ids: ["7a0ab5b8-1f62-4f75-86a7-4fcd8fd07b5d"],
},
exclude: {
ids: ["d7a7fd8c-8892-4475-81d3-3414b7d8e27d"],
},
},
inline: {
enabled: true,
templates: [
{
id: "inline-soap",
name: "Embedded SOAP",
generation: {
instructions: {
prompt: "Generate a concise SOAP note for the current encounter.",
},
sections: [
{
heading: "Subjective",
instructions: {
contentPrompt: "Summarize the patient's subjective symptoms.",
},
},
],
},
},
],
},
},
defaultTemplate: {
behaviour: "force-first-document",
template: {
source: "inline",
id: "inline-soap",
},
allowUserSelection: false,
},
},
documents: {
actions: {
sync: false,
},
allowedLanguages: ["en", "da"],
maxGenerated: 1,
},
});
```
## Prerequisites
* User must be authenticated.
## Defaults and omitted values
`setInteractionOptions()` applies only the option slices you provide. When you omit an option, the embedded Assistant does not apply an override for that setting and keeps the current or product default behavior.
Inline templates are available only to the embedded instance that receives the payload. Use an `id` when another option, such as `templates.defaultTemplate`, needs to reference the inline template.
For personal template section fields, the default editor behavior is:
| Field | Default visibility | Default editability |
| -------------- | ------------------ | --------------------- |
| `heading` | Visible | Editable |
| `description` | Visible | Editable |
| `miscPrompt` | Hidden | Editable when visible |
| `outputSchema` | Hidden | Editable when visible |
Use `visible: true` to show a prompt field. Use `editable: false` to make a visible field read-only.
## Input validation
* `mode.fallback`: Must be either `"in-person"` or `"virtual"`.
* `mode.options`: Must contain one or both of `"in-person"` and `"virtual"`.
* `spokenLanguage.fallback`: Must be a valid language code when provided.
* `spokenLanguage.options`: Must be an array of valid language codes when provided. If you provide both `fallback` and `options`, `fallback` should be one of the listed options.
* `templates.sources.personal.enabled`: Must be a boolean when provided.
* `templates.sources.personal.sectionFields`: Optional object controlling field-level access in the guided template editor. `heading` and `description` accept an optional `{ editable?: boolean }` object. `miscPrompt` and `outputSchema` accept an optional `{ visible?: boolean; editable?: boolean }` object.
* `templates.sources.standard.enabled`: Must be a boolean when provided.
* `templates.sources.standard.include.regions`: Must be an array of ISO 3166-1 alpha-3 template region codes when provided.
* `templates.sources.standard.include.families`: Must be an array of standard template family identifiers when provided.
* `templates.sources.standard.exclude.regions`: Must be an array of ISO 3166-1 alpha-3 template region codes when provided.
* `templates.sources.standard.exclude.families`: Must be an array of standard template family identifiers when provided.
* `templates.sources.project.enabled`: Must be a boolean when provided.
* `templates.sources.project.include.ids`: Must be an array of project template UUID strings when provided.
* `templates.sources.project.exclude.ids`: Must be an array of project template UUID strings when provided.
* `templates.sources.inline.enabled`: Must be a boolean when provided.
* `templates.sources.inline.templates`: Must be an array of inline template objects when provided. Inline template IDs only need to be unique within the current `setInteractionOptions()` payload and are required only when referenced by another option.
* `templates.defaultTemplate.behaviour`: Must be `"fallback"` or `"force-first-document"` when provided.
* `templates.defaultTemplate.template.source`: Must be `"standard"`, `"project"`, or `"inline"` when provided.
* `templates.defaultTemplate.template.id`: Must be a valid template identifier for the selected source when provided. Standard templates use fully resolved identifiers such as `"corti-soap-en"`; inline templates use the local inline template ID.
* `templates.defaultTemplate.allowUserSelection`: Must be a boolean when provided. Set it to `false` to hide user controls for selecting and persisting a default template.
* `documents.actions.sync`: Must be a boolean when provided.
* `documents.allowedLanguages`: Must be an array of valid BCP 47 locale codes when provided. Region-less codes (e.g., `"en"`, `"da"`) are recommended; region variants (e.g., `"en-US"`) are supported if the region-less code does not provide your desired results.
* `documents.maxGenerated`: Must be a number or `"unlimited"` when provided. `0` is treated as unlimited.
## Possible errors
* `UNAUTHORIZED`: User not authenticated.
* `INVALID_PAYLOAD`: Invalid mode, language, template, or boolean value.
* `INTERNAL_ERROR`: Failed to apply interaction options.
## Returns
This method is documented as a configuration action and does not expose a response payload in the current public reference.
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [Supported Values](/assistant/configuration-values)
* [Configuration Scenarios](/assistant/configuration-scenarios)
* [configureSession() (Deprecated)](/assistant/api/configure-session)
# showDeviceLinkQR()
Source: https://docs.corti.ai/assistant/api/show-device-link-qr
Reference for the Embedded API showDeviceLinkQR() method.
Use `showDeviceLinkQR()` to initiate device pairing for the Corti mobile companion app. The method opens a QR pairing screen that lives on a separate URL. How you surface this is up to you, but a common pattern is a "Pair mobile device" option in your application's settings that presents the URL in a modal or dedicated panel. The method resolves when the pairing request settles and returns the final status.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
Enable the companion app surface with
[`configureApp()`](/assistant/api/configure-app) before offering mobile
pairing controls in your host application.
QR code generation requires a **second** Keycloak token response for the same user — do not reuse the token from your original authentication request. Each application (desktop and companion) needs its own token so that both can refresh their sessions independently.
## Usage
```ts title="TypeScript" theme={null}
// Ensure that you have authenticated your user with your chosen method
// and have obtained the required tokens
// Replace these with your values
const ACCESS_TOKEN = "";
const REFRESH_TOKEN = "";
await api.configureApp({
companionApp: {
enabled: true,
},
});
// Generate the QR code for pairing the companion app
const result = await api.showDeviceLinkQR({
access_token: ACCESS_TOKEN,
refresh_token: REFRESH_TOKEN,
token_type: "Bearer",
});
if (result.status === "approved") {
// The mobile companion app was paired successfully.
}
```
## Prerequisites
* User must be authenticated in the Embedded Assistant.
* `companionApp.enabled` should be set to `true` in `configureApp()`.
* The payload must include a valid access token and refresh token for the same user session.
## Input validation
* `access_token`: Must be a valid OAuth access token.
* `refresh_token`: Must be present. The mobile companion app uses it to complete pairing after approval.
* `token_type`: Must match the OAuth token response, usually `"Bearer"`.
## Returns
```typescript theme={null}
type ShowDeviceLinkQRResponse = {
status: "approved" | "denied" | "expired" | "dismissed";
};
```
* `approved`: The user approved the mobile device pairing request.
* `denied`: The user denied the mobile device pairing request.
* `expired`: The QR code or approval window expired before pairing completed.
* `dismissed`: A new QR request replaced the previous one, the embedded route was dismissed, or the pairing flow could not complete.
## Possible errors
* `UNAUTHORIZED`: User not authenticated.
* `INVALID_PAYLOAD`: Token payload is missing required values.
* `INTERNAL_ERROR`: Failed to create or display the pairing request.
## Companion app installation
The pairing page displays a QR code. Users scan it from the Corti Companion app or enter the code manually. A confirmation prompt then appears on the desktop showing the device name, so the user can approve or deny to make sure they are pairing the intended device. Once paired, the companion app must be open and running for the phone to appear as a selectable microphone source in the Embedded Assistant.
If you want to surface download links in your own application's help center or onboarding flow, the app is available on iOS and Android:
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [configureApp()](/assistant/api/configure-app)
* [Web Component API](/assistant/web-component-api)
# startRecording()
Source: https://docs.corti.ai/assistant/api/start-recording
Reference for the Embedded API startRecording() method.
Use `startRecording()` to begin recording within the current embedded session.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
await api.startRecording();
```
## Prerequisites
* User must be authenticated.
* An interaction must exist.
* The application must be in an interview or session context.
## Input validation
* None
## Possible errors
* `NOT_READY`: Not in interview context or no interaction exists.
* `UNAUTHORIZED`: User not authenticated.
* `NOT_FOUND`: Interaction not found.
* `INTERNAL_ERROR`: Failed to connect to the recording service.
## Returns
`void`
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [createInteraction()](/assistant/api/create-interaction)
* [stopRecording()](/assistant/api/stop-recording)
# stopRecording()
Source: https://docs.corti.ai/assistant/api/stop-recording
Reference for the Embedded API stopRecording() method.
Use `stopRecording()` to end recording within the current embedded session.
Applies to Web Component, Window API, and PostMessage. Examples on this page
use the Web Component API shape.
## Usage
```typescript theme={null}
await api.stopRecording();
```
## Prerequisites
* Recording must already be active.
## Input validation
* None
## Possible errors
* `NOT_READY`: Not in an active recording session.
* `INTERNAL_ERROR`: Failed to disconnect from the recording service.
## Returns
`void`
## Related reference
* [API Reference Overview](/assistant/api-reference)
* [startRecording()](/assistant/api/start-recording)
* [getStatus()](/assistant/api/get-status)
# Authentication for Embedded Users
Source: https://docs.corti.ai/assistant/authentication
Choosing the right OAuth2 flow for Corti Embedded integrations
## Background
OAuth (Open Authorization) is an open-standard framework for access delegation, allowing applications to securely access a user's protected resources without exposing their login credentials. By keeping passwords private and limiting access to sensitive information, OAuth improves both security and access management across web, mobile, and desktop applications. OAuth 2.0, the current and most widely adopted version, expands upon the original protocol to support APIs, mobile apps, and connected devices, offering multiple authorization flows tailored to different application types.
Corti Assistant requires user-based authentication. Client credentials flows and other machine-to-machine authentication methods are NOT supported for embedded Corti Assistant integrations. You must authenticate as an end user, not as an application.
When embedding Corti Assistant in your application (via iFrame/WebView), **it's important to use the right OAuth2 grant type** that supports user-based authentication.
This guide explains the OAuth flows that work with embedded Corti Assistant, focusing on user-based authentication methods that are suitable for interactive scenarios.
***
## Supported OAuth grant types for Embedded Corti Assistant
The following OAuth flows support user-based authentication and can be used with embedded Corti Assistant. **Client credentials grant is NOT supported** as it does not provide user context.
### 1. Authorization code flow with PKCE (recommended)
**Best for:** Native apps, single-page apps, or any browser-based integration where a user is present.
**Why:** This flow is secure, interactive, and doesn't require a client secret (ideal for public clients). Proof Key for Code Exchange (PKCE) protects against code interception attacks.
**How it works:**
1. Your app redirects the user to Corti's OAuth2 authorization server.
2. The user logs in and grants permission.
3. Corti redirects back with an authorization code.
4. Your app exchanges the code (with the PKCE verifier) for an access token.
**Key advantages:**
* Secure and suitable for embedded web apps.
* No client secret is required.
* Enforces user interaction.
This example uses the [Corti JavaScript SDK](/sdk/js/overview) (`@corti/sdk`)
```javascript Step 1: Generate Authorization URL (Frontend) [expandable] theme={null}
import { CortiAuth, CortiEnvironment } from "@corti/sdk";
const auth = new CortiAuth({
environment: CortiEnvironment.Eu,
tenantName: "YOUR_TENANT_NAME",
});
// SDK automatically generates code verifier, stores it in localStorage and makes redirect
await auth.authorizePkceUrl({
clientId: "YOUR_CLIENT_ID",
redirectUri: "https://your-app.com/callback"
});
```
```javascript Step 2: Handle the Callback and Exchange Code for Tokens [expandable] theme={null}
// Extract the authorization code from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
console.error('Authorization failed:', error);
return;
}
if (code) {
// Exchange the authorization code for tokens using SDK
const tokenResponse = await auth.getPkceFlowToken({
clientId: "YOUR_CLIENT_ID",
redirectUri: "https://your-app.com/callback",
code: code,
});
const { accessToken, refreshToken } = tokenResponse;
}
```
This flow requires two stages: generating the code verifier/challenge and handling the token exchange after the redirect.
```bash Step 1: Redirect User to Authorize [expandable] theme={null}
// Generate code verifier + challenge
const code_verifier = crypto.randomUUID().replace(/-/g, '');
const code_challenge = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
const base64url = btoa(String.fromCharCode(...new Uint8Array(code_challenge)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// Store verifier for use after redirect
localStorage.setItem('pkce_verifier', code_verifier);
// Redirect to authorization server
window.location.href = `https://auth.us.corti.app/realms//protocol/openid-connect/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&code_challenge=${base64url}&code_challenge_method=S256&scope=openid`;
```
```bash Step 2: Exchange Code for Token (after redirect) [expandable] theme={null}
const code = new URLSearchParams(window.location.search).get('code');
const code_verifier = localStorage.getItem('pkce_verifier');
const response = await fetch('https://auth.us.corti.app/realms//protocol/openid-connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://yourapp.com/callback',
code,
code_verifier
})
});
const data = await response.json();
console.log('Access Token:', data.access_token);
```
This example uses standard .NET libraries for OAuth2 PKCE flow
```csharp Step 1: Generate Authorization URL (Frontend) [expandable] theme={null}
using System;
using System.Security.Cryptography;
using System.Text;
using System.Web;
public class PkceAuth
{
private const string ClientId = "YOUR_CLIENT_ID";
private const string RedirectUri = "https://your-app.com/callback";
private const string AuthBaseUrl = "https://auth.us.corti.app/realms//protocol/openid-connect";
public static (string authUrl, string codeVerifier) GenerateAuthorizationUrl()
{
// Generate code verifier (random string)
var codeVerifier = GenerateCodeVerifier();
// Generate code challenge (SHA256 hash, base64url encoded)
var codeChallenge = GenerateCodeChallenge(codeVerifier);
// Store code verifier (e.g., in session or secure storage)
// In a real app, store this securely for use after redirect
HttpContext.Current.Session["pkce_verifier"] = codeVerifier;
// Build authorization URL
var authUrl = $"{AuthBaseUrl}/auth?" +
$"response_type=code&" +
$"client_id={HttpUtility.UrlEncode(ClientId)}&" +
$"redirect_uri={HttpUtility.UrlEncode(RedirectUri)}&" +
$"code_challenge={codeChallenge}&" +
$"code_challenge_method=S256&" +
$"scope=openid";
return (authUrl, codeVerifier);
}
private static string GenerateCodeVerifier()
{
var bytes = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(bytes);
}
return Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
private static string GenerateCodeChallenge(string codeVerifier)
{
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
return Convert.ToBase64String(challengeBytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}
}
```
```csharp Step 2: Exchange Code for Token (after redirect) [expandable] theme={null}
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
public class TokenExchange
{
private const string ClientId = "YOUR_CLIENT_ID";
private const string RedirectUri = "https://your-app.com/callback";
private const string TokenUrl = "https://auth.us.corti.app/realms//protocol/openid-connect/token";
public static async Task ExchangeCodeForTokenAsync(string code, string codeVerifier)
{
using var httpClient = new HttpClient();
var formData = new List>
{
new KeyValuePair("grant_type", "authorization_code"),
new KeyValuePair("client_id", ClientId),
new KeyValuePair("redirect_uri", RedirectUri),
new KeyValuePair("code", code),
new KeyValuePair("code_verifier", codeVerifier)
};
var content = new FormUrlEncodedContent(formData);
var response = await httpClient.PostAsync(TokenUrl, content);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var tokenData = JsonSerializer.Deserialize(responseBody);
return new TokenResponse
{
AccessToken = tokenData.GetProperty("access_token").GetString(),
RefreshToken = tokenData.GetProperty("refresh_token").GetString(),
ExpiresIn = tokenData.GetProperty("expires_in").GetInt32()
};
}
}
public class TokenResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public int ExpiresIn { get; set; }
}
// Usage in callback handler (e.g., ASP.NET MVC Controller)
public class AuthController : Controller
{
public async Task Callback(string code)
{
if (string.IsNullOrEmpty(code))
{
return View("Error");
}
// Retrieve stored code verifier
var codeVerifier = Session["pkce_verifier"] as string;
if (string.IsNullOrEmpty(codeVerifier))
{
return View("Error");
}
var tokenResponse = await TokenExchange.ExchangeCodeForTokenAsync(code, codeVerifier);
// Store tokens securely (e.g., in session or secure cookie)
Session["access_token"] = tokenResponse.AccessToken;
return RedirectToAction("Index", "Home");
}
}
```
Use this gold standard for Corti Assistant embedded use cases.
***
### 2. Authorization code flow (without PKCE)
**Best for:** Server-side web applications embedding Corti Assistant where the client secret can be safely stored on the backend.
**Why:** Similar to PKCE, but requires storing a client secret — which is *not* safe in public or browser-based clients.
**Key Concerns:**
* Unsafe for apps where the frontend or iFrame can be inspected.
* Only acceptable in secure backend environments where the client secret can be protected.
This example uses the [Corti JavaScript SDK](/sdk/js/overview) (`@corti/sdk`)
```javascript Step 1: Create Authorization URL [expandable] theme={null}
import { CortiAuth, CortiEnvironment } from "@corti/sdk";
const auth = new CortiAuth({
environment: CortiEnvironment.Eu,
tenantName: "YOUR_TENANT_NAME",
});
// Generate authorization URL
await auth.authorizeURL({
clientId: "YOUR_CLIENT_ID",
redirectUri: "https://your-app.com/callback",
});
```
```javascript Step 2: Handle the Callback and Exchange Code for Tokens [expandable] theme={null}
// Extract the authorization code from URL parameters
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
console.error('Authorization failed:', error);
return;
}
if (code) {
// Exchange the authorization code for tokens using SDK (client-side)
const response = await fetch('http://localhost:3000/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
});
if (!response.ok) {
throw new Error('Authentication failed');
}
const tokens = await response.json();
}
```
```javascript Step 3: Exchange Code for Access Token (Backend) [expandable] theme={null}
// server.js or routes/callback.js
import express from "express";
import { CortiAuth } from "@corti/sdk";
const app = express();
const CLIENT_ID = "YOUR_CLIENT_ID";
const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
const TENANT_NAME = "YOUR_TENANT_NAME";
const ENVIRONMENT = "YOUR_ENVIRONMENT";
const REDIRECT_URI = "https://yourapp.com/callback"; // must match OAuth settings
app.get("/callback", async (req, res) => {
const authCode = req.query.code;
if (!authCode) {
return res.status(400).send("Missing authorization code");
}
try {
// Initialize CortiAuth SDK client
const auth = new CortiAuth({
environment: ENVIRONMENT,
tenantName: TENANT_NAME,
});
// Exchange code for token
const tokens = await auth.getCodeFlowToken({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
redirectUri: REDIRECT_URI,
code: authCode,
});
// Example "do something": simply log the access token
console.log("Access Token:", tokens.accessToken);
// Redirect back to app with token in query param (example)
res.json(tokens);
} catch (err) {
console.error("OAuth error:", err);
return res.status(500).send("Failed to exchange authorization code");
}
});
app.listen(3000, () => {
console.log("Server running at http://localhost:3000");
});
```
This version assumes:
* Your app has a **frontend (e.g., React)** that initiates the login.
* Your app has a **backend (e.g., Node.js + Express)** that securely stores the `client_secret` and handles the token exchange.
Use this only if your backend can securely store the client secret (e.g. not in a browser or mobile app). Ideal for server-rendered or hybrid web apps.
```bash Step 1: Frontend – Redirect the User to Log In [expandable] theme={null}
// login.tsx or similar
const clientId = 'YOUR_CLIENT_ID';
const redirectUri = 'https://yourapp.com/callback'; // Must match what's registered in Corti OAuth
const login = () => {
const authUrl = new URL('https://auth.us.corti.app/realms//protocol/openid-connect/auth');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', clientId);
authUrl.searchParams.set('redirect_uri', redirectUri);
authUrl.searchParams.set('scope', 'openid profile');
window.location.href = authUrl.toString(); // Send user to Corti login page
};
```
```bash Step 2: Corti Redirects Back with a Code theme={null}
https://yourapp.com/callback?code=abc123xyz
```
```bash Step 3: Backend – Exchange Code for Access Token [expandable] theme={null}
// server.js or routes/callback.js
const express = require('express');
const fetch = require('node-fetch');
const app = express();
const CLIENT_ID = 'YOUR_CLIENT_ID';
const CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
const REDIRECT_URI = 'https://yourapp.com/callback'; // Must match Step 1
app.get('/callback', async (req, res) => {
const authCode = req.query.code;
if (!authCode) {
return res.status(400).send('Missing authorization code');
}
// Exchange code for access token
const tokenResponse = await fetch('https://auth.us.corti.app/realms//protocol/openid-connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI
})
});
const tokenData = await tokenResponse.json();
if (tokenData.error) {
return res.status(500).send(`Token error: ${tokenData.error_description}`);
}
// Do something useful with the token, like create a session
console.log('Access Token:', tokenData.access_token);
// Optional: send token data to frontend or store in session
res.redirect(`/app?token=${tokenData.access_token}`);
});
app.listen(3000, () => console.log('App listening on http://localhost:3000'));
```
This example uses standard .NET libraries for OAuth2 authorization code flow
```csharp Step 1: Frontend – Redirect the User to Log In [expandable] theme={null}
using System;
using System.Web;
public class AuthHelper
{
private const string ClientId = "YOUR_CLIENT_ID";
private const string RedirectUri = "https://yourapp.com/callback";
private const string AuthBaseUrl = "https://auth.us.corti.app/realms//protocol/openid-connect";
public static string GetAuthorizationUrl()
{
var authUrl = $"{AuthBaseUrl}/auth?" +
$"response_type=code&" +
$"client_id={HttpUtility.UrlEncode(ClientId)}&" +
$"redirect_uri={HttpUtility.UrlEncode(RedirectUri)}&" +
$"scope=openid profile";
return authUrl;
}
}
// Usage in Razor view or controller
// Response.Redirect(AuthHelper.GetAuthorizationUrl());
```
```csharp Step 2: Corti Redirects Back with a Code theme={null}
https://yourapp.com/callback?code=abc123xyz
```
```csharp Step 3: Backend – Exchange Code for Access Token [expandable] theme={null}
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web.Mvc;
public class TokenExchange
{
private const string ClientId = "YOUR_CLIENT_ID";
private const string ClientSecret = "YOUR_CLIENT_SECRET";
private const string RedirectUri = "https://yourapp.com/callback";
private const string TokenUrl = "https://auth.us.corti.app/realms//protocol/openid-connect/token";
public static async Task ExchangeCodeForTokenAsync(string code)
{
using var httpClient = new HttpClient();
var formData = new List>
{
new KeyValuePair("grant_type", "authorization_code"),
new KeyValuePair("code", code),
new KeyValuePair("client_id", ClientId),
new KeyValuePair("client_secret", ClientSecret),
new KeyValuePair("redirect_uri", RedirectUri)
};
var content = new FormUrlEncodedContent(formData);
var response = await httpClient.PostAsync(TokenUrl, content);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
throw new Exception($"Token exchange failed: {errorBody}");
}
var responseBody = await response.Content.ReadAsStringAsync();
var tokenData = JsonSerializer.Deserialize(responseBody);
if (tokenData.TryGetProperty("error", out var error))
{
var errorDescription = tokenData.TryGetProperty("error_description", out var desc)
? desc.GetString()
: "Unknown error";
throw new Exception($"Token error: {errorDescription}");
}
return new TokenResponse
{
AccessToken = tokenData.GetProperty("access_token").GetString(),
RefreshToken = tokenData.TryGetProperty("refresh_token", out var refresh)
? refresh.GetString()
: null,
ExpiresIn = tokenData.GetProperty("expires_in").GetInt32()
};
}
}
public class TokenResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public int ExpiresIn { get; set; }
}
// Usage in ASP.NET MVC Controller
public class AuthController : Controller
{
public async Task Callback(string code)
{
if (string.IsNullOrEmpty(code))
{
return new HttpStatusCodeResult(400, "Missing authorization code");
}
try
{
var tokenResponse = await TokenExchange.ExchangeCodeForTokenAsync(code);
// Store tokens securely (e.g., in session or secure cookie)
Session["access_token"] = tokenResponse.AccessToken;
// Redirect to app with token
return RedirectToAction("Index", "Home");
}
catch (Exception ex)
{
return new HttpStatusCodeResult(500, $"Token error: {ex.Message}");
}
}
}
```
Summary of the flow:
| Step | Component | Description |
| :--- | :-------- | :-------------------------------------------------------------------------------------------------- |
| 1 | Frontend | Redirects user to Corti login |
| 2 | Corti | Redirects back to your app with `code` |
| 3 | Backend | Exchanges code + client secret for tokens Responds with session/token, redirects to frontend |
Requirements:
* Must use **HTTPS** for `redirect_uri`
* Your `client_secret` **must not** be exposed to the frontend
* The code returned is **valid for one use and short-lived**
Use only if the OAuth2 flow is entirely server-to-server.
***
### 3. Resource Owner Password Credentials (ROPC) grant (use with caution)
**Best for:** Controlled environments embedding Corti Assistant with trusted clients (e.g., internal tools).
**Why:** Allows username/password login directly in the app — but **bypasses the authorization server UI**.
**Risks:**
* Trains users to enter passwords into third-party apps.
* Easy to misuse, violates best practices.
* Only viable where UI constraints prevent redirecting (e.g., native kiosk apps without browsers).
This example uses the [Corti JavaScript SDK](/sdk/js/overview) (`@corti/sdk`)
```javascript [expandable] theme={null}
import { CortiAuth, CortiClient, CortiEnvironment } from "@corti/sdk";
const CLIENT_ID = "YOUR_CLIENT_ID";
const USERNAME = "user@example.com";
const PASSWORD = "your-password";
// Step 1: Exchange credentials for tokens using ROPC flow
const auth = new CortiAuth({
environment: CortiEnvironment.Eu,
tenantName: "YOUR_TENANT_NAME",
});
const tokenResponse = await auth.getRopcFlowToken({
clientId: CLIENT_ID,
username: USERNAME,
password: PASSWORD,
});
const { accessToken, refreshToken } = tokenResponse;
```
Use only in trusted/internal scenarios.
Here's a simple fetch example:
```bash [expandable] theme={null}
const response = await fetch('https://auth.us.corti.app/realms//protocol/openid-connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'password',
client_id: 'YOUR_CLIENT_ID',
username: 'user@example.com',
password: 'yourpassword'
})
});
const data = await response.json();
console.log('Access Token:', data.access_token);
```
This example uses standard .NET libraries for OAuth2 ROPC flow
Use only in trusted/internal scenarios.
```csharp [expandable] theme={null}
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
public class RopcAuth
{
private const string ClientId = "YOUR_CLIENT_ID";
private const string TokenUrl = "https://auth.us.corti.app/realms//protocol/openid-connect/token";
public static async Task GetTokenAsync(string username, string password)
{
using var httpClient = new HttpClient();
var formData = new List>
{
new KeyValuePair("grant_type", "password"),
new KeyValuePair("client_id", ClientId),
new KeyValuePair("username", username),
new KeyValuePair("password", password)
};
var content = new FormUrlEncodedContent(formData);
var response = await httpClient.PostAsync(TokenUrl, content);
response.EnsureSuccessStatusCode();
var responseBody = await response.Content.ReadAsStringAsync();
var tokenData = JsonSerializer.Deserialize(responseBody);
if (tokenData.TryGetProperty("error", out var error))
{
var errorDescription = tokenData.TryGetProperty("error_description", out var desc)
? desc.GetString()
: "Unknown error";
throw new Exception($"Authentication failed: {errorDescription}");
}
return new TokenResponse
{
AccessToken = tokenData.GetProperty("access_token").GetString(),
RefreshToken = tokenData.TryGetProperty("refresh_token", out var refresh)
? refresh.GetString()
: null,
ExpiresIn = tokenData.GetProperty("expires_in").GetInt32()
};
}
}
public class TokenResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public int ExpiresIn { get; set; }
}
// Example usage
class Program
{
static async Task Main()
{
try
{
var tokenResponse = await RopcAuth.GetTokenAsync(
"user@example.com",
"yourpassword"
);
Console.WriteLine($"Access Token: {tokenResponse.AccessToken}");
Console.WriteLine($"Expires in: {tokenResponse.ExpiresIn} seconds");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
```
This authentication method is not recommended, but sometimes necessary.
***
## Final guidance
**Important:** Embedded Corti Assistant requires user-based authentication. **Client credentials grant is NOT supported** as it does not provide user context.
Pick the flow that matches your interaction model as offered above. See further details and contact us for [support here](mailto:help@corti.aien/articles/11156327-choosing-the-right-oauth2-flow-for-corti-integrations).
**What to use when:**
* **For embedded Corti Assistant** (e.g., in an iFrame/WebView): \
**Use Authorization Code Flow with PKCE** to authenticate the end user securely. This is the recommended flow for most embedded integrations.
* **For server-side integrations** where you can securely store a client secret: \
**Authorization Code Flow (without PKCE)** may be used, but ensure the client secret is never exposed to the frontend.
* **For constrained environments** without redirect capabilities: \
**ROPC** may be used with caution, but only in trusted internal environments.
* **Client Credentials is NOT supported** — it does not provide user context and will not work with embedded Corti Assistant.
**Additional reference**:
* [OAuth 2.0 Authorization Framework (RFC 6749)](https://datatracker.ietf.org/doc/html/rfc6749)
* [IBM](https://www.ibm.com/think/topics/oauth)
# Configuration Guide
Source: https://docs.corti.ai/assistant/configuration
In-depth reference for configuring the embedded Assistant interface, features, appearance, and locale settings
The `configure` method allows you to customize the Corti Assistant interface to match your application's needs and user preferences. This guide provides a comprehensive reference for all available configuration options, their behavior, and best practices for customization.
This guide documents the deprecated `configure()` structure.
For new integrations, use the [Config Migration Guide](/assistant/configuration-migration) and then implement the new structure with [Configuration Scenarios](/assistant/configuration-scenarios). For rollout timing, see [Scheduled Deprecations](/assistant/deprecation-timeline).
## Overview
Configuration is applied using the `configure` method, which accepts three main categories of settings:
* **Features**: Toggle UI components and functionality
* **Appearance**: Customize visual styling and branding
* **Locale**: Set interface and dictation languages, plus custom string overrides
Configuration can be applied incrementally. You can call `configure` with only
the properties you want to change, and the rest will remain at their current
values. The method returns the full current configuration object, allowing you
to read the current state.
For implementation examples, see the [PostMessage API](/assistant/postmessage-api) or [Window API](/assistant/window-api) documentation.
## Feature toggles
Feature toggles control which UI components and functionality are visible and available in the embedded Assistant. Each feature can be independently enabled or disabled, allowing you to create a tailored experience that matches your application's workflow and user needs.
### interactionTitle
**Type**: `boolean`\
**Default**: `true`
Controls whether the interaction title field is displayed in the interface. The title typically shows the encounter title that was provided when creating the interaction.
**When to disable:**
* You want a more minimal, streamlined interface
* You're managing the interaction title externally in your application
* The title is redundant with information already displayed in your host application
**When to keep enabled:**
* Users need to see or edit the interaction title within the Assistant
* The title provides important context for the current session
* You want users to be able to identify different interactions
**Impact**: When disabled, the title field is completely hidden from the interface, reducing visual clutter but removing the ability for users to see or modify the interaction title within the Assistant.
### aiChat
**Type**: `boolean`\
**Default**: `true`
Controls whether the AI chat feature is available to users. The AI chat allows users to interact with Corti's AI assistant for questions, clarifications, and document-related queries.
**When to disable:**
* You want to restrict AI chat functionality for compliance or policy reasons
* Your workflow doesn't require AI assistance features
* You're providing alternative support channels for user questions
**When to keep enabled:**
* Users benefit from AI-powered assistance during documentation
* You want to provide contextual help and clarifications
* AI chat enhances the user experience and productivity
**Impact**: When disabled, all AI chat UI elements and functionality are hidden. Users will not be able to access AI assistance features within the Assistant interface.
### documentFeedback
**Type**: `boolean`\
**Default**: `true`
Controls whether users can provide feedback on generated documents. This includes the ability to rate document quality, report issues, or provide corrections.
**When to disable:**
* You handle document feedback through external systems
* Feedback collection is not part of your workflow
* You want to simplify the interface by removing feedback mechanisms
**When to keep enabled:**
* You want to collect user feedback to improve document quality
* Feedback helps identify issues or areas for improvement
* You want users to have a way to report problems with generated documents
**Impact**: When disabled, all document feedback controls and related UI are hidden. Users will not be able to provide feedback on documents directly within the Assistant.
### navigation
**Type**: `boolean`\
**Default**: `false`
Controls whether the navigation sidebar is visible, providing access to settings, archive, and other areas of the Assistant - primarily allowing them to freely revisit previous interactions and even create new ones.
**When to enable:**
* You want users to have full access to Assistant features
* Users should be able to access settings and preferences
* Archive functionality is needed for your workflow
**When to keep disabled:**
* You want a focused, minimal interface for specific workflows
* Navigation is handled externally in your application
* You're creating a single-purpose integration that doesn't need full navigation
**Impact**: When enabled, the navigation sidebar becomes visible, allowing users to access:
* `/settings/preferences` - Edit defaults like languages and default session settings
* `/settings/input` - Edit dictation input settings
* `/settings/account` - Edit general account settings
* `/settings/archive` - View items in and restore from archive
**Best Practice**: Enable navigation when you want users to have self-service access to Assistant features. Disable it for tightly controlled, workflow-specific integrations where you manage navigation externally.
When **disabled**, users can only access the current interaction. With navigation **enabled**, users can freely access previous interactions or even create new ones. Consider your workflow needs and user preferences when deciding whether to enable navigation.
### virtualMode
**Type**: `boolean`\
**Default**: `true`
Controls whether the interface operates in virtual mode (for remote/telemedicine consultations) or live mode (for in-person consultations). This affects the recording interface, audio handling, and user experience.
**When to set to `false` (live mode):**
* Consultations are conducted in-person
* Recording happens locally or through local audio devices
* You're integrating with physical clinic environments
* The consultation type is explicitly in-person
**When to keep as `true` (virtual mode):**
* Consultations are conducted remotely (telemedicine)
* Audio is captured through web-based or remote systems
* The default virtual consultation workflow applies
* Remote consultation is the primary or only mode
**Impact**:
* **Virtual mode (`true`)**: Optimized for remote consultations with web-based audio capture
* **Live mode (`false`)**: Optimized for in-person consultations with local audio handling
**Best Practice**: Match this setting to your actual consultation type. If your application supports both modes, you may want to configure this dynamically based on the specific encounter type.
### syncDocumentAction
**Type**: `boolean`\
**Default**: `false`
Controls whether the "Synchronize document" button is available for syncing documents directly to EHR systems or external systems.
**When to enable:**
* You want users to be able to sync documents directly from the Assistant interface
* Direct EHR synchronization is part of your workflow
* Users need immediate access to sync functionality
**When to keep disabled:**
* Document syncing is handled externally through your application
* You manage document export through your own systems
* You want to control the sync process outside the Assistant
**Impact**: When enabled, the synchronize document button appears in the document interface. The button text can be customized using locale overrides (see [String Overrides](#string-overrides)).
**Best Practice**: Enable this if you want users to have direct control over document synchronization. If you handle syncing programmatically through your application, you may prefer to keep this disabled and manage syncing externally.
### templateEditor
**Type**: `boolean`\
**Default**: `true`
Controls whether the Template Assembler is available to end users. When enabled, it allows users to create custom templates for documentation tailored to their needs and preferences.
**When to enable:**
* You want to give end users flexibility to create and tailor their own templates.
**When to keep disabled:**
* You want end users to work only with a predefined, limited set of templates.
* Your integration relies on a fixed mapping of section keys, and you’re not set up to support an evolving list of new section keys.
**Impact**: When the Template Assembler is disabled, users are restricted to a fixed set of templates and cannot customize them.
**Best Practice**: Keep the Template Assembler enabled unless there is a strong reason to restrict template customization.
## Appearance Customization
### Primary Color
**Type**: `string | null`\
**Default**: `null` (uses built-in default theme)
You can customize the primary accent color used throughout the Corti Assistant interface to match your application's branding. This color is applied to interactive elements, buttons, links, focus indicators, and other accent elements throughout the interface.
**Format**: Hex color code as a string (e.g., `"#00a6ff"`, `"#1a73e8"`)
**When to customize:**
* You want to maintain brand consistency with your host application
* Your application has a specific brand color palette
* You need to match existing design systems or style guides
* Visual consistency across your integrated experience is important
**When to use default:**
* Brand consistency is not a priority
* You prefer Corti's default accessible color scheme
* You want to minimize customization complexity
**Impact**: The primary color affects:
* Button backgrounds and hover states
* Link colors
* Focus indicators and active states
* Accent elements and highlights
* Selected states and active UI elements
**Important considerations:**
* The color must meet WCAG 2.2 AA contrast requirements (see [WCAG Compliance](#wcag-22-aa-compliance))
* The color is applied across all UI states (default, hover, active, focus, disabled)
* Test thoroughly to ensure accessibility and usability
```javascript theme={null}
await api.configure({
appearance: {
primaryColor: "#00a6ff",
},
});
```
### WCAG 2.2 AA compliance
Always ensure WCAG 2.2 AA conformance when customizing appearance
Corti Assistant's default theme has been evaluated against WCAG 2.2 Level AA and meets applicable success criteria in our supported browsers.
**Important**: This conformance claim applies only to the default configuration. Customer changes (e.g., color palettes, CSS overrides, third-party widgets, or content) are outside the scope of this claim. Customers are responsible for ensuring their customizations continue to meet WCAG 2.2 AA.
#### Required WCAG 2.2 AA criteria
When supplying a custom accent color or theme, you must ensure WCAG 2.2 AA conformance, including:
* **1.4.3 Contrast (Minimum)**:
* Normal text: ≥ 4.5:1 contrast ratio
* Large text: ≥ 3:1 contrast ratio
* **1.4.11 Non-text Contrast**:
* UI boundaries, focus rings, and selected states: ≥ 3:1 contrast ratio
* **2.4.11 Focus Not Obscured (Minimum)**:
* Focus indicators must remain visible and unobstructed
#### Best practice for color customization
1. **Test contrast ratios**: Use tools like [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) to verify your color choices
2. **Test all states**: Verify contrast for default, hover, active, disabled, and focus states
3. **Test on all backgrounds**: Ensure your color works on both light and dark backgrounds if applicable
4. **Maintain focus visibility**: Ensure focus indicators remain clearly visible with your custom color
5. **Consider color blindness**: Test with color blindness simulators to ensure usability
Corti provides accessible defaults. If you override them, verify contrast for
all states (default, hover, active, disabled, focus) and on all backgrounds
you use.
## Locale settings
Locale settings control the language used for the interface and dictation, as well as allow for custom string overrides.
### Interface Language
**Type**: `string | null`\
**Default**: `null` (uses user's default or browser setting)
Sets the language for the user interface, including buttons, labels, menus, messages, and all UI text. This is separate from the dictation language, allowing you to have a German interface with English dictation, for example.
**Format**: Language code as a string (e.g., `"en"`, `"de-DE"`, `"fr-FR"`)
**When to set:**
* Your application serves users in specific languages
* You want to ensure a consistent language experience
* Users prefer a specific interface language regardless of browser settings
* You're building a localized application
**When to use default (`null`):**
* You want to respect user browser/OS language preferences
* Language detection should be automatic
* You support multiple languages and want automatic selection
**Available languages**: See [Available Interface Languages](#available-interface-languages)
**Best Practice**: Set the interface language based on your application's user preferences or locale settings. You can also allow users to change it if navigation is enabled.
### Dictation Language
**Type**: `string`\
**Default**: `"en"`
Sets the language for speech recognition and dictation. This determines which language model is used for transcribing spoken audio. The dictation language can be different from the interface language.
**Format**: Language code as a string (e.g., `"en"`, `"de"`, `"fr"`, `"da-DK"`)
**When to customize:**
* Users dictate in a language different from the interface language
* You need to support multilingual dictation
* Regional variations require specific language codes (e.g., `"en-GB"` vs `"en"`)
**Important Notes:**
* **Region-specific**: Different regions (EU, US) support different dictation languages
* **Separate from interface language**: Interface and dictation languages are independent
* **Must be valid**: The language code must be supported in your region (see [Available Dictation Languages](#available-dictation-languages))
**Available languages**: See [Available Dictation Languages](#available-dictation-languages)
**Best Practice**: Set the dictation language based on the actual language users will speak during consultations. This may differ from the interface language, especially in multilingual environments.
### String Overrides
**Type**: `Record`\
**Default**: `{}` (empty object)
Allows you to customize specific UI strings in the interface by providing key-value pairs where keys are string identifiers and values are the replacement text.
**Format**: Object with string keys and string values:
```typescript theme={null}
{
"string.key": "Custom text",
"another.key": "Another custom text"
}
```
**When to use:**
* You need to customize button labels to match your terminology
* You want to localize specific strings that aren't covered by interface language
* You need to match your application's terminology (e.g., "EHR" vs "EMR")
* You want to provide context-specific labels
**Limitations:**
* Only specific keys are available for override (see [Available String Overrides](#available-string-overrides))
* Not all UI strings can be customized
* Overrides apply regardless of interface language
**Best practice**: Use string overrides sparingly for critical terminology that must match your application. For broader localization, use the interface language setting instead.
#### Available string overrides
Currently, the following keys are exposed for override:
| Key | Default value | Purpose |
| --------------------------------------- | ------------------------ | ------------------------------------------------------------------- |
| `interview.document.syncDocument.label` | *"Synchronize document"* | The button text for the *"synchronize document"* button if enabled. |
More string override keys may be added in future releases. Contact support if
you need additional strings customized.
### Available interface languages
Updated February 2026
| Language code | Language |
| ------------- | ----------------- |
| `en` | English |
| `de-DE` | German |
| `fr-FR` | French |
| `it-IT` | Italian |
| `sv-SE` | Swedish |
| `da-DK` | Danish |
| `nb-NO` | Norwegian Bokmål |
| `nn-NO` | Norwegian Nynorsk |
### Available dictation languages
Updated February 2026
#### EU Region
| Language code | Language |
| ------------- | --------------- |
| `en` | English |
| `en-GB` | British English |
| `de` | German |
| `fr` | French |
| `sv` | Swedish |
| `da` | Danish |
| `nl` | Dutch |
| `no` | Norwegian |
#### US Region
| Language code | Language |
| ------------- | -------- |
| `en` | English |
## Configuration structure
The `configure` action accepts a configuration object with three main sections:
```typescript theme={null}
{
features: {
interactionTitle: boolean,
aiChat: boolean,
documentFeedback: boolean,
navigation: boolean,
virtualMode: boolean,
syncDocumentAction: boolean
templateEditor: boolean
},
appearance: {
primaryColor: string | null
},
locale: {
interfaceLanguage: string | null,
dictationLanguage: string,
overrides: Record
}
}
```
All properties are optional. You can configure only the sections you need, and the rest will maintain their current values. The action returns the complete current configuration object, allowing you to read the current state.
## Best practice
### Minimum supported resolution
To ensure a reliable and accessible experience, Assistant requires a minimum viewport size depending on how it is deployed.
Resolution requirements are defined by **functional guarantees**, not device types. At the minimum supported size, all critical workflows must remain fully usable without layout breakage or hidden actions.
#### Embedded (iframe / SDK)
* **Recommended Width:** ≥ `768px` (Note: Template Preview in the Template Picker Modal is not supported below this width)
* **Minimum Width:** ≥ `640px`
* **Minimum Height:** ≥ `480px`
At minimum width:
* No horizontal scrolling is required.
* Primary actions (recording, transcript, facts, document editing) remain visible and accessible.
* All core workflows can be completed without layout overlap or clipping.
If embedded in a narrower container, the Embedded Assistant will switch to a compact layout. Below the minimum supported width, layout integrity is not guaranteed.
As an integrator, you should ensure that your embedding container respects the
minimum supported width to guarantee full functionality.
#### Narrow containers (mobile)
The app calls `getIsMobile()`, which returns true if browser UA matches: `Mobile|Android|iPhone|iPad|iPod`.
So the “switch point” is effectively: when the browser identifies as a mobile device, not at XY pixels wide.
#### Accessibility requirements
At the minimum supported resolution, Assistant:
* Complies with WCAG 2.2 AA reflow requirements.
* Supports up to **200% browser zoom** without loss of functionality.
* Does not require two-dimensional scrolling to complete primary workflows.
### Configuration timing and lifecycle
**Apply configuration early**: Configure the Assistant as soon as it's ready (after the `ready` event) and before users interact with it. This ensures a consistent experience from the start.
**Configure before navigation**: If you're navigating to specific routes, set your configuration first to ensure the target page loads with the correct settings.
**Reconfigure dynamically**: You can update configuration at any time during a session. This is useful for:
* Adapting to different user roles or contexts
* Responding to user preferences changes
* Switching between different workflow modes
**Configuration persistence**: Configuration settings persist for the duration of the session but do not persist across page reloads or new sessions. You'll need to reapply configuration each time the Assistant is initialized.
### Incremental configuration
The `configure` action supports incremental updates. You can update only specific sections without affecting others:
* Update only `features` to change UI visibility
* Update only `appearance` to change branding
* Update only `locale` to change language settings
This allows you to make targeted changes without needing to specify the entire configuration object each time.
### Reading current configuration
The `configure` action always returns the complete current configuration object, regardless of what you pass in. This allows you to:
* Read the current state of all settings
* Verify that your configuration was applied correctly
* Build upon existing configuration rather than replacing it entirely
### Error handling
Always implement proper error handling when configuring:
* **Handle configuration failures**: Configuration may fail due to invalid values, network issues, or other errors
* **Provide fallbacks**: Have default configuration ready in case of failures
* **Log errors appropriately**: Log configuration errors for debugging while maintaining user experience
* **Validate before applying**: Validate configuration values (especially colors and language codes) before sending them
### Accessibility best practice
**Color customization:**
* Always test color contrast ratios using tools like [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
* Test all UI states: default, hover, active, disabled, and focus
* Ensure focus indicators remain clearly visible
* Test on both light and dark backgrounds if applicable
* Consider color blindness: Test with simulators to ensure usability
**Feature toggles:**
* Ensure that disabling features doesn't break keyboard navigation
* Verify that screen readers can still navigate the interface
* Test that essential functionality remains accessible when features are disabled
* Consider the impact on users who rely on specific features
**Language settings:**
* Ensure interface language changes don't break layout or functionality
* Test that all UI elements are properly localized
* Verify that right-to-left languages (if supported) work correctly
### Performance optimization
**Configure once at startup**: Apply your primary configuration once when the Assistant is ready, rather than repeatedly calling `configure`.
**Batch configuration changes**: If you need to change multiple settings, do it in a single `configure` call rather than multiple separate calls.
**Cache configuration objects**: Store your configuration objects and reuse them across sessions to avoid reconstructing them each time.
**Minimize reconfiguration**: Avoid unnecessary reconfiguration during a session. Only update configuration when user preferences or context actually change.
### User experience guidelines
**Match user workflows**: Configure features based on your users' actual workflow needs. Consider:
* What features do users actually need for their tasks?
* What can be simplified or hidden to reduce cognitive load?
* How does the Assistant fit into the broader application workflow?
**Consistent branding**: Use appearance settings to maintain visual consistency with your host application. This creates a more cohesive, integrated experience.
**Respect user preferences**:
* Use interface language settings that match your application's user preferences
* Set dictation language based on the actual language users will speak
* Consider allowing users to change settings if navigation is enabled
**Context-aware configuration**:
* Configure differently for different user roles (e.g., physicians vs. nurses)
* Adapt configuration based on consultation type (virtual vs. in-person)
* Consider workflow-specific configurations for different use cases
**Progressive disclosure**: Start with a minimal configuration and enable additional features as needed. This reduces initial complexity while allowing power users to access advanced features.
### Security and compliance
**No sensitive data in configuration**: Configuration values are visible in client-side code. Never include sensitive information like API keys, tokens, or personal data in configuration.
**Validate user permissions**: If you're configuring based on user roles or permissions, validate those permissions server-side before applying configuration.
**Compliance considerations**:
* Ensure customizations maintain compliance with relevant regulations (HIPAA, GDPR, etc.)
* Document any customizations that affect compliance
* Test that disabled features don't impact required functionality
### Testing recommendations
**Test all feature combinations**: Test how different feature toggle combinations affect the interface and functionality.
**Test color customizations thoroughly**:
* Test with various color values
* Verify accessibility in all states
* Test with different screen sizes and resolutions
**Test language settings**:
* Verify all supported languages work correctly
* Test interface language and dictation language combinations
* Ensure string overrides work as expected
**Test incremental updates**: Verify that partial configuration updates work correctly and don't reset other settings.
**Test error scenarios**: Test what happens when invalid configuration values are provided.
## Related documentation
* [configure() (Deprecated)](/assistant/api/configure) - Complete reference for the deprecated `configure` method
* [PostMessage API](/assistant/postmessage-api) - Learn how to use the PostMessage integration method
* [Window API](/assistant/window-api) - Learn how to use the Window API integration method
* [Welcome](/assistant/welcome) - Overview of Corti Assistant and integration options
* [Proxy Guide](/assistant/proxy) - How to run the embedded Assistant behind a reverse proxy
Please [contact us](mailto:help@corti.ai) for help or questions about configuration.
# Config Migration Guide
Source: https://docs.corti.ai/assistant/configuration-migration
Move from configure() and configureSession() to configureApp() and setInteractionOptions().
Use this guide to move an existing Embedded API integration from the legacy configuration structure to the new split between app-level configuration and interaction options.
Due by 2026-11-29. `configure()` and `configureSession()` remain supported
until then, but new integrations should move to `configureApp()` and
`setInteractionOptions()` now. See [Scheduled
Deprecations](/assistant/deprecation-timeline) for rollout timing.
## Overview
The new configuration structure separates two responsibilities:
* `configureApp()` is for app-level configuration such as UI settings, appearance, locale, and network settings. It is patchable and you may call it multiple times.
* `setInteractionOptions()` is for interaction or session-level configuration such as mode, spoken language, template defaults, and document actions. Set it before the user starts or opens an interaction.
`configure()` and `configureSession()` keep working during the deprecation
period. Use [Scheduled Deprecations](/assistant/deprecation-timeline) if you
need rollout timing and compatibility details.
## Reference
### Mapping from `configure()`
The second column shows the request shape to use in the new method call, not a response path.
| Current field | Use in | Notes |
| ----------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `features.interactionTitle` | `configureApp({ ui: { interactionTitle: ... } })` | App-level UI setting |
| `features.aiChat` | `configureApp({ ui: { aiChat: ... } })` | App-level UI setting |
| `features.documentFeedback` | `configureApp({ ui: { documentFeedback: ... } })` | App-level UI setting |
| `features.navigation` | `configureApp({ ui: { navigation: ... } })` | App-level UI setting |
| `features.virtualMode` | `setInteractionOptions({ mode: { options: [...] } })` | Use `options: ["in-person", "virtual"]` when virtual mode should be available, or `options: ["in-person"]` when it should not be available |
| `features.syncDocumentAction` | `setInteractionOptions({ documents: { actions: { sync: ... } } })` | Interaction-level document action |
| `features.templateEditor` | `setInteractionOptions({ templates: { sources: { personal: { enabled: ... } } } })` | Keeps the existing personal template capability |
| `appearance.primaryColor` | `configureApp({ appearance: { primaryColor: ... } })` | App branding |
| `locale.interfaceLanguage` | `configureApp({ locale: { interfaceLanguage: ... } })` | UI language |
| `locale.dictationLanguage` | `configureApp({ locale: { dictationLanguage: ... } })` | Default dictation language |
| `locale.overrides` | `configureApp({ locale: { overrides: ... } })` | String overrides |
| `network.websocketBaseUrl` | `configureApp({ network: { websocketBaseUrl: ... } })` | Proxy-only WebSocket endpoint override |
`configureApp({ debug: true })` enables a debug panel for development.
There is no legacy `configure()` equivalent for this setting, and you should
not enable it in staging or production.
### Mapping from `configureSession()`
| Current field | Use in | Notes |
| ----------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `defaultMode` | `setInteractionOptions({ mode: { fallback: ... } })` | Pair this with `mode.options` to decide which modes are available |
| `defaultLanguage` | `setInteractionOptions({ spokenLanguage: { fallback: ... } })` | Interaction-level spoken language fallback |
| `defaultTemplateKey` | `setInteractionOptions({ templates: { defaultTemplate: ... } })` | Use `source: "standard"`, `behaviour: "fallback"`, and the fully resolved template id |
| `defaultOutputLanguage` | `templates.defaultTemplate.template.id` | Compose the new template id from the old key plus language, for example `corti-soap` + `en` becomes `corti-soap-en` |
## Migration path
Move UI, appearance, locale, and network settings out of `configure()` and into `configureApp()`.
Move mode, spoken language, template defaults, document sync, and personal
template availability out of `configure()` or `configureSession()` and into
`setInteractionOptions()`.
## Before and after examples
### App appearance and UI settings
### Development-only debug panel
Use `debug` when you need the embedded Assistant debug panel during local
development or troubleshooting.
```javascript theme={null}
await api.configureApp({
debug: true,
});
```
The debug panel is intended for development only. Keep `debug` disabled in
staging and production.
### Interface language, dictation language, and string overrides
### Default mode and virtual mode
Use `mode.options` to replace `features.virtualMode` and `mode.fallback` to replace `defaultMode`.
When `virtualMode` is `false`, the key migration step is to remove `"virtual"` from `mode.options`.
### Default template
This remains fallback behavior, just like before. Map `defaultTemplateKey` to a standard template source and pass the fully resolved template id.
In the new API, pass the fully resolved template id. If the old configuration used `defaultTemplateKey: "corti-soap"` together with `defaultOutputLanguage: "en"`, the new `template.id` should be `"corti-soap-en"`.
The old configuration resolved the selected template from the pair
`defaultTemplateKey` plus `defaultOutputLanguage`. In the new API, you must
pass the already resolved template id directly, for example `corti-soap-en`.
### Document sync action
***
### usage
Emitted when credits have been consumed because of either of these triggers:
* **Ending/pausing a recording**: credits consumed for transcription and fact extraction
* **Ending dictation**: credits consumed for transcription
* **Generating a document**: credits consumed for text generation
```typescript theme={null}
{
type: 'CORTI_EMBEDDED_EVENT',
event: 'usage',
payload: {
creditsConsumed: 0.13,
}
}
```
This value is not accumulative and only refers to the latest trigger.
**Replacement:** Use [`account.creditsConsumed`](/assistant/events/generated/account/creditsConsumed) instead.
***
### Listening for legacy events
```javascript theme={null}
window.addEventListener("message", (event) => {
if (event.data?.type === "CORTI_EMBEDDED_EVENT") {
switch (event.data.event) {
case "ready":
console.log("Embedded app ready");
break;
case "loaded":
console.log("Navigation completed:", event.data.payload.path);
break;
case "documentGenerated":
console.log("Document generated:", event.data.payload.document);
break;
case "documentUpdated":
console.log("Document updated:", event.data.payload.document);
break;
case "documentSynced":
console.log("Document synced:", event.data.payload.document);
break;
case "recordingStarted":
console.log("Recording started");
break;
case "recordingStopped":
console.log("Recording stopped");
break;
default:
console.log("Unknown event:", event.data.event);
}
}
});
```
## Timeline
* **Current**: Both legacy and new events are dispatched during the migration window
* **Future**: Legacy event support ends on 2026-08-20
* **Action required**: Migrate to the new event format before the deadline
## Related pages
Review the current Embedded API event names, payloads, and generated reference pages.
Check rollout timing and upcoming shutdown dates for embedded changes.
Follow release announcements related to Embedded API changes and migrations.
# Embedding Assistant in React Web App
Source: https://docs.corti.ai/assistant/guides/react-integration
Complete guide for integrating Corti Assistant into a React web application
This guide walks you through integrating Corti Assistant into a React web application. You'll learn the key concepts, architecture patterns, and implementation approach using the Web Component API.
**Complete working example available**: This guide references the [basic React example](https://github.com/corticph/corti-examples/tree/main/embedded-assistant/react/basic-example) in our examples repository. Clone it to see a fully functional implementation, then follow this guide to understand how it works and adapt it to your needs.
This guide uses ROPC (Resource Owner Password Credentials) authentication, matching the React basic example.
***
## What you'll learn
By following this guide, you'll understand:
* How to architect a secure client-server integration
* Why ROPC authentication works well for embedded EHR scenarios
* How to use the Web Component API with React
* How to manage the interaction lifecycle programmatically
* Key customization points and production considerations
## Prerequisites
Before starting, ensure you have:
* **Node.js 18+** installed
* **Corti API access** with valid credentials
* **OAuth client** configured for ROPC (Resource Owner Password Credentials) flow
* **User account** created in Corti Console for authentication
* Basic familiarity with React and Express
Don't have credentials yet? Visit [Corti Console](https://console.corti.app) to create a user and OAuth client. Ensure your OAuth client has "Direct Access Grants" enabled for ROPC flow.
***
## Architecture overview
The integration follows a client-server architecture:
**Backend (Express Server)**:
* Handles OAuth authentication using ROPC flow via `@corti/sdk`
* Provides configuration endpoint for environment-specific settings
* Keeps sensitive credentials server-side
* Runs on port 8013 by default
**Frontend (React + Vite)**:
* Fetches auth tokens and configuration from backend
* Embeds Corti Assistant using `@corti/embedded-web` Web Component
* Manages interaction lifecycle (create, navigate, handle events)
* Uses React 19's Suspense for async data loading
* Runs on port 8015, with `/api` proxied to the backend
**Why this architecture?**
* **Security**: Credentials stay on the server, never exposed to the browser
* **Separation of concerns**: Auth logic separate from UI logic
* **Development speed**: Simple to run and test locally
***
## Key concepts
### Authentication flow
1. **Backend authenticates** with Corti using stored credentials (ROPC)
2. **Frontend requests** tokens from backend `/api/auth` endpoint
3. **Web Component** receives tokens and authenticates the embedded session
4. **No user interaction** required - seamless experience
**Why ROPC?** Users are already logged into your application. Forcing them to re-authenticate with Corti via browser popup would be confusing and disruptive. ROPC allows seamless embedded authentication.
### Web Component integration
The integration uses ``, a React wrapper around the standard Web Component:
```tsx theme={null}
import {
CortiEmbeddedReact,
useCortiEmbeddedApi,
} from "@corti/embedded-web/react";
// In your component:
;
```
The basic React example keeps the embedded Assistant hidden during navigation, then sets `visibility` to `"visible"` after you receive `embedded.navigated` or `interaction.loaded` (interaction ready). For timeout and retry guidance around this pattern, see [Embedded Reliability, Timeouts, and Recovery](/assistant/reliability-timeouts).
**Key benefits**:
* **Type-safe**: Full TypeScript support with IDE autocomplete
* **React-friendly**: Hooks-based API (`useCortiEmbeddedApi`)
* **Event handling**: Native React event props for Assistant callbacks
* **Lifecycle management**: Automatic cleanup and ref management
### Interaction lifecycle
Every clinical encounter in Corti Assistant is called an "interaction":
1. **Create interaction** - Define encounter metadata (type, identifier, timestamps)
2. **Navigate to session** - Load the session view with recording interface
3. **User records** - Clinician documents the encounter
4. **Handle events** - React to document generation, errors, state changes
**Why create interactions programmatically?** This allows you to:
* Link Corti sessions to your EHR's encounter IDs
* Pre-fill encounter metadata from your system
* Control when and how sessions start
* Track interactions in your database
***
## Implementation guide
### Project structure
```text theme={null}
your-react-app/
├── server.ts # Backend: Auth + config endpoints
├── src/
│ ├── App.tsx # Root: Suspense boundary + data fetching
│ ├── components/
│ │ └── EmbeddedAssistant.tsx # Main integration component
│ ├── lib/
│ │ └── auth.ts # API calls to backend
│ └── types.ts # TypeScript definitions
├── .env # Credentials (never commit!)
└── package.json
```
### Step 1: Backend setup
**Purpose**: Securely authenticate with Corti and provide tokens to the frontend.
**Key file**: `server.ts`
**What it does**:
* `/api/auth` - Uses `@corti/sdk` to perform ROPC authentication
* `/api/config` - Returns base URL based on environment (EU/US)
* `/api/health` - Provides a simple backend health check
* CORS configured for the local Vite dev server origin
* Backend port defaults to 8013; frontend origin defaults to `http://localhost:8015`
**Implementation approach**:
```typescript theme={null}
// Uses Corti SDK for authentication
const auth = new CortiAuth({
environment: process.env.CORTI_ENVIRONMENT,
tenantName: process.env.CORTI_TENANT_NAME,
});
// ROPC flow - exchanges username/password for tokens
const tokenResponse = await auth.getRopcFlowToken({
clientId: process.env.CORTI_CLIENT_ID,
username: process.env.CORTI_USER_EMAIL,
password: process.env.CORTI_USER_PASSWORD,
});
// Return credentials in the shape expected by api.auth()
res.json({
access_token: tokenResponse.accessToken,
refresh_token: tokenResponse.refreshToken,
id_token: "",
token_type: tokenResponse.tokenType || "Bearer",
expires_in: tokenResponse.expiresIn,
mode: "stateful",
});
```
**See the full implementation**: [server.ts in example repo](https://github.com/corticph/corti-examples/blob/main/embedded-assistant/react/basic-example/server.ts)
### Step 2: Frontend data fetching
**Purpose**: Fetch auth tokens and config before rendering the Assistant.
**Key file**: `src/lib/auth.ts`
**What it does**:
* Promise-based API calls to backend
* Simple in-memory caching to prevent duplicate requests
* Separates data fetching logic from UI components
**Why separate?** Keeps components focused on rendering and keeps backend communication in one place.
**See the implementation**: [auth.ts in example repo](https://github.com/corticph/corti-examples/blob/main/embedded-assistant/react/basic-example/src/lib/auth.ts)
### Step 3: App root with suspense
**Purpose**: Coordinate parallel data fetching and handle loading states.
**Key file**: `src/App.tsx`
**Key pattern**: React 19's `use()` hook with Suspense for async data
```tsx theme={null}
function AppWithData({ configPromise, authPromise }) {
const config = use(configPromise); // Suspends until resolved
const authData = use(authPromise); // Fetched in parallel
if (authData.error) {
return ;
}
return ;
}
```
**Why this pattern?**
* **Parallel fetching**: Config and auth load simultaneously
* **Declarative loading**: Suspense fallback handles loading UI automatically
* **Error boundaries**: Easy to add error handling at the boundary level
**See the implementation**: [App.tsx in example repo](https://github.com/corticph/corti-examples/blob/main/embedded-assistant/react/basic-example/src/App.tsx)
### Step 4: Embedded Assistant component
**Purpose**: Render the Web Component and manage the interaction lifecycle.
**Key file**: `src/components/EmbeddedAssistant.tsx`
**Core responsibilities**:
1. Render `` with configuration
2. Use `useCortiEmbeddedApi` hook to access API methods
3. Handle `onReady` event to authenticate and configure the embedded app
4. Set interaction-level options before creating the interaction
5. Create the interaction, navigate to the session route, and show the embed after the interaction is ready
6. Manage status display, retry, and error states
**Critical pattern - preventing double initialization**:
```tsx theme={null}
const hasInitialized = useRef(false);
const handleReady = async () => {
if (hasInitialized.current) return; // Guard against React StrictMode
hasInitialized.current = true;
// Safe to authenticate and create interaction
};
```
**Why needed?** React 19's StrictMode intentionally double-invokes effects in development. Without this guard, you'd create two interactions.
**Event handling strategy**:
* `onReady` - Authenticate, configure app UI, set interaction options, create interaction, and navigate to session
* `onEvent` - Receive Assistant events for logging or workflow integration
* `onError` - Display errors to user
The example also adds lifecycle timeouts and a retry path around the critical events. See [Embedded Reliability, Timeouts, and Recovery](/assistant/reliability-timeouts) for the recommended behavior, and see the full implementation for details.
**API sequence**:
```tsx theme={null}
await api.auth(authData);
await api.configureApp({
ui: {
interactionTitle: true,
aiChat: true,
documentFeedback: true,
navigation: true,
},
});
await api.setInteractionOptions({
mode: {
fallback: "in-person",
options: ["in-person", "virtual"],
},
documents: {
actions: {
sync: true,
},
},
});
const interaction = await api.createInteraction({
assignedUserId: null,
encounter: {
identifier: `encounter-${Date.now()}`,
status: "planned",
type: "first_consultation",
period: { startedAt: new Date().toISOString() },
},
});
await api.navigate({ path: `/session/${interaction.id}` });
```
**See the implementation**: [EmbeddedAssistant.tsx in example repo](https://github.com/corticph/corti-examples/blob/main/embedded-assistant/react/basic-example/src/components/EmbeddedAssistant.tsx)
***
## Running the example
Clone and run the complete example:
```bash theme={null}
git clone https://github.com/corticph/corti-examples.git
cd corti-examples/embedded-assistant/react/basic-example
npm install
cp .env.example .env
# Edit .env with your credentials
npm run dev
```
Open [http://localhost:8015](http://localhost:8015) to see the integration in action. The backend runs at [http://localhost:8013](http://localhost:8013), and Vite proxies `/api` requests to that backend.
***
## Customization points
### Interaction creation
The example creates an interaction in `EmbeddedAssistant.tsx` after authentication and configuration:
```tsx theme={null}
const interaction = await api.createInteraction({
assignedUserId: null,
encounter: {
identifier: `encounter-${Date.now()}`,
status: "planned",
type: "first_consultation",
period: {
startedAt: new Date().toISOString(),
},
},
});
```
### Assistant configuration
The example calls `api.configureApp()` after authentication for app-level UI settings:
```tsx theme={null}
await api.configureApp({
ui: {
interactionTitle: true,
aiChat: true,
documentFeedback: true,
navigation: true,
},
});
```
The example calls `api.setInteractionOptions()` for interaction-level options:
```tsx theme={null}
await api.setInteractionOptions({
mode: {
fallback: "in-person",
options: ["in-person", "virtual"],
},
documents: {
actions: {
sync: true,
},
},
});
```
See [Configuration Reference](/assistant/configuration) for all options.
### Event and error handling
The example wires `onEvent` and `onError` callbacks on ``:
```tsx theme={null}
const handleEvent = () => {
// Events are logged internally by the component
};
const handleError = (event: CustomEvent) => {
setStatus({
message: `Error: ${event.detail?.message || "Unknown error"}`,
type: "error",
});
};
```
See [Events Reference](/assistant/events/index) for all available events.
***
## Production considerations
### Environment variables
Never commit credentials to version control:
```bash theme={null}
# Use environment-specific .env files
.env.development # Local dev credentials
.env.production # Production credentials (injected via CI/CD)
# Add to .gitignore
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore
```
### CORS configuration
Update CORS for production domains:
```typescript theme={null}
app.use(
cors({
origin: [
"https://your-production-domain.com",
process.env.CLIENT_ORIGIN || (process.env.NODE_ENV === "development" ? "http://localhost:8015" : null),
].filter(Boolean),
credentials: true,
}),
);
```
***
## Next steps
Build on the React basic example with the current Embedded API:
* **Document export** - Handle document events and save generated content to your host application
* **Session management** - Store created interaction IDs and navigate back with `api.navigate({ path: "/session/" })`
* **App configuration** - Use `api.configureApp()` for UI, appearance, locale, and network settings
* **Interaction options** - Use `api.setInteractionOptions()` for mode, document actions, spoken language, and templates
* **Event monitoring** - Use `onEvent` and `onError` to track Assistant activity in your application
***
## Troubleshooting
### CORS errors
If you see CORS errors, verify Vite's dev server origin matches the `CLIENT_ORIGIN` used by `server.ts`. The example uses strict port 8015 for Vite and port 8013 for the backend.
**Fix**: Set `CLIENT_ORIGIN` to match your frontend URL:
```bash theme={null}
CLIENT_ORIGIN=http://localhost:8015
```
### Authentication fails
* Verify credentials in `.env` match your Corti Console setup
* Ensure OAuth client has "Direct Access Grants" enabled for ROPC
* Check user account exists and is active in your Corti tenant
* Verify `CORTI_ENVIRONMENT` matches your tenant region (eu/us)
### Web Component not loading
* Check browser console for import errors
* Verify `@corti/embedded-web` package is installed correctly
* Ensure `baseURL` matches your region (`https://assistant.eu.corti.app`)
* Confirm microphone permissions are granted (required for recording)
### Double initialization in Dev Mode
This is expected with React 19 StrictMode. The `hasInitialized` ref guard prevents issues. In production builds, StrictMode is disabled and this won't occur.
***
## Related documentation
* [Web Component API Reference](/assistant/web-component-api)
* [Authentication Guide](/assistant/authentication)
* [Configuration Reference](/assistant/configuration)
* [API Reference](/assistant/api-reference)
* [Events Reference](/assistant/events/index)
* [Reliability Guide](/assistant/reliability-timeouts)
* [Example Repository](https://github.com/corticph/corti-examples/tree/main/embedded-assistant)
# Choose Your Integration
Source: https://docs.corti.ai/assistant/integrations-overview
Choose the right integration method for embedding Corti Assistant into your application
Corti Assistant is a full-featured clinical AI application that can be embedded directly into your healthcare application. It provides ambient documentation, dictation, clinical decision support, and more—all accessible through a programmable interface.
**Most common integration pattern:** C# desktop application → WebView → localhost HTML page → Web Component. This approach is used by most integrators and provides full control from your native application while leveraging web technologies for the UI layer.
**Ready to start building?** Check out the [Web Component examples on GitHub](https://github.com/corticph/corti-examples/tree/main/embedded-assistant) to see complete React and vanilla TypeScript implementations. The Web Component is the recommended integration method and provides the best developer experience.
**Production reliability:** Add timeout and retry handling for lifecycle
events such as `interaction.loaded`. See [Embedded Reliability, Timeouts,
and Recovery](/assistant/reliability-timeouts).
***
## Integration architecture
When you embed Corti Assistant, you're integrating a complete application into your own. This embedded Assistant communicates with your application through one of three integration methods:
```mermaid theme={null}
graph LR
A[Your Application] -->|Integration Layer| B[Corti Assistant]
B -->|Clinical Data| C[Corti Platform]
classDef nodeNeutral fill:#f9f9f9,stroke:#333,stroke-width:2px
classDef nodeBlue fill:#e8f4f8,stroke:#0066cc,stroke-width:2px
classDef nodeGray fill:#f0f0f0,stroke:#666,stroke-width:2px
class A nodeNeutral
class B nodeBlue
class C nodeGray
```
Your integration layer handles:
* **Authentication** - Managing user sessions with OAuth2 tokens
* **API Communication** - Creating interactions, generating documents, controlling recording
* **Event Handling** - Receiving updates about interactions, documents, and user actions
* **UI Embedding** - Displaying the Assistant interface in your application
***
## Three integration methods
Corti provides three ways to embed the Assistant, each optimized for different architectural needs:
### Quick decision guide
```mermaid theme={null}
graph TD
Start[Choose Integration Method] --> Q1{Starting a new integration?}
Q1 -->|Yes| WebComp[✅ Web Component Modern, recommended approach]
Q1 -->|No| Q2{Already using postMessage?}
Q2 -->|Yes| PostMsg[PostMessage API Continue with existing implementation]
Q2 -->|No| Q3{Same origin scenario?}
Q3 -->|Yes| WindowAPI[Window API Direct API access]
Q3 -->|No| WebComp2[✅ Web Component Cross-origin support]
classDef nodeGreen fill:#d4edda,stroke:#28a745,stroke-width:2px
classDef nodeYellow fill:#fff3cd,stroke:#ffc107,stroke-width:2px
classDef nodeCyan fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px
class WebComp,WebComp2 nodeGreen
class PostMsg nodeYellow
class WindowAPI nodeCyan
```
### Comparison table
Choose your integration method based on your architectural requirements:
| | Web Component | PostMessage | Window API |
| :----------------------- | :------------------------------------: | :----------------------------------: | :--------------------------------: |
| **Recommended For** | Native apps (C#) + WebView, web apps | Existing postMessage implementations | Same-origin / direct URL embedding |
| **Native App Support** | ✅ Yes (C#, .NET, Electron via WebView) | ✅ Yes (via WebView) | ⚠️ Same-origin only |
| **Framework Support** | Universal (React, Vue, Angular...) | Universal | Universal (same-origin only) |
| **Cross-Origin Support** | ✅ Yes | ✅ Yes | ❌ No (same-origin only) |
| **Type Safety** | Full TypeScript | Manual typing required | Full TypeScript |
| **API Style** | Promise-based methods | postMessage request/response | Promise-based methods |
| **Event Handling** | Native addEventListener | Manual postMessage parsing | Native addEventListener |
| **Ease of Use** | 5/5 | 3/5 | 4/5 |
| **React Support** | Built-in hooks and components | Manual implementation | Manual implementation |
| **Setup Complexity** | Install NPM package | Create iframe + message handlers | Load script + direct API access |
| **iframe Management** | Automatic | Manual | Manual (if using iframe) |
***
### Web Component (recommended)
```mermaid theme={null}
graph TB
subgraph "Path 1: Web Applications"
WA[React / Vue / Angular or any web framework]
end
subgraph "Path 2: Native Applications"
NA[C# / .NET / Electron Desktop Application]
NW[WebView Control loads HTML page]
NA -->|hosts| NW
end
subgraph "Integration Layer"
B["<corti-embedded> Web Component"]
C[Event Listeners]
end
subgraph "Corti Assistant"
D[Assistant Interface]
E[Corti Platform APIs]
end
WA -->|import and render| B
NW -->|import and render| B
B <-->|cross-origin iframe| D
D <--> E
B -->|events| C
classDef nodeBlue fill:#e8f4f8,stroke:#0066cc,stroke-width:2px
classDef nodePurple fill:#e8d4f8,stroke:#8b5cf6,stroke-width:2px
classDef nodeLightPurple fill:#f3e8ff,stroke:#8b5cf6,stroke-width:1px
classDef nodeGreen fill:#d4edda,stroke:#28a745,stroke-width:2px
class WA,D nodeBlue
class NA nodePurple
class NW nodeLightPurple
class B nodeGreen
```
A modern, framework-agnostic custom HTML element that wraps the Assistant in a cross-origin iframe. The Web Component handles all postMessage complexity internally and provides a clean TypeScript API.
**Works for both:**
* **Web applications** - React, Vue, Angular, or any framework (including server-rendered pages)
* **Native applications** - C#, .NET, Electron with WebView controls loading a local HTML page
**Best for:**
* Native desktop applications (C#, .NET, Electron) with WebView controls
* Web applications using React, Vue, Angular, or any modern framework
* Mobile applications with WebView components
* New integrations starting from scratch
* Teams that want TypeScript support and native event handling
* Cross-origin scenarios (different domains)
**Architecture highlights:**
* Standard Web Component that works everywhere
* Built-in React hooks and components for optimal DX
* Automatic iframe communication handling
* Promise-based API with TypeScript definitions
Learn how to integrate using the Web Component API
***
### PostMessage API
```mermaid theme={null}
graph TB
subgraph "Your Application"
A[Parent Window/Frame]
B[Manual iframe Management]
C[Custom postMessage Handler]
end
subgraph "Corti Assistant"
D[Assistant Interface]
E[Corti Platform APIs]
end
A -->|Create and Mount| B
B <-->|postMessage Protocol| D
D <--> E
D -->|postMessage Events| C
classDef nodeYellow fill:#fff3cd,stroke:#ffc107,stroke-width:2px
classDef nodeBlue fill:#e8f4f8,stroke:#0066cc,stroke-width:2px
class B nodeYellow
class D nodeBlue
```
Direct iframe integration where you manually manage the iframe element and communicate via browser's postMessage API. This is the low-level protocol that the Web Component abstracts.
**Best for:**
* Existing implementations already using postMessage
* Teams with specific iframe management requirements
* Legacy applications where adding new dependencies is difficult
**Architecture highlights:**
* Full control over iframe lifecycle and positioning
* Direct access to postMessage protocol
* Works cross-origin (different domains)
* Requires manual event parsing and error handling
Learn how to integrate using the PostMessage API
***
### Window API
```mermaid theme={null}
graph TB
subgraph "Your Application (same origin)"
A[Parent Window]
B[Direct Window Reference]
end
subgraph "Corti Assistant (same origin)"
D[Assistant Interface]
E[window.CortiEmbedded]
F[Corti Platform APIs]
end
A -->|window.open or iframe| B
B -->|Direct API Access| E
E <--> D
D <--> F
classDef nodeCyan fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px
classDef nodeBlue fill:#e8f4f8,stroke:#0066cc,stroke-width:2px
class B nodeCyan
class E nodeBlue
```
Same-origin integration where your application and Corti Assistant run on the same domain, allowing direct JavaScript API access through the global `window.CortiEmbedded` object.
**Best for:**
* Same-origin scenarios (both apps on same domain)
* Direct URL embedding in webviews or popup windows
* Mobile webview integrations where same-origin is guaranteed
* Scenarios requiring the simplest possible integration
**Architecture highlights:**
* No iframe or postMessage complexity
* Direct function calls to JavaScript API
* Only works when same-origin policy is satisfied
* Promise-based API with full TypeScript support
Learn how to integrate using the Window API
***
## Common integration flow
Regardless of which method you choose, the integration follows the same high-level flow:
Add the Corti Assistant interface to your application using your chosen integration method (Web Component, iframe, or window reference).
Implement OAuth2 authentication to obtain user tokens. All methods require user-based authentication with OAuth2 tokens (access, refresh, and ID tokens).
Learn how to set up OAuth2 authentication for your users
Pass authentication tokens to the Assistant and configure the interface (features, appearance, locale).
Use the API to create interactions that represent clinical encounters and manage their lifecycle.
Listen for events from the Assistant (document generated, recording started, errors, etc.) and update your application UI accordingly.
***
## Authentication requirements
All three integration methods require OAuth2 user authentication. See the [Authentication Guide](/assistant/authentication) for complete setup instructions.
All Embedded Assistant integrations require user-based OAuth2 authentication with `access_token`, `refresh_token`, and `id_token`. Client credentials and machine-to-machine flows are not supported.
***
## API capabilities
Once embedded and authenticated, all three integration methods provide access to the same comprehensive API:
* Authenticate users with OAuth2 tokens
* Refresh authentication tokens
* Configure interface features and appearance
* Set language and locale preferences
* Create new clinical interactions
* Manage interaction lifecycle
* Navigate between sessions
* Start and stop audio recording
* Control recording state
* Monitor recording status
* Handle recording events
* Add clinical facts to interactions
* Generate documents from captured data
* Retrieve available templates
* Configure document generation behavior
* Listen for interaction events
* Monitor document generation status
* Track recording state changes
* Handle errors and warnings
Full documentation of all API methods, parameters, and return types
***
## Next steps
Modern, recommended approach with framework support
Direct iframe control with manual postMessage handling
Same-origin integration with direct API access
Complete TypeScript and React examples on GitHub
# Introduction to Embedded Assistant API
Source: https://docs.corti.ai/assistant/introduction
Access an API for embedding Corti Assistant in your workflow
The Corti Embedded Assistant API enables seamless integration of Corti Assistant into host applications, such as Electronic Health Record (EHR) systems, web-based clinical portals, or native applications using embedded WebViews. The implementation provides a robust, consistent, and secure interface for parent applications to control and interact with embedded Corti Assistant.
The details outlined below are for you to embed the Corti Assistant "AI scribe solution" natively within your application. To learn more about the full Corti API, please see more [here](/api-reference/welcome)
***
## Quick start guides
Get started quickly with our platform-specific integration guides:
Complete guide for React applications with the Web Component API
**C#/.NET Desktop Guide Coming Soon**
A complete integration guide for WPF, WinForms, and .NET MAUI applications using WebView2 is coming in Q1 2026. In the meantime, the [Web Component API](/assistant/web-component-api) works with any WebView-based application.
***
## Overview
The Embedded Assistant API is a communication interface that allows your application to embed and control Corti Assistant within your own application interface. It provides programmatic control over authentication, session management, interaction creation, document generation, and more.
The API enables two-way communication between your application and the embedded Corti Assistant, allowing you to:
* Authenticate users and manage sessions
* Create and manage clinical interactions
* Configure the Assistant interface and appearance
* Control recording functionality
* Receive real-time events and updates
* Access generated documents and transcripts
***
## Requirements
Before getting started, ensure you have:
* **Created an OAuth Client for Corti Assistant**: You'll need to [create a Corti Assistant specific client](mailto:help@corti.aien/articles/11400088-creating-an-api-client#h_239039d8fe) from the [Developer Console](https://console.corti.app).
* **Modern browser or WebView**: For web applications, use a modern browser (Chrome, Firefox, Safari, or Edge). For native apps, use a modern WebView (WebView2, WKWebView, or Chromium-based WebView)
* **HTTPS**: The embedded Assistant must be loaded over HTTPS (required for microphone access)
* **Microphone permissions**: Your application must request and handle microphone permissions appropriately
* **OAuth2 client**: You'll need an OAuth2 client configured for user-based authentication
## Available regions
* **EU**: [https://assistant.eu.corti.app](https://assistant.eu.corti.app)
* **EU MD**: [https://assistantmd.eu.corti.app](https://assistantmd.eu.corti.app) (medical device compliant)
* **US**: [https://assistant.us.corti.app](https://assistant.us.corti.app)
## Integration methods
The Embedded Assistant API offers three integration methods. For most use cases, we recommend the **Web Component API** for its simplicity and broad compatibility.
For detailed comparison and guidance, see the [Integration Method Comparison Guide](/assistant/integrations-overview).
### Web Component API (recommended)
Works for all scenarios: iframe, WebView, same-origin, and cross-origin. Framework-agnostic with built-in React support and full TypeScript definitions.
[**Web Component API Documentation**](/assistant/web-component-api) | [**Full Examples**](https://github.com/corticph/corti-examples/tree/main/embedded-assistant)
### Alternative methods
* [**PostMessage API**](/assistant/postmessage-api) - Lower-level iframe communication (not recommended for new integrations)
* [**Window API**](/assistant/window-api) - Same-origin direct access for specific use cases
## Documentation
* [**Web Component API**](/assistant/web-component-api) (Recommended) - Complete guide with vanilla TypeScript and React examples
* [**PostMessage API**](/assistant/postmessage-api) - Lower-level API for specific use cases
* [**Window API**](/assistant/window-api) - Same-origin direct API access
* [**Integration Method Comparison**](/assistant/integrations-overview) - Detailed comparison and selection guide
* [**API Reference**](/assistant/api-reference) - Complete reference for all methods, events, and integration patterns
* [**Embedded Reliability**](/assistant/reliability-timeouts) - Timeout and retry guidance for critical lifecycle events
* [**OAuth Authentication**](/assistant/authentication) - Guide for implementing OAuth2 authentication flows
* [**Examples Repository**](https://github.com/corticph/corti-examples) - Full working examples
## Next steps
1. Follow a [platform-specific guide](#quick-start-guides) to get started quickly
2. Review the [OAuth Authentication Guide](/assistant/authentication) to set up user authentication
3. Choose your [integration method](/assistant/integrations-overview) based on your use case
4. Add [timeout and retry handling](/assistant/reliability-timeouts) for lifecycle events such as `interaction.loaded`
5. Consult the [API Reference](/assistant/api-reference) for all available methods and events
Please [contact us](mailto:help@corti.ai) for help or questions.
# PostMessage API
Source: https://docs.corti.ai/assistant/postmessage-api
Use the PostMessage API to integrate Corti Assistant via iframe or WebView
The PostMessage API enables secure cross-origin communication between your application and the embedded Corti Assistant. This method is suitable for iframe or WebView integrations requiring fine-grained control over the iframe lifecycle.
**Web Component API is recommended.** The [Web Component
API](/assistant/web-component-api) provides the same functionality with a
simpler interface. PostMessage API remains fully supported but involves
unnecessary complexity. It is not deprecated.
**Web Component has full working examples.** Complete, runnable examples are
available for the [Web Component integration
method](https://github.com/corticph/corti-examples/tree/main/embedded-assistant),
which is the recommended approach for most integrations. PostMessage API
examples demonstrating the lower-level protocol will be added to the
repository in the future.
## Overview
The PostMessage API uses the browser's `postMessage` mechanism to enable secure communication between your application and the embedded Corti Assistant, even when they're served from different origins. This makes it ideal for embedding Corti Assistant in iframes or WebViews.
## Requirements
Before getting started, ensure you have:
* **Access to Corti Assistant**: You'll need credentials and access to one of the available regions
* **HTTPS**: The embedded Assistant must be loaded over HTTPS (required for microphone access)
* **Microphone permissions**: Your application must request and handle microphone permissions appropriately
* **OAuth2 client**: You'll need an OAuth2 client configured for user-based authentication
* **Modern browser or WebView**: For web applications, use a modern browser. For native apps, use a modern WebView
## Recommendations
* **Validate message origins** to ensure security
* **Use specific target origins** instead of `'*'` when possible
* **Implement proper error handling** for all API calls
* **Handle authentication token refresh** to maintain user sessions
* **Request microphone permissions** before initializing the embedded Assistant
## Available regions
* **US**: [https://assistant.us.corti.app](https://assistant.us.corti.app)
* **EU**: [https://assistant.eu.corti.app](https://assistant.eu.corti.app)
* **EU MD**: [https://assistantmd.eu.corti.app](https://assistantmd.eu.corti.app) (medical device compliant)
## Features
* Secure cross-origin communication
* Works with any iframe or WebView implementation
* Fully asynchronous with request/response pattern
## Quick start
### Step 1: Set up authentication
Before using the PostMessage API, authenticate your users using OAuth2. See the [Authentication Guide](/assistant/authentication) for complete setup instructions including Authorization Code Flow with PKCE (recommended), obtaining tokens, and handling token refresh.
All Embedded Assistant integrations require user-based OAuth2 authentication.
Client credentials and machine-to-machine flows are not supported.
* Handle token refresh to maintain sessions
* Never expose client secrets in client-side code
### Step 2: Load the Embedded Assistant
Load the Corti Assistant in an iframe or WebView:
#### Required iframe `allow` permissions
When you embed Corti Assistant in an iframe, you must delegate the browser permissions that the embedded app needs.
* `microphone` is required for recording and dictation
* `display-capture` is required if you use virtual recording to capture audio from another tab, window, or screen
* `clipboard-write` is recommended so users can copy generated content reliably across browsers
Use the `allow` attribute on the iframe:
```html theme={null}
```
These examples omit `*` intentionally. In an iframe `allow` attribute, each
feature defaults to the iframe `src` origin, which is the safer default for
Corti Assistant embeds. Use `feature *` only if you explicitly want to grant
that permission to any origin the iframe might later navigate to.
If you omit `allow="microphone"` on the iframe, the embedded Assistant cannot
access the user's microphone even if the site itself has microphone
permission.
If you support virtual mode, include `display-capture` so the embedded
Assistant can request browser-managed capture of remote audio streams.
```html European Region expandable theme={null}
```
```html European Region (Medical Device) expandable theme={null}
```
```html Americas Region expandable theme={null}
```
## Message format
All messages sent to the embedded app follow this structure:
```typescript theme={null}
{
type: 'CORTI_EMBEDDED',
version: 'v1',
action: string,
requestId?: string,
payload?: object
}
```
### Message properties
* `type`: Always `'CORTI_EMBEDDED'`
* `version`: API version (currently `'v1'`)
* `action`: The method to invoke (see [API Reference](/assistant/api-reference) for all methods)
* `requestId`: Optional unique identifier for tracking responses
* `payload`: Optional data specific to the action
### Same API as Web Component
The PostMessage API provides access to the same methods documented in the [API Reference](/assistant/api-reference), including [auth()](/assistant/api/auth), [configureApp()](/assistant/api/configure-app), [setInteractionOptions()](/assistant/api/set-interaction-options), and [createInteraction()](/assistant/api/create-interaction), with identical parameters and behavior.
**Key difference:** Instead of direct method calls that return Promises, you communicate through messages:
1. **Send a message** with `action` field matching the method name
2. **Receive the response** asynchronously via a separate `CORTI_EMBEDDED_RESPONSE` message
Example for `auth`:
```javascript theme={null}
const assistantOrigin = new URL(iframe.src).origin;
// Web Component: Direct call with Promise
const user = await api.auth({
access_token,
refresh_token,
id_token,
token_type,
});
// PostMessage: Send message, listen for response
iframe.contentWindow.postMessage(
{
type: "CORTI_EMBEDDED",
version: "v1",
action: "auth", // Method name goes here
requestId: "auth-1",
payload: { access_token, refresh_token, id_token, token_type },
},
assistantOrigin,
);
// Later, in your message listener:
window.addEventListener("message", (event) => {
if (event.origin !== assistantOrigin) {
return;
}
if (
event.data.type === "CORTI_EMBEDDED_RESPONSE" &&
event.data.requestId === "auth-1"
) {
const user = event.data.payload; // Same result as Web Component
}
});
```
**Same functionality, different invocation pattern.**
## Response handling
Responses from the embedded app are sent via `postMessage` and can be identified by checking the message type:
```javascript theme={null}
window.addEventListener("message", (event) => {
// Handle responses
if (event.data?.type === "CORTI_EMBEDDED_RESPONSE") {
const { requestId, success, payload, error } = event.data;
// Handle response
}
// Handle events
if (event.data?.type === "CORTI_EMBEDDED_EVENT") {
const { event: eventType, payload } = event.data;
// Handle event
}
});
```
## Events
Corti Assistant dispatches events to notify your application of state changes and important updates. When using the PostMessage API, these events are wrapped in the `CORTI_EMBEDDED_EVENT` message type.
### Event format translation
Core events documented in the [Events Reference](/assistant/events) are wrapped for postMessage delivery:
**Core Event Structure:**
```json theme={null}
{
"event": "recording.started",
"confidential": false,
"payload": {
"mode": "dictation",
"language": "en",
"interactionId": "int_123"
}
}
```
**PostMessage wrapper:**
```json theme={null}
{
"type": "CORTI_EMBEDDED_EVENT",
"event": "recording.started",
"confidential": false,
"payload": {
"mode": "dictation",
"language": "en",
"interactionId": "int_123"
}
}
```
### Listening for events
Set up a message listener to receive events from the embedded Assistant:
```javascript Listening for Events expandable theme={null}
const ALLOWED_ORIGINS = [
"https://assistant.eu.corti.app",
"https://assistantmd.eu.corti.app",
"https://assistant.us.corti.app",
];
window.addEventListener("message", (event) => {
// Validate origin for security
if (!ALLOWED_ORIGINS.includes(event.origin)) {
return;
}
// Check for Corti events
if (event.data?.type === "CORTI_EMBEDDED_EVENT") {
const { event: eventName, confidential, payload } = event.data;
// Handle different event types
switch (eventName) {
case "recording.started":
console.log("Recording started:", payload);
break;
case "recording.paused":
console.log("Recording paused:", payload);
break;
case "document.generated":
console.log("Document generated:", payload);
handleDocumentGenerated(payload);
break;
case "error.triggered":
console.error("Error occurred:", payload);
break;
default:
console.log("Unknown event:", eventName, payload);
}
}
});
function handleDocumentGenerated(payload) {
const { documentId, documentName, interactionId } = payload;
// Update your UI, sync to backend, etc.
}
```
### Available events
For a complete list of events and their payload structures, see the [Events Overview](/assistant/events).
Common events include:
* `recording.started` - Recording has started
* `recording.paused` - Recording has paused
* `document.generated` - Document has been generated
* `document.updated` - Document has been edited
* `document.synced` - Document synced to external system
* `error.triggered` - An error occurred
### Legacy events
The embedded Assistant also dispatches [legacy
events](/assistant/events/legacy-events) using camelCase names (e.g.,
`recordingStarted`, `documentGenerated`). These are deprecated and will be
removed in a future version.
## Error handling
Always handle errors when making requests:
```javascript Error Handling expandable theme={null}
try {
const result = await sendMessage("auth", {
accessToken: "your-access-token",
refreshToken: "your-refresh-token",
id_token: "your-id-token",
token_type: "Bearer",
});
console.log("Authentication successful:", result);
} catch (error) {
console.error("Authentication failed:", error.message);
// Handle authentication failure
}
```
## Security considerations
When using `postMessage`, always:
1. **Validate message origin**: Check `event.origin` to ensure messages come from trusted sources
2. **Use specific target origins**: Replace `'*'` with the specific origin when possible
3. **Sanitize data**: Never trust data from postMessage without validation
```javascript Security Best Practices expandable theme={null}
const ALLOWED_ORIGINS = [
"https://assistant.eu.corti.app",
"https://assistantmd.eu.corti.app",
"https://assistant.us.corti.app",
];
window.addEventListener("message", (event) => {
// Validate origin
if (!ALLOWED_ORIGINS.includes(event.origin)) {
console.warn("Message from untrusted origin:", event.origin);
return;
}
// Process message
if (event.data?.type === "CORTI_EMBEDDED_EVENT") {
// Handle event
}
});
// Send messages with specific origin
iframe.contentWindow.postMessage(
{
type: "CORTI_EMBEDDED",
version: "v1",
action: "auth",
payload: {
/* ... */
},
},
"https://assistant.eu.corti.app",
); // Specific origin instead of '*'
```
## Next steps
* Review the [OAuth Authentication Guide](/assistant/authentication) to set up user authentication
* See the [API Reference](/assistant/api-reference) for all available methods and their parameters
* Learn about [events](/assistant/events) that the embedded app can send
* Check out the [Window API](/assistant/window-api) for same-origin integrations
Please [contact us](mailto:help@corti.ai) for help or questions.
# Release Notes
Source: https://docs.corti.ai/assistant/release-notes
Updates and improvements to Corti Assistant and Embedded API integrations
Corti Assistant release notes moved to [Release Notes](/release-notes/corti-assistant).
# Embedded Reliability, Timeouts, and Recovery
Source: https://docs.corti.ai/assistant/reliability-timeouts
Recommended timeout and retry patterns for Embedded Assistant lifecycle events.
Use this guidance to keep the host application responsive when an embedded lifecycle event does not arrive. The most important event to guard is [`interaction.loaded`](/assistant/events/generated/interaction/loaded), which confirms that the requested interaction is ready to display after navigation.
Load the Assistant with `visibility="hidden"`, navigate to the interaction,
and show the component after you receive
[`embedded.navigated`](/assistant/events/generated/embedded-api/navigated) or
[`interaction.loaded`](/assistant/events/generated/interaction/loaded). This
prevents users from seeing an incomplete embedded state.
## Recommended timeout
Start a timeout whenever your integration initiates navigation that depends on `interaction.loaded`. Use a default timeout between 10 and 30 seconds. Corti recommends starting with 20 seconds, then tuning based on your own network and WebView conditions.
When the timeout expires:
1. Stop waiting for the stale event listener.
2. Show a user-visible recovery state.
3. Provide a retry action that reinitializes the embed, remounts the Web Component, or reloads the iframe.
4. Log the timeout in your own monitoring so your team can separate integration, network, and service issues.
Do not leave users on an indefinite spinner. A clear recovery state is safer than a silent loading screen.
## User-facing recovery copy
Use short copy that explains the state without exposing implementation details:
```text theme={null}
Assistant is taking longer than expected to load.
Check your connection, then try again.
```
Use an action label such as `Retry` or `Reload Assistant`. When the user retries, create a fresh embedded instance or reload the iframe before repeating authentication, configuration, interaction creation, and navigation.
## Minimal implementation
The following example starts a timer before navigation, clears it when `interaction.loaded` arrives, and exposes a retry path that remounts the Web Component.
```ts title="TypeScript" theme={null}
const INTERACTION_LOADED_TIMEOUT_MS = 20000;
type EmbedState = "loading" | "ready" | "recoverable-error";
let interactionLoadedTimer: ReturnType | undefined;
function setEmbedState(state: EmbedState) {
// Render your own loading, ready, or retry UI here.
console.log("Embedded Assistant state:", state);
}
function clearInteractionLoadedTimer() {
if (interactionLoadedTimer) clearTimeout(interactionLoadedTimer);
interactionLoadedTimer = undefined;
}
function waitForInteractionLoaded(
corti: HTMLElement,
startEmbed: (corti: HTMLElement) => Promise,
) {
clearInteractionLoadedTimer();
setEmbedState("loading");
const handleLoaded = () => {
clearInteractionLoadedTimer();
setEmbedState("ready");
};
corti.addEventListener("interaction.loaded", handleLoaded, { once: true });
interactionLoadedTimer = setTimeout(() => {
corti.removeEventListener("interaction.loaded", handleLoaded);
setEmbedState("recoverable-error");
}, INTERACTION_LOADED_TIMEOUT_MS);
return startEmbed(corti);
}
async function retryEmbed(
corti: HTMLElement,
startEmbed: (corti: HTMLElement) => Promise,
) {
clearInteractionLoadedTimer();
const freshCorti = corti.cloneNode(false) as HTMLElement;
corti.replaceWith(freshCorti);
await waitForInteractionLoaded(freshCorti, startEmbed);
}
```
## What to monitor
Track these signals in your host application:
* Timeout count and timeout rate for `interaction.loaded`
* Browser or WebView family and version
* Assistant region and `baseURL`
* Whether the retry succeeds
* The last Embedded API method called before the timeout
Do not log OAuth tokens, request bodies, transcripts, generated documents, or other sensitive clinical data.
## Related pages
Use the recommended integration method for new embedded deployments.
Review lifecycle events emitted by the Embedded Assistant.
# Web Component API
Source: https://docs.corti.ai/assistant/web-component-api
Integrate Corti Assistant using the modern Web Component approach
The Web Component API is the **recommended approach** for embedding Corti Assistant into your application. It provides a modern, framework-agnostic integration method using standard Web Components that work with any framework.
This approach works for **both native applications with WebView controls** (C#, .NET, Electron) and **web applications** built with any modern framework. We provide dedicated React wrappers for enhanced developer experience, while other frameworks (Vue, Angular, Svelte, etc.) can use the raw HTML element directly or with small framework-specific wrappers.
**Want to skip to code?** Full working examples are available in the [Corti
Examples
Repository](https://github.com/corticph/corti-examples/tree/main/embedded-assistant)
for both [vanilla
TypeScript](https://github.com/corticph/corti-examples/tree/main/embedded-assistant/vanilla-ts/basic-example)
and
[React](https://github.com/corticph/corti-examples/tree/main/embedded-assistant/react/basic-example).
***
## Installation
Install the Corti Embedded Web package:
```bash npm theme={null}
npm install @corti/embedded-web
```
```bash yarn theme={null}
yarn add @corti/embedded-web
```
```bash pnpm theme={null}
pnpm add @corti/embedded-web
```
## Authentication
Before using the Web Component API, authenticate your users using OAuth2. See the [Authentication Guide](/assistant/authentication) for complete setup instructions including Authorization Code Flow with PKCE (recommended), obtaining tokens, and handling token refresh.
All Embedded Assistant integrations require user-based OAuth2 authentication.
Client credentials and machine-to-machine flows are not supported.
***
## Usage
The Web Component works with any modern framework (React, Vue, Angular, Svelte, etc.) since it's based on standard Web Components. We provide dedicated React wrappers for enhanced developer experience, while other frameworks can use the raw HTML element with small framework-specific wrappers if needed.
### Vanilla TypeScript/JavaScript
This approach works universally - use it directly or wrap it in framework-specific components for Vue, Angular, or other frameworks.
**Full working example**: See the complete [vanilla TypeScript
example](https://github.com/corticph/corti-examples/tree/main/embedded-assistant/vanilla-ts/basic-example)
with authentication, interaction creation, and event handling.
Import the web component and use it as a custom HTML element:
```typescript theme={null}
import "@corti/embedded-web";
```
Add the custom element to your HTML:
```html theme={null}
```
Access the API via the element reference (which should be automatically typed if retrieved via `querySelector`):
```typescript theme={null}
async function initializeAssistant() {
const corti = document.querySelector("corti-embedded");
// Wait for the custom element to be defined
await customElements.whenDefined("corti-embedded");
// Wait for the embedded assistant to be ready
await new Promise((resolve) => {
corti.addEventListener("embedded.ready", () => resolve(), { once: true });
});
// Authenticate
await corti.auth({
access_token: "your-access-token",
refresh_token: "your-refresh-token",
id_token: "your-id-token",
token_type: "Bearer",
});
// Create interaction
const interaction = await corti.createInteraction({
assignedUserId: null,
encounter: {
identifier: `encounter-${Date.now()}`,
status: "planned",
type: "first_consultation",
period: { startedAt: new Date().toISOString() },
},
});
// Navigate to session (object form supported in @corti/embedded-web@0.3.0+)
await corti.navigate({ path: `/session/${interaction.id}` });
// Handle errors
corti.addEventListener("error", (event: CustomEvent) => {
console.error("Error:", event.detail?.message);
});
// Optional: Listen to all events for debugging and monitoring
corti.addEventListener(
"event",
(event: CustomEvent<{ name: string; payload: unknown }>) => {
console.log("Event:", event.detail.name, event.detail.payload);
},
);
}
// Initialize when the DOM is ready
initializeAssistant().catch(console.error);
```
### React (built-in support)
For React, we provide dedicated components and hooks for the best developer experience. For other frameworks like Vue or Angular, use the vanilla approach above and create small framework-specific wrappers as needed.
**Full working example**: See the complete [React
example](https://github.com/corticph/corti-examples/tree/main/embedded-assistant/react/basic-example)
with authentication, interaction creation, and event handling.
In React, use the `baseURL` prop. In plain HTML, use the `baseurl` attribute.
Both map to the same underlying component property.
Import the React component and hook:
```typescript theme={null}
import {
CortiEmbeddedReact,
type CortiEmbeddedReactRef,
useCortiEmbeddedApi,
} from "@corti/embedded-web/react";
```
Use the component in your React application:
```tsx theme={null}
import { useRef } from "react";
import {
CortiEmbeddedReact,
type CortiEmbeddedReactRef,
useCortiEmbeddedApi,
} from "@corti/embedded-web/react";
function EmbeddedAssistant({ baseURL, authData }) {
const cortiRef = useRef(null);
const api = useCortiEmbeddedApi(cortiRef);
const hasInitialized = useRef(false);
const handleReady = async () => {
// Guard against React StrictMode double-invocation
if (hasInitialized.current) return;
hasInitialized.current = true;
try {
// Authenticate
await api.auth(authData);
// Create interaction
const interaction = await api.createInteraction({
assignedUserId: null,
encounter: {
identifier: `encounter-${Date.now()}`,
status: "planned",
type: "first_consultation",
period: { startedAt: new Date().toISOString() },
},
});
// Navigate to session (object form supported in @corti/embedded-web@0.3.0+)
await api.navigate({ path: `/session/${interaction.id}` });
} catch (error) {
console.error("Error:", error);
}
};
const handleError = (event: CustomEvent) => {
console.error("Error:", event.detail?.message);
};
const handleEvent = (event: CustomEvent) => {
console.log("Event:", event.detail.name, event.detail.payload);
};
return (
);
}
```
***
## Component properties
### `baseURL` (required)
The base URL of the Corti Assistant instance. Choose the appropriate region:
* **EU**: `https://assistant.eu.corti.app`
* **EU MD**: `https://assistantmd.eu.corti.app` (medical device compliant)
* **US**: `https://assistant.us.corti.app`
### `visibility` (optional)
Controls the initial visibility of the embedded assistant:
* `"visible"` (default): Assistant is visible on mount
* `"hidden"`: Assistant is hidden on mount
Load the assistant with `visibility="hidden"` and listen for the [`embedded.navigated`](/assistant/events/generated/embedded-api/navigated) event before showing it. This event confirms the interaction is ready to display, preventing users from seeing a loading or incomplete state.
For production integrations, combine hidden loading with a timeout and retry flow for [`interaction.loaded`](/assistant/events/generated/interaction/loaded). See [Embedded Reliability, Timeouts, and Recovery](/assistant/reliability-timeouts).
***
## API methods
All API methods are available through the element reference (vanilla) or via the `useCortiEmbeddedApi` hook (React).
**Shared API surface**
The Web Component provides the same operations documented in the [API Reference](/assistant/api-reference). Web Component, Window API, and PostMessage expose the same capabilities, but their invocation shapes differ.
This page focuses on the package API exposed by the Web Component and React wrapper. For Window API and PostMessage-specific call shapes, see the [Window API](/assistant/window-api) and [PostMessage API](/assistant/postmessage-api) pages. Use the [API Reference](/assistant/api-reference) for shared method semantics and payload details.
### Core methods
* **`auth(credentials)`** - Authenticate the user session
* **`configureApp(config)`** - Configure app-level UI, appearance, locale, and network settings
* **`createInteraction(interaction)`** - Create a new clinical interaction
* **`navigate(pathOrPayload)`** - Navigate to a specific route
* **`startRecording()`** - Start audio recording
* **`stopRecording()`** - Stop audio recording
* **`getStatus()`** - Get current session status
* **`setInteractionOptions(config)`** - Configure interaction-level defaults and options
* **`getTemplates()`** - Retrieve available document templates
* **`addFacts(factsArray)`** - Add clinical facts to the current interaction
* **`setCredentials(credentials)`** - Update authentication credentials
* **`showDeviceLinkQR(tokenResponse)`** - Display the QR pairing flow for the Corti mobile companion app
`navigate()` compatibility in Web Component / React:
* Versions earlier than `@corti/embedded-web@0.3.0`: pass a path string,
for example `navigate("/session/")`.
* `@corti/embedded-web@0.3.0` and later: both
`navigate("/session/")` and `navigate({ path: "/session/" })`
are accepted.
For consistency with Window API and PostMessage, object form is recommended
when you use `@corti/embedded-web@0.3.0` or later.
### Deprecated methods
* **`configure(config)` (Deprecated)** - Configure interface features and appearance using the legacy configuration structure
* **`configureSession(config)` (Deprecated)** - Configure session-specific settings using the legacy configuration structure
See the [API Reference](/assistant/api-reference) for detailed documentation of each method.
***
## Events
The Web Component emits events for all Assistant activities. Listen for events using standard event listeners.
### Ready event
Emitted when the Assistant is fully loaded and ready to receive API calls:
```typescript theme={null}
corti.addEventListener("embedded.ready", (event) => {
console.log("Assistant is ready", event.detail);
});
```
### Other named events
You can listen for all other named events directly by listening for them as above.
For example, the `recording.started` event (as detailed [here](/assistant/events/generated/recording/started)):
```typescript theme={null}
corti.addEventListener("recording.started", (event) => {
console.log("Recording has started", event.detail);
});
```
For a complete list of events and their payloads, see the [Events Reference](/assistant/events/index).
### Error event
Emitted when an API call fails or an error occurs:
```typescript theme={null}
corti.addEventListener("error", (event: CustomEvent) => {
console.error("Error:", event.detail);
// event.detail contains: { message: string, code?: string, details?: unknown }
});
```
### Generic event
You can also listen for all events, regardless of type, by simply listening for `"event"` as per below:
```typescript theme={null}
corti.addEventListener(
"event",
(event: CustomEvent<{ name: string; payload: unknown }>) => {
console.log("Event name:", event.detail.name);
console.log("Event payload:", event.detail.payload);
},
);
```
This is useful for debugging and logging purposes, but be aware that there are *many* events with lots of data, so be mindful about putting this into production.
***
## End-user authentication
Before using most API methods, you must authenticate with valid OAuth2 tokens. The Web Component expects tokens from an OAuth2 authentication flow.
See the [Authentication Guide](/assistant/authentication) for detailed information on obtaining tokens.
```typescript theme={null}
await api.auth({
access_token: "your-access-token",
refresh_token: "your-refresh-token",
id_token: "your-id-token",
token_type: "Bearer",
});
```
***
## Configuration
Configure the Assistant interface after authentication:
Use `configureApp()` for app-level settings and `setInteractionOptions()` for
interaction-level defaults.
```typescript theme={null}
await api.configureApp({
ui: {
interactionTitle: true,
aiChat: true,
documentFeedback: false,
navigation: true,
},
appearance: {
primaryColor: "#00a6ff",
},
locale: {
interfaceLanguage: "en",
dictationLanguage: "en",
},
});
await api.setInteractionOptions({
mode: {
options: ["in-person", "virtual"],
},
templates: {
sources: {
personal: {
enabled: true,
},
},
},
documents: {
actions: {
sync: false,
},
},
});
```
See [Configuration Scenarios](/assistant/configuration-scenarios) for practical examples and [Supported Values](/assistant/configuration-values) for the current configuration options.
***
## Full working examples
Complete, runnable examples are available in the [Corti Examples Repository](https://github.com/corticph/corti-examples):
Demonstrates `` web component usage with vanilla TypeScript, including authentication, interaction creation, navigation, and event handling.
Demonstrates `CortiEmbeddedReact` component and `useCortiEmbeddedApi` hook usage with React, including authentication, interaction creation, navigation, and event handling.
Add a timeout and retry path for critical lifecycle events such as
`interaction.loaded`. See [Embedded Reliability, Timeouts, and
Recovery](/assistant/reliability-timeouts) for a copyable pattern.
***
## TypeScript support
The package includes full TypeScript definitions:
```typescript theme={null}
import type {
CortiEmbeddedAPI,
CreateInteractionPayload,
ConfigureAppPayload,
KeycloakTokenResponse,
} from "@corti/embedded-web";
import type { CortiEmbeddedReactRef } from "@corti/embedded-web/react";
```
***
## Next steps
Compare all three integration methods and choose the right one for your architecture
Set up OAuth2 authentication for your users
Complete API documentation for all methods
Customize the Assistant interface and behavior
# Overview of the Corti Embedded Assistant
Source: https://docs.corti.ai/assistant/welcome
Embed a first-class ambient scribing experience into your healthcare application in minutes.
## Build ambient scribing directly into your application
Corti Assistant is an embeddable ambient scribing application for software teams building EHRs and healthcare platforms.
It gives you a production-ready, healthcare-grade assistant that listens to clinical conversations, structures medical information, and generates EHR-ready documentation. You embed it directly into your product so it feels near-native to clinicians, not like a separate tool they have to manage.
This documentation is for developers who want to ship ambient scribing quickly, without building speech recognition, clinical NLP, document generation, and compliance infrastructure from scratch.
Corti Assistant can run as a standalone app, but it is designed first and foremost to be embedded into existing healthcare software.
***
## Why embed an assistant instead of building one?
Embedding Corti Assistant lets you focus on your core product while offering a full ambient scribing experience that fits naturally into your workflow.
Instead of stitching together speech-to-text, clinical extraction, templates, and exports yourself, you integrate a single embedded surface that already works end to end.
When embedded, the assistant:
* Lives inside your EHR or healthcare application
* Uses your existing authentication and session model
* Appears exactly where clinicians already work
* Shares context like patient, encounter, and workflow state
The result is a seamless experience where documentation happens as part of the product, not alongside it.
From a development perspective, this means weeks of integration instead of months or years of AI development, model tuning, and compliance work.
You also inherit enterprise-grade security and certifications out of the box, including HIPAA, GDPR, SOC 2 Type 2, ISO 27001, WCAG 2.2 AA, and more.
***
## From conversation to structured clinical documentation
At its core, Corti Embedded Assistant turns spoken consultations into structured, editable documentation.
The workflow is simple and predictable:
**Conversation → Transcript → Clinical facts → EHR-ready documents**
During a consultation, the assistant listens continuously. Speech is transcribed using healthcare-optimized recognition, then analyzed to extract clinically relevant facts such as symptoms, history, vitals, and plans.
These facts are presented as structured, editable items that clinicians can review, adjust, or reorganize before generating one or more documents.
From a single conversation, you can generate multiple outputs such as SOAP notes, H\&P, emergency notes, referrals, discharge summaries, or patient summaries, all based on the same curated fact set.
***
## Embedded by design
Corti Assistant is delivered as an embeddable application with APIs that give you control over how it behaves inside your product.
You decide:
* When sessions start and stop
* How recording is controlled
* How documents are generated and exported
* How authentication and identity are handled
Three integration methods are available:
* [**Web Component API**](/assistant/web-component-api)\*\* (Recommended)\*\* - Modern, framework-agnostic integration using standard Web Components with full TypeScript support. Works seamlessly in vanilla JavaScript, React, Vue, Angular, and other frameworks for both iframe and same-origin scenarios. [Full examples available](https://github.com/corticph/corti-examples/tree/main/embedded-assistant).
* [**PostMessage API**](/assistant/postmessage-api) - Lower-level iframe communication API. Fully supported but not recommended due to added complexity compared to the Web Component API.
* [**Window API**](/assistant/window-api) - Same-origin direct API access. Useful for specific scenarios like direct URL embedding in webviews/iframes.
The Web Component approach is recommended for most integrations. PostMessage API adds unnecessary complexity, while Window API serves specific use cases.
***
## What you get out of the box
The embedded assistant is not just transcription. It is a complete documentation surface that includes:
* Real-time AI chat for editing documents, asking clinical questions, and referencing guidelines
* Familiar clinical templates like SOAP, H\&P, and custom formats
* Multi-document generation from a single encounter
* Drag-and-drop, editable clinical facts
* One-click export to your EHR or downstream systems
The UI can be customized to match your application, including branding, feature visibility, language support, and terminology overrides.
***
## Built for healthcare, operated at scale
Corti Assistant runs on the same healthcare-specific platform used by large health systems.
The underlying Corti API handles:
* Medical speech recognition
* Clinical fact extraction
* Language models tuned for healthcare
* Document generation
* Security, privacy, and compliance
For organisations with advanced needs, enterprise capabilities are available through the same embedded interface, including EHR connectivity, medical coding, standardized documentation, custom guidelines, and priority support.
# Window API
Source: https://docs.corti.ai/assistant/window-api
Use the Window API for direct integration with Corti Assistant
The Window API provides a direct, Promise-based TypeScript API exposed on `window.CortiEmbedded` for integration with Corti Assistant. This method is suitable for same-origin integrations requiring direct JavaScript access.
**Web Component API is recommended for most cases.** The [Web Component API](/assistant/web-component-api) is recommended when you embed the Assistant via a local host page. The Window API remains useful for specific scenarios, such as embedding via direct URL in webviews/iframes. It is fully supported and not deprecated.
**Web Component has full working examples.** Complete, runnable examples are available for the [Web Component integration method](https://github.com/corticph/corti-examples/tree/main/embedded-assistant), which is the recommended approach for most integrations. Window API examples for same-origin scenarios will be added to the repository in the future.
## Overview
The Window API offers a Promise-based, TypeScript-friendly interface for integrating Corti Assistant into your application. It provides direct access to `window.CortiEmbedded.v1`, making it feel like a traditional JavaScript SDK.
## Requirements
### Implementation requirements
To use the Window API, you'll need to implement a WebView or similar browser component within your native application. The embedded Corti Assistant runs as a web application and requires a modern browser environment to function properly.
### Minimum requirements
* **Modern WebView**: Use a modern WebView implementation that supports:
* **WebView2** (Windows) - Recommended for Windows applications
* **WKWebView** (iOS/macOS) - Recommended for Apple platforms
* **WebView** (Android) - Use the latest Chromium-based WebView
* **Electron WebView** - For Electron-based applications
* **Browser compatibility**: The WebView must support:
* ES6+ JavaScript features
* Modern Web APIs (WebRTC, MediaDevices API)
* PostMessage API
* Local Storage and Session Storage
* **Microphone permissions**: Your application must request and handle microphone permissions:
* Request microphone access before initializing the embedded Assistant
* Handle permission denial gracefully
* Provide clear messaging to users about why microphone access is needed
* Ensure permissions are granted at the OS level (not just browser level)
### Platform-specific considerations
**Windows (WebView2)**
* Ensure WebView2 Runtime is installed or bundled with your application
* Request microphone permissions in your application manifest
* Handle permission prompts appropriately
**iOS/macOS (WKWebView)**
* Add `NSMicrophoneUsageDescription` to your Info.plist
* Request microphone permissions using `AVAudioSession` or similar APIs
* Ensure permissions are granted before loading the embedded Assistant
**Android (WebView)**
* Request `RECORD_AUDIO` permission in your AndroidManifest.xml
* Request runtime permissions using `ActivityCompat.requestPermissions()`
* Handle permission callbacks appropriately
## Recommendations
* **Use TypeScript** for better type safety and developer experience
* **Implement proper error handling** for all API calls
* **Handle token refresh** to maintain user sessions
* **Request microphone permissions early** in your application flow
* **Test on target platforms** to ensure WebView compatibility
## Quick Start
### Step 1: Set up authentication
Before using the Window API, authenticate your users using OAuth2. See the [Authentication Guide](/assistant/authentication) for complete setup instructions including Authorization Code Flow with PKCE (recommended), obtaining tokens, and handling token refresh.
All Embedded Assistant integrations require user-based OAuth2 authentication. Client credentials and machine-to-machine flows are not supported.
* Handle token refresh to maintain sessions
* Never expose client secrets in client-side code
### Step 2: Wait for the Embedded App to be ready
The embedded Corti Assistant will send an `embedded.ready` event when it's loaded and ready to receive API calls:
```javascript Basic Setup expandable theme={null}
window.addEventListener("message", async (event) => {
if (
event.data?.type === "CORTI_EMBEDDED_EVENT" &&
event.data.event === "embedded.ready"
) {
// The API is now available
const api = window.CortiEmbedded.v1;
console.log("Corti Assistant is ready");
}
});
```
### Step 3: Authenticate the user
Once the API is ready, authenticate the user with their OAuth2 tokens:
```javascript Authentication expandable theme={null}
const api = window.CortiEmbedded.v1;
const user = await api.auth({
access_token: "your-access-token", // From OAuth2 flow
refresh_token: "your-refresh-token", // From OAuth2 flow
id_token: "your-id-token", // From OAuth2 flow
token_type: "Bearer",
});
console.log("Authenticated user:", user);
```
### Step 4: Configure and use
After authentication, you can configure the interface and start using the Assistant:
```javascript Configure and Use expandable theme={null}
// Configure app-level settings
await api.configureApp({
ui: {
interactionTitle: false,
aiChat: false,
navigation: true,
},
appearance: {
primaryColor: "#00a6ff",
},
locale: {
interfaceLanguage: "en",
dictationLanguage: "en",
},
});
// Configure interaction defaults
await api.setInteractionOptions({
mode: {
options: ["in-person", "virtual"],
},
});
// Create an interaction
const interaction = await api.createInteraction({
assignedUserId: null,
encounter: {
identifier: `encounter-${Date.now()}`,
status: "planned",
type: "first_consultation",
period: {
startedAt: new Date().toISOString(),
},
title: "Initial Consultation",
},
});
// Navigate to the interaction
await api.navigate({
path: `/session/${interaction.id}`,
});
```
## API structure
The API is available at `window.CortiEmbedded.v1` and provides the following methods:
```typescript theme={null}
window.CortiEmbedded.v1 = {
auth: (payload) => Promise,
configureApp: (payload) => Promise,
createInteraction: (payload) => Promise,
addFacts: (payload) => Promise,
setInteractionOptions: (payload) => Promise,
navigate: (payload) => Promise,
setCredentials: (payload) => Promise,
startRecording: () => Promise,
stopRecording: () => Promise,
getStatus: () => Promise,
};
```
### Same API as Web Component
The Window API provides the exact same methods as described in the [API Reference](/assistant/api-reference). The only difference is the invocation style - with Window API, you call methods directly via `window.CortiEmbedded.v1.methodName()` instead of through a Web Component.
Example:
* Web Component: `await api.auth({ ... })`
* Window API: `await window.CortiEmbedded.v1.auth({ ... })`
Same method, same parameters, same return values - just different access patterns.
## Events
Corti Assistant dispatches events to notify your application of user activity, state change, data updates, and many more interactions. When using the Window API, events are delivered through the same `postMessage` mechanism.
### Event format translation
Core events documented in the [Events Reference](/assistant/events) are wrapped in the `CORTI_EMBEDDED_EVENT` message type:
**Core event structure:**
```json theme={null}
{
"event": "event-name",
"confidential": true,
"payload": {
"various": "properties"
}
}
```
**Window API delivery:**
```json theme={null}
{
"type": "CORTI_EMBEDDED_EVENT",
"event": "recording.started",
"confidential": false,
"payload": {
"mode": "virtual",
"language": "en",
"interactionId": "int_123",
"interactionState": "ongoing"
}
}
```
### Listening for events
Even when using the Window API for method calls, events are delivered via `postMessage`. Set up a listener:
```javascript Listening for Events expandable theme={null}
window.addEventListener("message", (event) => {
// Check for Corti events
if (event.data?.type === "CORTI_EMBEDDED_EVENT") {
const { event: eventName, confidential, payload } = event.data;
// Handle different event types
switch (eventName) {
case "recording.started":
console.log("Recording started:", payload);
updateRecordingState(true);
break;
case "recording.paused":
console.log("Recording paused:", payload);
updateRecordingState(false);
break;
case "document.generated":
console.log("Document generated:", payload);
handleNewDocument(payload);
break;
case "error.triggered":
console.error("Error occurred:", payload);
showErrorNotification(payload);
break;
default:
console.log("Unknown event:", eventName);
}
}
});
function updateRecordingState(isRecording) {
// Update your UI to reflect recording state
}
function handleNewDocument(payload) {
const { documentId, documentName, templateId } = payload;
// Process the new document
}
```
### Combining API calls and events
Use the Window API for actions and events for state updates:
```javascript Combined Usage expandable theme={null}
const api = window.CortiEmbedded.v1;
// Set up event listener
window.addEventListener("message", (event) => {
if (event.data?.type === "CORTI_EMBEDDED_EVENT") {
const { event: eventName, payload } = event.data;
if (eventName === "recording.started") {
console.log("Recording started successfully");
}
}
});
// Trigger action via Window API
try {
await api.startRecording();
// Event will be received via message listener above
} catch (error) {
console.error("Failed to start recording:", error);
}
```
### Available events
For a complete list of events and their payload structures, see the [Events Overview](/assistant/events).
Common events include:
* `recording.started` - Recording has started
* `recording.paused` - Recording has paused
* `document.generated` - Document has been generated
* `document.updated` - Document has been edited
* `interaction.loaded` - Interaction has been loaded
* `error.triggered` - An error occurred
### Legacy events
The embedded Assistant also dispatches [legacy events](/assistant/events/legacy-events) using camelCase names (e.g., `recordingStarted`, `documentGenerated`). These are deprecated and will be removed in a future version.
## Error handling
All API methods return Promises and can throw errors. Always wrap calls in try-catch blocks:
```javascript Error Handling expandable theme={null}
try {
const api = window.CortiEmbedded.v1;
const user = await api.auth({
access_token: "your-access-token",
refresh_token: "your-refresh-token",
id_token: "your-id-token", // From OAuth2 flow
token_type: "Bearer",
});
console.log("Authentication successful:", user);
} catch (error) {
console.error("Authentication failed:", error.message);
// Handle authentication failure
}
```
## TypeScript support
If you're using TypeScript, you can extend the Window interface to get type safety:
```typescript TypeScript Definitions expandable theme={null}
interface CortiEmbeddedAPI {
auth: (payload: AuthPayload) => Promise;
configureApp: (payload: ConfigureAppPayload) => Promise;
createInteraction: (
payload: CreateInteractionPayload,
) => Promise;
addFacts: (payload: AddFactsPayload) => Promise;
setInteractionOptions: (payload: SetInteractionOptionsPayload) => Promise;
navigate: (payload: NavigatePayload) => Promise;
setCredentials: (payload: SetCredentialsPayload) => Promise;
startRecording: () => Promise;
stopRecording: () => Promise;
getStatus: () => Promise;
}
interface Window {
CortiEmbedded: {
v1: CortiEmbeddedAPI;
};
}
```
## Helper function
You can create a helper function to ensure the API is ready:
```javascript Helper Function expandable theme={null}
function waitForCortiAPI() {
return new Promise((resolve) => {
if (window.CortiEmbedded?.v1) {
resolve(window.CortiEmbedded.v1);
return;
}
const listener = (event) => {
if (
event.data?.type === "CORTI_EMBEDDED_EVENT" &&
event.data.event === "embedded.ready"
) {
window.removeEventListener("message", listener);
resolve(window.CortiEmbedded.v1);
}
};
window.addEventListener("message", listener);
});
}
// Usage
async function useAPI() {
const api = await waitForCortiAPI();
const user = await api.auth({
access_token: "your-access-token",
refresh_token: "your-refresh-token",
id_token: "your-id-token",
token_type: "Bearer",
});
}
```
## Next steps
* Review the [OAuth Authentication Guide](/assistant/authentication) to set up user authentication
* See the [API Reference](/assistant/api-reference) for all available methods and their parameters
* Learn about [events](/assistant/events) that the embedded app can send
* Check out the [PostMessage API](/assistant/postmessage-api) for cross-origin integrations
Please [contact us](mailto:help@corti.ai) for help or questions.
# Create an API client
Source: https://docs.corti.ai/authentication/creating_clients
Quick steps to creating your first client on the Corti Console.
Start by creating a project. This gives you a workspace and a \$50 trial credit.
Inside your project, create a new client. Choose a clear name such as `test`, `staging`, or `prod`. The client automatically inherits the region you selected when you created the project.
After creating the client, you will receive credentials that allow your backend to request OAuth access tokens. Store these securely and never expose them to browsers or mobile apps.
* `client_id`
* `client_secret`
* `tenant-name` (usually `base`)
* `environment` (inherited from the project - `eu` or `us`)
Use the client credentials to fetch an access token, then call the Corti API with a Bearer token and the correct `Tenant-Name` header.
# Environments & Tenants
Source: https://docs.corti.ai/authentication/environments_tenants
Learn how Corti environments and tenants work, and how they affect authentication and data residency.
## Environments
Corti operates separate regional environments. Your environment determines where data is stored and which API endpoints you use. It also defines which identity provider you authenticate against.
Available environments:
| Environment | Region | Base API URL | Auth Base URL | Access |
| ----------- | ----------------- | -------------------------- | --------------------------- | ------------ |
| `eu` | Azure West Europe | `https://api.eu.corti.app` | `https://auth.eu.corti.app` | Public |
| `us` | Azure US East | `https://api.us.corti.app` | `https://auth.us.corti.app` | Public |
| `fr` | Scaleway Paris | `https://api.fr.corti.app` | `https://auth.fr.corti.app` | Upon request |
| `dk` | Denmark | To be announced | To be announced | Upon request |
Your API client must authenticate against the correct environment. Tokens from one region cannot be used in another. When you create a project in the console, you choose its region. All API clients created under that project automatically inherit the project's region.
**Corti Models** is available only in the EU region and is served from a dedicated base URL, `https://ai.eu.corti.app`, rather than the regional API URLs above. Credentials from a US-hosted project can't access it. See the [Corti Models quickstart](/models/quickstart).
## Tenants
A tenant represents a shared identity realm for Corti API customers. All API customers operate inside the shared tenant named `base`. This keeps authentication consistent while maintaining strict segregation of customer data at the application layer.
You include the tenant name in the authentication URL or in headers when interacting with certain APIs. For most customers, this will always be: `Tenant-Name: "base"`
Bespoke private tenants are available only for specific enterprise or regulatory scenarios that require full isolation at the identity realm level. These cases are rare and need a dedicated review. If you believe your organisation needs its own tenant, speak with your Corti representative.
# Overview
Source: https://docs.corti.ai/authentication/overview
Learn how to authenticate with client credentials for use with the Corti API.
This guide covers authentication for use with the Corti API. If you are
looking for authenticating users with Corti Assistant Embedded, then see more
[here](/assistant/authentication).
## Authenticating with Corti Auth
Corti uses OAuth 2.0 client credentials for server-to-server authentication. This flow requires that you fetch a short-lived access token based on a `client_id` and `client_secret` from the Corti Auth Server before calling the API.
```mermaid theme={null}
%%{init: {
"sequence": {
"mirrorActors": false,
"boxTextMargin": 20
}
}}%%
sequenceDiagram
participant Service as Your Backend
participant OAuth as Corti Auth
participant Corti as Corti API
Service->>OAuth: POST /token grant_type=client_credentials client_id, client_secret
OAuth-->>Service: 200 OK access_token (short-lived)
Service->>Corti: API Request Authorization: Bearer {{access_token}} Tenant-Name: base
Corti-->>Service: API Response
```
Note that both the client secret and the access tokens generated have full access to the API. They should never be shared or exposed to the client. For best practices on keeping your credentials secure, read [our guide](/authentication/security_best_practices).
Need to open a real-time speech-to-text WebSocket directly from a browser? Request a **limited-scope token** with `scope="openid transcribe"` and/or `scope="openid streams"`. The resulting token can only be used against the corresponding streaming endpoints — see [Limited-scope credentials for streaming APIs](/authentication/security_best_practices#4-if-you-must-use-tokens-in-special-cases-use-limited-scope-credentials).
### Fetching an Access Token with OAuth 2.0 client-credentials
```bash title="cURL (base realm)" theme={null}
# Replace these with your values
CLIENT_ID=""
CLIENT_SECRET=""
ENVIRONMENT=""
curl \
"https://auth.${ENVIRONMENT}.corti.app/realms/base/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "client_id=${CLIENT_ID}" -d "client_secret=${CLIENT_SECRET}" \
-d 'grant_type=client_credentials' -d 'scope=openid'
```
```bash title="cURL (custom tenant)" theme={null}
# Replace these with your values
CLIENT_ID=""
CLIENT_SECRET=""
ENVIRONMENT=""
TENANT=""
curl \
"https://auth.${ENVIRONMENT}.corti.app/realms/${TENANT}/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "client_id=${CLIENT_ID}" -d "client_secret=${CLIENT_SECRET}" \
-d 'grant_type=client_credentials' -d 'scope=openid'
```
```json title="Response" theme={null}
{
"access_token": "eyJhbGciOi...",
"expires_in": 300,
"token_type": "Bearer",
"scope": "profile openid email"
}
```
For more detailed instructions on how to get an access token in various languages, see our guide here: [Authentication Quickstart](/authentication/quickstart)
### Using the access token in API requests
Once you have an access token, include it in the Authorization header. You must also provide the Tenant-Name header to specify which tenant context the request operates in.
```curl title="Example request" theme={null}
curl -X GET "https://api.$environment.corti.app/v2/interactions" \
-H "Authorization: Bearer {{access_token}}" \
-H "Tenant-Name: base"
```
Must contain the bearer token returned from the OAuth server.
The tenant identifier where the request is executed. Default tenants
typically use `base`, but enterprise setups may use a custom tenant name.
## Why we use client credentials instead of an API key
API keys are simple, but they are static. If one leaks, whoever has it can call your APIs until you rotate it. Client credentials solve this by issuing short-lived tokens that expire automatically, which limits the blast radius of a leak and improves auditability.
Key differences:
* API keys are long-lived, client credentials produce short-lived tokens (5 minutes).
* API keys cannot express scopes or granular permissions, OAuth tokens can.
* OAuth flows integrate with identity providers and tenancy models, which makes them safer and easier to govern in enterprise environments.
# Quickstart - Authenticating to the Corti API
Source: https://docs.corti.ai/authentication/quickstart
Learn how to authenticate with client credentials.
This guide shows how to authenticate with the Corti API using OAuth 2.0 client credentials.
## Authenticate with an SDK (recommended)
The fastest way to authenticate is with an official SDK. The SDK handles the OAuth2 token exchange and refresh automatically. See [SDK overview](/sdk/overview) for setup and usage details.
Install SDK to your project:
```bash title="JavaScript" theme={null}
npm install @corti/sdk
# or
yarn add @corti/sdk
# or
pnpm add @corti/sdk
```
```bash title="C# .NET" theme={null}
dotnet add package Corti.Sdk
# Alternatively, in your .csproj:
#
```
Create a client to call API:
```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const ENVIRONMENT = "";
const TENANT = "";
const client = new CortiClient({
environment: ENVIRONMENT,
tenantName: TENANT,
auth: {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
},
});
```
```csharp title="C# .NET" theme={null}
using Corti;
// Replace these with your values
const string CLIENT_ID = "";
const string CLIENT_SECRET = "";
const string ENVIRONMENT = "";
const string TENANT = "";
var client = new CortiClient(
tenantName: TENANT,
environment: ENVIRONMENT,
auth: CortiClientAuth.ClientCredentials(
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET)
);
```
If you only need a token:
```ts title="JavaScript" theme={null}
import { CortiAuth } from "@corti/sdk";
// Replace these with your values
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const ENVIRONMENT = "";
const TENANT = "";
const auth = new CortiAuth({
environment: ENVIRONMENT,
tenantName: TENANT,
});
const token = await auth.getToken({
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
});
console.log("accessToken:", token.accessToken);
```
```csharp title="C# .NET" theme={null}
using Corti;
// Replace these with your values
const string CLIENT_ID = "";
const string CLIENT_SECRET = "";
const string ENVIRONMENT = "";
const string TENANT = "";
var auth = CustomAuthClient.Create(
new CortiAuthClientOptions
{
TenantName = TENANT,
Environment = ENVIRONMENT,
});
var tokenResponse = await auth.GetTokenAsync(
new OAuthTokenRequest
{
ClientId = CLIENT_ID,
ClientSecret = CLIENT_SECRET,
});
Console.WriteLine(tokenResponse.AccessToken);
```
For full SDK authentication documentation including bearer tokens, PKCE, authorization code, and ROPC flows, see the [JavaScript Authentication Guide](/sdk/js/authentication) or the [.NET Authentication Guide](/sdk/dotnet/authentication).
## Authenticate using manual code examples
If you prefer to handle OAuth manually without an SDK, use the examples below:
```js title="JavaScript" expandable theme={null}
// Replace these with your values
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const ENVIRONMENT = "";
const TENANT = "";
async function getAccessToken() {
const tokenUrl = `https://auth.${ENVIRONMENT}.corti.app/realms/${TENANT}/protocol/openid-connect/token`;
const params = new URLSearchParams();
params.append("client_id", CLIENT_ID);
params.append("client_secret", CLIENT_SECRET);
params.append("grant_type", "client_credentials");
params.append("scope", "openid");
const res = await fetch(tokenUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params
});
if (!res.ok) {
throw new Error(`Failed to get token, status ${res.status}`);
}
const data = await res.json();
return data.access_token;
}
// Example usage
getAccessToken().then(token => {
console.log("Access token:", token);
}).catch(err => {
console.error("Error:", err);
});
```
```csharp title="C# .NET" expandable theme={null}
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
class Program
{
private const string CLIENT_ID = "";
private const string CLIENT_SECRET = "";
private const string ENVIRONMENT = "";
private const string TENANT = "";
private static async Task GetAccessTokenAsync()
{
var tokenUrl = $"https://auth.{ENVIRONMENT}.corti.app/realms/{TENANT}/protocol/openid-connect/token";
using var http = new HttpClient();
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair("client_id", CLIENT_ID),
new KeyValuePair("client_secret", CLIENT_SECRET),
new KeyValuePair("grant_type", "client_credentials"),
new KeyValuePair("scope", "openid")
});
var response = await http.PostAsync(tokenUrl, content);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadAsStringAsync();
using var json = JsonDocument.Parse(payload);
return json.RootElement.GetProperty("access_token").GetString()!;
}
static async Task Main()
{
var token = await GetAccessTokenAsync();
Console.WriteLine($"Access token: {token}");
}
}
```
```python title="Python" expandable theme={null}
import requests
# Replace these with your values
CLIENT_ID = ""
CLIENT_SECRET = ""
ENVIRONMENT = ""
TENANT = ""
def get_access_token():
"""Request an OAuth2 client-credentials access token from Corti."""
url = f"https://auth.{ENVIRONMENT}.corti.app/realms/{TENANT}/protocol/openid-connect/token"
data = {
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"grant_type": "client_credentials",
"scope": "openid",
}
res = requests.post(url, data=data, headers={"Content-Type": "application/x-www-form-urlencoded"})
res.raise_for_status()
return res.json()["access_token"]
# Example usage
if __name__ == "__main__":
token = get_access_token()
print("Access token:", token)
```
```bash title="cURL" theme={null}
# Replace these with your values
CLIENT_ID=""
CLIENT_SECRET=""
ENVIRONMENT=""
TENANT=""
curl "https://auth.${ENVIRONMENT}.corti.app/realms/${TENANT}/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "grant_type=client_credentials" \
-d "scope=openid"
```
Tokens expire after **300 seconds** (5 minutes), refresh as needed.
# Security Best Practices
Source: https://docs.corti.ai/authentication/security_best_practices
Five essential steps for keeping your client credentials and access tokens secure.
Client credentials act as a powerful service account. Anyone holding them can act on your behalf and access your tenant. Protect them as you would any internal system password.
## How to keep your tokens safe
### 1. Use environment variables and a proper secret store
Never hardcode your `client_secret` in source files. Use environment variables and a secure secret manager provided by your cloud platform or infrastructure. Rotate secrets if exposure is suspected.
### 2. Never expose credentials in frontend or untrusted environments
Client credentials must only live on trusted servers. Do not embed them in browser code, mobile apps, desktop apps, or any environment you cannot fully control. Instead, your backend should request access tokens, validate requests, and decide what your users can do.
Never expose client credentials (`client_id` + `client_secret`) to a frontend application. A full-scope access token should also stay on the backend, because it grants the same access as the credentials that produced it.
If you cannot avoid passing a token to the frontend — for example, when opening a real-time speech-to-text WebSocket directly from the browser — issue a **limited-scope token** instead. Request the token with `scope="openid transcribe"` and/or `scope="openid streams"`, and the resulting access token will only be accepted by the corresponding streaming endpoints (`/transcribe` and `/streams`). It cannot be used to read or modify any other data, so the blast radius if it is intercepted is limited to a streaming session.
See [Limited-scope credentials for streaming APIs](#4-if-you-must-use-tokens-in-special-cases-use-limited-scope-credentials) below for the full request format.
### 3. Use a backend proxy to handle all Corti API calls from frontends
When you need a frontend to be able to call the Corti API, it is advised to use a proxy. This means all requests are through a proxy that you control. The proxy injects authentication, performs validation, and enforces user-level rules.
### 4. If you must use tokens in special cases, use limited-scope credentials
Some scenarios — most commonly browser-based real-time speech-to-text — make it impractical to keep every token on the backend. For these cases, Corti supports **limited-scope tokens** that restrict the access token to the streaming APIs only. If such a token is intercepted, it cannot be used to call any other endpoint or to read or modify your data.
**Available streaming scopes:**
| Scope value | Grants access to |
| :-------------------------- | :---------------------------------------- |
| `openid transcribe` | The `/transcribe` WebSocket endpoint only |
| `openid streams` | The `/streams` WebSocket endpoint only |
| `openid transcribe streams` | Both streaming WebSocket endpoints |
The `openid` scope is always required alongside the streaming scope(s).
**Request a limited-scope token from your backend** using the standard OAuth 2.0 client credentials grant — only the `scope` parameter changes:
```bash title="Limited-scope token (transcribe + streams)" theme={null}
curl \
"https://auth.${ENVIRONMENT}.corti.app/realms/${TENANT}/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "client_id=${CLIENT_ID}" -d "client_secret=${CLIENT_SECRET}" \
-d 'grant_type=client_credentials' \
-d 'scope=openid transcribe streams'
```
If you are using the Corti SDK, pass `scopes: ["transcribe"]` and/or `scopes: ["streams"]` to `auth.getToken(...)` — see [Scoped tokens in the JavaScript SDK](/sdk/js/authentication#scoped-tokens) for full examples.
**Recommended pattern:**
1. Frontend asks your backend to start a streaming session.
2. Backend authenticates the user, then calls Corti Auth with `scope=openid transcribe`, `scope=openid streams`, or both, depending on which streaming API the frontend needs.
3. For `/transcribe`, the backend returns the resulting short-lived `access_token` to the frontend, and the frontend uses it to open the `/transcribe` WebSocket.
4. For `/streams`, the backend must also create an interaction via the interactions API and return the resulting `websocketUrl` to the frontend. The browser then connects using that `websocketUrl` rather than constructing a `/streams` WebSocket URL itself.
5. Refresh by repeating the same backend flow before expiry: issue a new limited-scope token, and for `/streams` create a new interaction if a new `websocketUrl` is required.
If you can route WebSocket traffic through your own server, prefer a [proxy](#3-use-a-backend-proxy-to-handle-all-corti-api-calls-from-frontends) over exposing tokens to the frontend at all — it gives you full control over authentication and per-user authorization.
## Related guides
* [Authentication overview](/authentication/overview) — OAuth 2.0 client credentials and how to use access tokens.
* [JavaScript SDK – Scoped tokens](/sdk/js/authentication#scoped-tokens) — issuing and using scoped tokens from the SDK.
* [Dictation Web Component – Authentication](/sdk/dictation/authentication) — passing tokens (including scoped tokens) to the Dictation component.
* [Streams endpoint](/stt/streams) and [Transcribe endpoint](/stt/transcribe) — the streaming APIs that accept limited-scope tokens.
# ACHI
Source: https://docs.corti.ai/coding/achi
Australian Classification of Health Interventions. The national standard for procedure coding in Australia
**Australian Classification of Health Interventions (ACHI).** Maintained by IHACPA. The mandatory procedure classification used alongside ICD-10-AM for inpatient episodes across Australian hospitals.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# CCAM
Source: https://docs.corti.ai/coding/ccam
Classification commune des actes médicaux. The standard for medical procedure coding in France
The French procedure classification, maintained by the ATIH and CNAM. Required for both inpatient PMSI coding and ambulatory physician billing alongside [CIM-10-FR](/coding/cim-10-fr). 8,554 codes using seven-character alphanumeric identifiers (four letters followed by three digits, e.g., `AAFA001`). No encounter-type suffix required.
## Quick start
System identifier: `ccam`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["ccam"],
"context": [{"type": "text", "text": "Patient a bénéficié d\u2019une appendicectomie par cœlioscopie pour appendicite aiguë."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["ccam"],
context: [{ type: "text", text: "Patient a bénéficié d'une appendicectomie par cœlioscopie pour appendicite aiguë." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["ccam"],
"context": [{"type": "text", "text": "Patient a bénéficié d'une appendicectomie par cœlioscopie pour appendicite aiguë."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
We have only evaluated this implementation on data from a limited set of specialties. This may not generalize to all hospitals. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api\&use_case=I'm%20using%20CCAM%20%28France%29%20via%20the%20Corti%20Medical%20Coding%20API%20and%20would%20like%20to%20share%20feedback%20on%20my%20experience.) to share feedback or report issues.
# CCI
Source: https://docs.corti.ai/coding/cci
Canadian Classification of Health Interventions. The national standard for procedure coding in Canada
**Canadian Classification of Health Interventions (CCI).** Maintained by CIHI. The mandatory procedure classification used alongside ICD-10-CA for inpatient episodes in Canadian hospitals.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# Clinical Documentation Integrity (CDI)
Source: https://docs.corti.ai/coding/cdi
Accelerate concurrent and retrospective CDI review by surfacing code suggestions and documentation gaps directly from the clinical note
An implementation guide for CDI teams and the engineering teams building tools for them.
CDI programs ensure clinical documentation accurately reflects patient severity, supports correct DRG assignment, and captures all codes eligible for reimbursement or risk adjustment. The Medical Coding API accelerates both concurrent and retrospective CDI review by surfacing code suggestions and documentation gaps directly from the note — giving CDI specialists a head start on every review.
**What is CDI?** Clinical Documentation Integrity programs bridge the gap between what physicians document and what coders can code. CDI specialists review clinical notes, identify where documentation lacks the specificity needed for accurate coding, and issue physician queries to close those gaps. The goal is not to change clinical care — it is to ensure the documentation reflects the care that was actually delivered.
## Before Building
CDI programs operate in two modes, and each has different integration requirements.
**Concurrent review** happens while the patient is still admitted. CDI specialists review notes daily, identify documentation gaps, and issue queries to the attending physician before discharge. Speed matters — the API needs to be called as notes are updated, and results need to surface quickly.
**Retrospective review** happens after discharge, typically before or just after claim submission. The focus is on finding missed codes and documentation gaps that affect DRG accuracy and reimbursement. Volume matters — you may process thousands of encounters in batch.
Most mature CDI programs do both. Start with the mode that matches your team's current workflow, then expand.
The highest-value output of a CDI integration is not the code list — it is the physician query. A well-designed query workflow turns API `candidates` into actionable questions for physicians.
Consider:
* How do CDI specialists draft queries today? Will the API pre-populate query templates, or surface candidates that specialists triage manually?
* Where do queries live? Are they tracked in a CDI platform, in the EHR, or in a separate worklist?
* How do you track query response and resolution? The API can help measure query yield (what percentage of queried candidates convert to confirmed codes).
The `candidates` list is your query pipeline. Each candidate represents a clinical concept the model found in the note that may warrant physician clarification. Evidence spans show exactly where in the note the concept was mentioned — this is the foundation of a well-supported query.
Inpatient and outpatient CDI serve different goals and use different coding systems.
**Inpatient CDI** focuses on DRG accuracy, CC/MCC capture, and severity of illness. Use `icd10cm-inpatient` for diagnoses and `icd10pcs` for procedures.
**Outpatient CDI** focuses on E\&M level support, chronic condition capture, and medical decision-making complexity. Use `icd10cm-outpatient` for diagnoses and `cpt` for procedures.
If your CDI team covers both settings, your integration needs to select the correct coding systems based on encounter type — the same note processed with inpatient vs. outpatient systems will return different results.
### Success Metrics
The percentage of CDI queries that result in a documentation update and confirmed code change. This is the single most important CDI metric.
Measure:
* Queries issued per period (driven by `candidates` surfaced by the API)
* Query response rate (did the physician respond?)
* Query agreement rate (did the physician agree and update documentation?)
* Net new codes captured from queries
Track which `candidates` items drove successful queries to identify the API's most valuable predictions for your patient population.
Complication and Comorbidity (CC) and Major CC (MCC) designations directly affect DRG weight and reimbursement. Missing a single MCC can mean thousands of dollars in lost revenue.
Measure:
* CC/MCC capture rate before and after API integration
* DRG shifts attributable to CDI queries (cases where documentation improvement changed the DRG)
* Revenue impact of DRG shifts
The API's `candidates` list often surfaces conditions documented in the note but not yet coded at the specificity needed for CC/MCC designation — these are your highest-value query targets.
CDI specialists have limited time per chart. The API should help them focus that time on the charts and findings that matter most.
Measure:
* Charts reviewed per CDI specialist per day
* Time per chart review
* Percentage of charts flagged for query vs. confirmed clean
A well-tuned integration lets specialists skip charts where the API's `codes` align with what's already documented and focus on charts with a high `candidates` count — where documentation gaps are most likely.
Over time, CDI programs should improve the baseline quality of documentation — not just catch errors after the fact.
Measure:
* Average number of `candidates` per note (trending down indicates improving documentation)
* Query rate by physician (identifies who benefits most from education)
* Repeat query topics (identifies systemic documentation gaps by condition type)
These longitudinal metrics help CDI leadership shift from reactive review to proactive physician education.
***
## Implementation
### Concurrent Review — Inpatient
In a concurrent review workflow, the API processes notes as they are updated during the admission. CDI specialists review results daily alongside the chart.
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-inpatient"],
context: [
{
type: "text",
text: "Progress Note — Day 3: Patient continues on IV vancomycin for MRSA bacteremia. Blood cultures from yesterday still pending. Acute kidney injury improving — creatinine down to 1.8 from 2.4. Patient also has history of CHF, currently euvolemic on home dose of furosemide. Diabetes managed with insulin sliding scale, glucose well controlled.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmInpatient],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Progress Note — Day 3: Patient continues on IV vancomycin for MRSA bacteremia. Blood cultures from yesterday still pending. Acute kidney injury improving — creatinine down to 1.8 from 2.4. Patient also has history of CHF, currently euvolemic on home dose of furosemide. Diabetes managed with insulin sliding scale, glucose well controlled.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Progress Note — Day 3: Patient continues on IV vancomycin for MRSA bacteremia. Blood cultures from yesterday still pending. Acute kidney injury improving — creatinine down to 1.8 from 2.4. Patient also has history of CHF, currently euvolemic on home dose of furosemide. Diabetes managed with insulin sliding scale, glucose well controlled.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Progress Note — Day 3: Patient continues on IV vancomycin for MRSA bacteremia. Blood cultures from yesterday still pending. Acute kidney injury improving — creatinine down to 1.8 from 2.4. Patient also has history of CHF, currently euvolemic on home dose of furosemide. Diabetes managed with insulin sliding scale, glucose well controlled."
}
]
}'
```
**What to do with the results:**
* **`codes`** — these are the diagnoses the model confidently predicts from the note. In a concurrent review, compare these against what's already been coded. Missing codes may indicate documentation gaps.
* **`candidates`** — these are your query candidates. Each represents a clinical concept found in the note that may need physician clarification before it can be coded. For example, the model might surface "acute kidney injury" as a candidate if the note mentions rising creatinine but doesn't explicitly document the diagnosis.
**Integration pattern:**
1. Call the API after each significant note update (attending notes, progress notes, operative reports, consult notes)
2. Diff the results against the current working code list
3. Surface net-new `candidates` items to the CDI specialist as potential queries
4. Use evidence spans to show the specialist exactly where in the note each concept was mentioned
5. Track which candidates convert to confirmed codes across the stay
### Retrospective Review — Post-Discharge
In a retrospective workflow, the API processes the complete chart after discharge. The focus is on finding codes that were documented but not captured.
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-inpatient"],
context: [
{
type: "text",
text: "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmInpatient],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin."
}
]
}'
```
**Integration pattern:**
1. Process the complete discharge summary (or concatenated key notes) after coding is complete
2. Compare API `codes` against the submitted code set — codes present in the API response but absent from the claim are review candidates
3. Rank `candidates` by DRG impact — a missed MCC that shifts the DRG is higher priority than a missed secondary diagnosis
4. Route the highest-impact findings to CDI specialists for review and potential late query or coding amendment
### Outpatient CDI
Outpatient CDI workflows typically run post-encounter, comparing the API's predictions against what was submitted.
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-outpatient", "cpt"],
context: [
{
type: "text",
text: "Assessment and Plan: 1. Hypertension — poorly controlled, BP 158/94. Adding amlodipine 5mg daily to existing lisinopril. 2. Type 2 diabetes with diabetic nephropathy — A1c 8.1%, increasing metformin, adding GLP-1 agonist. Urine albumin-creatinine ratio elevated at 45. 3. Obesity — BMI 34.2, counseled on diet and exercise, referral to nutrition. 4. Depression screening positive — PHQ-9 score 14, starting sertraline 50mg, follow-up in 2 weeks.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmOutpatient, CommonCodingSystemEnum.Cpt],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Assessment and Plan: 1. Hypertension — poorly controlled, BP 158/94. Adding amlodipine 5mg daily to existing lisinopril. 2. Type 2 diabetes with diabetic nephropathy — A1c 8.1%, increasing metformin, adding GLP-1 agonist. Urine albumin-creatinine ratio elevated at 45. 3. Obesity — BMI 34.2, counseled on diet and exercise, referral to nutrition. 4. Depression screening positive — PHQ-9 score 14, starting sertraline 50mg, follow-up in 2 weeks.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "text",
"text": "Assessment and Plan: 1. Hypertension — poorly controlled, BP 158/94. Adding amlodipine 5mg daily to existing lisinopril. 2. Type 2 diabetes with diabetic nephropathy — A1c 8.1%, increasing metformin, adding GLP-1 agonist. Urine albumin-creatinine ratio elevated at 45. 3. Obesity — BMI 34.2, counseled on diet and exercise, referral to nutrition. 4. Depression screening positive — PHQ-9 score 14, starting sertraline 50mg, follow-up in 2 weeks.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "text",
"text": "Assessment and Plan: 1. Hypertension — poorly controlled, BP 158/94. Adding amlodipine 5mg daily to existing lisinopril. 2. Type 2 diabetes with diabetic nephropathy — A1c 8.1%, increasing metformin, adding GLP-1 agonist. Urine albumin-creatinine ratio elevated at 45. 3. Obesity — BMI 34.2, counseled on diet and exercise, referral to nutrition. 4. Depression screening positive — PHQ-9 score 14, starting sertraline 50mg, follow-up in 2 weeks."
}
]
}'
```
**For E\&M audit workflows:** Compare the number and complexity of conditions in `codes` + `candidates` against the billed E\&M level. A note with 4+ documented conditions and prescription management supports a higher complexity level than a note with only 1-2 conditions.
***
## Tying It All Together
CDI builds on the [encounter coding](/coding/encounter-coding) foundation by adding a human review layer focused specifically on documentation quality. The API doesn't replace CDI specialists — it gives them a head start by surfacing the candidates and evidence they need to write better queries, faster.
Start with the workflow your CDI team uses today (concurrent or retrospective), measure query yield as your primary success metric, and expand from there.
Please [contact us](mailto:help@corti.ai) if you need help setting up a CDI workflow or have questions about concurrent or retrospective review.
# CHOP
Source: https://docs.corti.ai/coding/chop
Schweizerische Operationsklassifikation. The Swiss standard for procedure coding
**CHOP (Schweizerische Operationsklassifikation).** Published by the Swiss Federal Statistical Office (BFS). The Swiss procedure classification, used alongside the Swiss edition of ICD-10-GM for inpatient episode coding across Swiss hospitals.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# CIE-10-ES
Source: https://docs.corti.ai/coding/cie-10-es
Clasificación Internacional de Enfermedades, 10ª Revisión, Modificación Clínica. The Spanish national standard for diagnosis coding
**CIE-10-ES.** Maintained by the Ministerio de Sanidad. The Spanish adaptation of ICD-10-CM, used across Spanish hospitals and outpatient settings for diagnosis coding, paired with CIE-10-ES Procedimientos for inpatient episodes.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# CIE-10-PCS
Source: https://docs.corti.ai/coding/cie-10-pcs
The Spanish adaptation of ICD-10-PCS. The national standard for procedure coding in Spanish hospitals
**CIE-10-PCS.** Maintained by the Ministerio de Sanidad. The Spanish procedure classification, adapted from ICD-10-PCS and used alongside CIE-10-ES for inpatient hospital coding.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# CIM-10-FR
Source: https://docs.corti.ai/coding/cim-10-fr
French modification of ICD-10 (CIM-10-FR). The standard for diagnosis coding in France
The French modification of ICD-10, maintained by the ATIH. Required for diagnosis coding across French hospitals and the basis for PMSI reporting and T2A reimbursement. Around 16,000 alphanumeric codes (3–5 characters, e.g., `E11.9`).
## Quick start
System identifiers: `cim10fr-inpatient` · `cim10fr-outpatient`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["cim10fr-inpatient"],
"context": [{"type": "text", "text": "Patient hospitalisé pour pneumonie communautaire avec diabète de type 2 connu."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["cim10fr-inpatient"],
context: [{ type: "text", text: "Patient hospitalisé pour pneumonie communautaire avec diabète de type 2 connu." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["cim10fr-inpatient"],
"context": [{"type": "text", "text": "Patient hospitalisé pour pneumonie communautaire avec diabète de type 2 connu."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
We have only evaluated this implementation on data from a limited set of specialties. This may not generalize to all hospitals. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api\&use_case=I'm%20using%20CIM-10-FR%20%28France%29%20via%20the%20Corti%20Medical%20Coding%20API%20and%20would%20like%20to%20share%20feedback%20on%20my%20experience.) to share feedback or report issues.
## Encounter type
**Encounter type is required.** CIM-10-FR coding guidelines differ significantly between inpatient and outpatient settings. For instance, in the outpatient setting, one should only code established diagnoses. In the inpatient setting, one should also include all diagnoses that are uncertain, even those that are ruled out. Use `cim10fr-inpatient` for inpatient cases and `cim10fr-outpatient` for outpatient cases. The correct guideline set is applied automatically based on the suffix.
## Inpatient procedures
For French inpatient PMSI episodes, CIM-10-FR is paired with [CCAM](/coding/ccam) for procedure coding. Use `cim10fr-inpatient` for diagnoses and `ccam` for procedures in the same request.
# Coding Systems
Source: https://docs.corti.ai/coding/coding-systems
Supported medical coding systems, feature availability, and typical combinations by encounter type
## Feature availability
**Stable** Production-ready AI, evaluated on large, diverse datasets. **Beta** Fully functional, with ongoing rapid improvements. **Alpha** Early access only. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to request access.
Click wherever it appears to request early access to that feature.
Interested in a country not listed? [Contact us](https://www.corti.ai/contact-us) to discuss your use case.
## Typical combinations
* **US inpatient admission:** `icd10cm-inpatient` + `icd10pcs`
* **US outpatient / ED / office visit:** `icd10cm-outpatient` + `cpt`
* **UK inpatient admission:** `icd10uk-inpatient` + `opcs4`
* **UK outpatient:** `icd10uk-outpatient`
* **German inpatient admission:** `icd10gm-inpatient` + `ops`
* **German outpatient:** `icd10gm-outpatient`
* **French inpatient admission (PMSI):** `cim10fr-inpatient` + `ccam`
* **French outpatient / ambulatory:** `cim10fr-outpatient` + `ccam`
* **International inpatient:** `icd10int-inpatient` (or a national extension such as `icd10gm-inpatient`)
Please [contact us](mailto:help@corti.ai) if you need help choosing the right coding system for your use case or require a system not listed here.
# CPT
Source: https://docs.corti.ai/coding/cpt
Current Procedural Terminology. The universal language for describing medical, surgical, and diagnostic services in the US outpatient setting and for physician billing
The standard procedure coding system for US outpatient settings, maintained by the AMA and updated annually. Required for physician billing, ambulatory surgery centers, and most outpatient claims. 5-digit numeric codes (e.g., `99213` for an office visit, `93000` for an ECG).
## Quick start
System identifier: `cpt`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["cpt"],
"context": [{"type": "text", "text": "Office visit for established patient with hypertension and follow-up ECG performed."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["cpt"],
context: [{ type: "text", text: "Office visit for established patient with hypertension and follow-up ECG performed." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["cpt"],
"context": [{"type": "text", "text": "Office visit for established patient with hypertension and follow-up ECG performed."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
We have only evaluated this implementation on data from a limited set of specialties. This may not generalize to all hospitals. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api\&use_case=I'm%20using%20CPT%20%28United%20States%29%20via%20the%20Corti%20Medical%20Coding%20API%20and%20would%20like%20to%20share%20feedback%20on%20my%20experience.) to share feedback or report issues.
# Encounter Diagnosis Coding
Source: https://docs.corti.ai/coding/encounter-coding
Assign accurate diagnosis and procedure codes to clinical encounters using the Medical Coding API
An implementation guide for product and engineering teams building encounter coding workflows using the Corti Medical Coding API.
The most common use of the Medical Coding API is straightforward: take a clinical note, get back structured diagnosis and procedure codes, and route them for human review before submission. This page walks through the decisions, implementation patterns, and success metrics for building that workflow.
## Before Building
Before writing integration code, align on the fundamentals:
The coding systems you request determine the type of codes returned. Getting this right is the most important configuration decision.
**Inpatient** encounters (hospital admissions, observation stays) use:
* `icd10cm-inpatient` for diagnosis codes
* `icd10pcs` for procedure codes
**Outpatient** encounters (office visits, ED encounters, ambulatory surgery) use:
* `icd10cm-outpatient` for diagnosis codes
* `cpt` for procedure codes
**International** encounters use the WHO base classification or a national extension (e.g., `icd10` for the WHO version, `icd10gm` for Germany). See [Coding Systems](/coding/coding-systems) for the full list.
If your platform handles both inpatient and outpatient encounters, use encounter metadata (admission type, facility type, or department) to select the correct coding system at request time.
Decide how code suggestions flow through your system and who interacts with them:
* **Coder-in-the-loop**: Route suggestions to a professional coder who confirms, rejects, or adjusts each code before submission. This is a common workflow in health systems with dedicated coding teams.
* **Physician-facing auto-suggest**: Surface suggestions directly to the treating physician during or after documentation. This works well in smaller practices or outpatient settings where physicians code their own encounters.
* **Pre-populated worklist**: Use API suggestions to pre-fill a coding worklist that coders then review in their existing coding tool.
* **Automated pipeline**: Feed API output directly into downstream billing or analytics systems without manual review.
The `codes` list contains high-confidence predictions suitable for pre-population or automation. The `candidates` list contains clinically relevant but optional codes that benefit from human judgment — surface these as suggestions rather than defaults.
Encounter coding is most valuable when it sits inside existing workflows rather than as a standalone tool.
Determine:
* **Where notes come from** — Are you pulling finalized notes from an EHR, receiving them via HL7/FHIR, or generating them with Corti's ambient documentation?
* **Where codes go** — Do confirmed codes write back to the EHR, feed into a billing system, or populate a claim form?
* **When coding runs** — Does it trigger automatically on note finalization, or does a coder manually initiate it?
Integration scope will heavily influence build complexity. A lightweight copy/paste workflow can be built in days. A fully embedded EHR integration with bi-directional write-back is a larger effort — but delivers significantly more value.
### Success Metrics
Identifying the right metrics early helps you evaluate the integration and build confidence with clinical and revenue cycle stakeholders.
The primary measure of model quality is agreement with the final billed code set.
Measure:
* Agreement rate between API `codes` and final billed codes
* False positive rate (API-suggested codes rejected by reviewers)
* False negative rate (codes added by reviewers that the API missed)
Run a shadow period before go-live: process notes through the API without surfacing results, then compare against what coders submitted independently. This gives you a baseline accuracy number before changing any workflows.
A high acceptance rate (above 80%) with stable accuracy indicates the model is adding genuine value. A low acceptance rate may indicate a mismatch between encounter types and coding system configuration.
If the API is working well, coders should be able to review more encounters per hour because they are confirming suggestions rather than coding from scratch.
Measure:
* Encounters coded per hour (before vs. after)
* Average time per encounter
* Percentage of API suggestions accepted without modification
Coding errors are a leading cause of claim denials. Better initial code suggestions should reduce denial rates over time.
Measure:
* Claim denial rate (before vs. after)
* Denial reasons related to coding errors (incorrect code, missing modifier, insufficient specificity)
* Rework rate for returned claims
This metric takes longer to materialize — typically 2-3 months after go-live — but is one of the strongest ROI indicators for revenue cycle leadership.
The elapsed time between note finalization and code submission reflects both coder efficiency and workflow friction.
Measure:
* Average time from note finalization to code submission
* Backlog size (encounters awaiting coding)
Reducing time-to-code accelerates the revenue cycle and improves cash flow. It also reduces the cognitive burden on coders who otherwise need to re-read notes they may have seen days ago.
***
## Implementation
### Inpatient Encounter Workflow
For inpatient encounters, you typically need both diagnosis and procedure codes. This requires two API calls — one for each coding system.
```ts title="JavaScript" theme={null}
const diagnoses = await client.codes.predict({
system: ["icd10cm-inpatient"],
context: [
{
type: "text",
text: "Discharge Summary: 72-year-old female admitted with acute exacerbation of COPD and community-acquired pneumonia. Treated with IV antibiotics and bronchodilators. Intubated on day 2 for respiratory failure, extubated day 5. Also managed type 2 diabetes with insulin sliding scale. Discharged on oral antibiotics and home oxygen.",
},
],
});
const procedures = await client.codes.predict({
system: ["icd10pcs"],
context: [
{
type: "text",
text: "Operative Report: Endotracheal intubation performed on day 2 for acute hypoxic respiratory failure. Mechanical ventilation maintained for 72 hours. Extubation performed on day 5 without complication.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var diagnoses = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmInpatient],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Discharge Summary: 72-year-old female admitted with acute exacerbation of COPD and community-acquired pneumonia. Treated with IV antibiotics and bronchodilators. Intubated on day 2 for respiratory failure, extubated day 5. Also managed type 2 diabetes with insulin sliding scale. Discharged on oral antibiotics and home oxygen.",
},
],
});
var procedures = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10Pcs],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Operative Report: Endotracheal intubation performed on day 2 for acute hypoxic respiratory failure. Mechanical ventilation maintained for 72 hours. Extubation performed on day 5 without complication.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
headers = {
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
}
url = f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/"
# Diagnoses
diagnoses_response = requests.post(
url,
headers=headers,
json={
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 72-year-old female admitted with acute exacerbation of COPD and community-acquired pneumonia. Treated with IV antibiotics and bronchodilators. Intubated on day 2 for respiratory failure, extubated day 5. Also managed type 2 diabetes with insulin sliding scale. Discharged on oral antibiotics and home oxygen.",
}
],
},
)
diagnoses_response.raise_for_status()
diagnoses = diagnoses_response.json()
# Procedures
procedures_response = requests.post(
url,
headers=headers,
json={
"system": ["icd10pcs"],
"context": [
{
"type": "text",
"text": "Operative Report: Endotracheal intubation performed on day 2 for acute hypoxic respiratory failure. Mechanical ventilation maintained for 72 hours. Extubation performed on day 5 without complication.",
}
],
},
)
procedures_response.raise_for_status()
procedures = procedures_response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
# Diagnoses
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 72-year-old female admitted with acute exacerbation of COPD and community-acquired pneumonia. Treated with IV antibiotics and bronchodilators. Intubated on day 2 for respiratory failure, extubated day 5. Also managed type 2 diabetes with insulin sliding scale. Discharged on oral antibiotics and home oxygen."
}
]
}'
# Procedures
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10pcs"],
"context": [
{
"type": "text",
"text": "Operative Report: Endotracheal intubation performed on day 2 for acute hypoxic respiratory failure. Mechanical ventilation maintained for 72 hours. Extubation performed on day 5 without complication."
}
]
}'
```
### Outpatient Encounter Workflow
For outpatient encounters, diagnosis and procedure codes can be requested in a single call.
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-outpatient", "cpt"],
context: [
{
type: "text",
text: "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmOutpatient, CommonCodingSystemEnum.Cpt],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "text",
"text": "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "text",
"text": "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication."
}
]
}'
```
### Using Evidence Spans
Every code in the response includes `evidences` — references pointing back to the context that drove the prediction. Use these to build trust in the review workflow.
```json theme={null}
{
"system": "icd10cm-inpatient",
"code": "J44.1",
"display": "Chronic obstructive pulmonary disease with (acute) exacerbation",
"evidences": [
{
"contextIndex": 0,
"text": "acute exacerbation of COPD",
"start": 52,
"end": 78
}
]
}
```
In your review interface:
* Highlight the evidence span in the original note when a coder hovers or selects a code
* Let coders see at a glance why the model suggested each code
* Use evidence spans to speed up the confirm/reject decision — coders can validate the suggestion without re-reading the full note
### Input Formats
The `context` field accepts an array of context objects. You can pass multiple context items to provide the model with more clinical information.
See [How it works](/coding/how-it-works) for details on the request schema.
***
## Tying It All Together
Encounter diagnosis coding is the foundation that other medical coding workflows build on. Once you have a working integration:
* Add [CDI review workflows](/coding/cdi) to surface documentation gaps and query candidates
* Use [revenue cycle patterns](/coding/revenue-cycle) for HCC capture and retrospective under-coding detection
Start with a single encounter type (inpatient or outpatient), run a shadow period to establish your accuracy baseline, and expand from there.
Please [contact us](mailto:help@corti.ai) if you need help setting up your encounter coding workflow or have questions about coding system selection.
# HCPCS Level II
Source: https://docs.corti.ai/coding/hcpcs
Healthcare Common Procedure Coding System, Level II. The US standard for non-physician products, supplies, and services, including durable medical equipment, drugs, and ambulance services
**HCPCS Level II.** Maintained by CMS. Covers products, supplies, and services not included in CPT, including durable medical equipment, prosthetics, orthotics, drugs administered in a clinical setting, and ambulance services. Complements CPT for full US professional billing.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# How it works
Source: https://docs.corti.ai/coding/how-it-works
Learn how to make basic and advanced requests for code prediction
Use the following request to send a clinical context, get structured medical codes back -- ready for clinician review:
```
POST https://api.$environment.corti.app/v2/tools/coding/
```
## Basic Usage
Set `system` to an array of one or more system identifiers. Systems are typically combined by encounter type — for example, `icd10cm-outpatient` + `cpt` for office visits and ED encounters, or `icd10cm-inpatient` + `icd10pcs` for hospital admissions. See [Coding Systems](/coding/coding-systems) for the full list.
The `context` field is an array of context objects. Two input types are supported:
* **Text** — `type: "text"` with the clinical text in `text`. Pass multiple objects to provide multiple context sources.
* **Document** — `type: "documentId"` with a document ID in `documentId`, referencing a document already stored in Corti.
`codes` contains the medical entities the model predicts should be coded. `candidates` contains entities that are clinically relevant but not strictly required — optional codes surfaced for human review. `usageInfo.creditsConsumed` reports the credits used for the request, which is reflected in your account billing.
***
## Advanced Usage
Use the optional `filter` field to restrict which codes the model may predict. It accepts an object with three properties:
* **`include`** — codes or categories to include. An empty list (the default) means all codes in the system are eligible.
* **`exclude`** — codes or categories to subtract from the include set. An empty list (the default) means nothing is excluded.
* **`expand`** — when `true` (the default), category codes are expanded to their assignable leaf codes. When `false`, codes are passed through as-is.
Processing follows include → exclude → result semantics: the final prediction set is the include set minus the exclude set.
```json theme={null}
"filter": {
"include": ["J18"],
"exclude": ["J18.9"],
"expand": true
}
```
Omit the `filter` field for open-ended prediction — the API will predict across the full coding system.
Please [contact us](mailto:help@corti.ai) if you need help configuring your requests or have questions about advanced usage.
# ICD-10
Source: https://docs.corti.ai/coding/icd-10
International Classification of Diseases 10th Edition
**ICD-10 is an international code system for diagnoses, published by the World Health Organization.** It is used worldwide, but most nations have implemented their own modification of the original ICD-10 system. We support both the version published by WHO and the modifications of each country.
## Overview
* Around 11,000 assignable codes in the version published by WHO.
* Most countries publish national modifications of ICD-10. Some modifications, such as the British, have a large overlap with the international version. Other countries, such as the US, have little overlap.
* Only conditions that affect patient care should be coded.
* The rules for ICD-10 coding are different for inpatient and outpatient settings. In outpatient, only established diagnoses should be coded, while in inpatient, uncertain and ruled-out conditions should be coded. We, therefore, require the user to specify the setting.
## National extensions
Many countries publish national adaptations of ICD-10 that add country-specific codes or additional granularity beyond the international edition. National extensions are supported as separate coding system identifiers:
| Extension | Country | Status |
| :------------------------------ | :------------- | :-----------: |
| [ICD-10](/coding/icd-10-int) | International | |
| [ICD-10-CM](/coding/icd-10-cm) | USA | |
| [NHS ICD-10](/coding/icd-10-uk) | United Kingdom | |
| [CIM-10-FR](/coding/cim-10-fr) | France | |
| [ICD-10-GM](/coding/icd-10-gm) | Germany | |
| [ICD-10-AM](/coding/icd-10-am) | Australia | |
| [ICD-10-CA](/coding/icd-10-ca) | Canada | |
| [SKS ICD-10](/coding/icd-10-dk) | Denmark | |
| [ICD-10-NL](/coding/icd-10-nl) | Netherlands | |
| [ICD-10-NO](/coding/icd-10-no) | Norway | |
| [CIE-10-ES](/coding/cie-10-es) | Spain | |
| [ICD-10-SE](/coding/icd-10-se) | Sweden | |
| [ICD-10-GM](/coding/icd-10-ch) | Switzerland | |
When enabled, the `system` field accepts the relevant ICD-10 identifier with an encounter-type suffix (e.g., `icd10int-inpatient`, `icd10gm-inpatient`, `cim10fr-outpatient`).
# ICD-10-AM
Source: https://docs.corti.ai/coding/icd-10-am
Australian Modification of ICD-10. The national standard for diagnosis coding across public and private hospitals in Australia
**ICD-10-AM (Australian Modification).** Maintained by the Independent Health and Aged Care Pricing Authority (IHACPA). The mandatory diagnosis classification for all Australian public and private hospitals, used alongside ACHI for inpatient episode coding.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# ICD-10-CA
Source: https://docs.corti.ai/coding/icd-10-ca
Canadian Enhancement of ICD-10. The national standard for diagnosis coding across Canadian hospitals
**ICD-10-CA (Canadian Enhancement).** Maintained by the Canadian Institute for Health Information (CIHI). The national diagnosis classification used across Canadian hospitals, paired with CCI for inpatient procedure coding.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# ICD-10-GM (Swiss Edition)
Source: https://docs.corti.ai/coding/icd-10-ch
The Swiss trilingual edition of ICD-10-GM, published in German, French, and Italian
**ICD-10-GM (Swiss Edition).** Published by the Swiss Federal Statistical Office (BFS). Switzerland adopts the German ICD-10-GM directly, with the BFS publishing the classification in three official languages: German, French, and Italian. Used alongside CHOP for inpatient episode coding.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# ICD-10-CM
Source: https://docs.corti.ai/coding/icd-10-cm
International Classification of Diseases, 10th Revision, Clinical Modification. The standard for documenting diagnoses in the United States
The US clinical modification of ICD-10, maintained by the CDC and updated annually. Required for all US inpatient, outpatient, and physician billing. Over 74,000 alphanumeric codes (3–7 characters, e.g., `E11.649`) covering diagnoses, symptoms, injuries, and health encounters.
## Quick start
System identifiers: `icd10cm-inpatient` · `icd10cm-outpatient`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-inpatient"],
"context": [{"type": "text", "text": "72-year-old male admitted with acute STEMI. History of type 2 diabetes and hypertension."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["icd10cm-inpatient"],
context: [{ type: "text", text: "72-year-old male admitted with acute STEMI. History of type 2 diabetes and hypertension." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["icd10cm-inpatient"],
"context": [{"type": "text", "text": "72-year-old male admitted with acute STEMI. History of type 2 diabetes and hypertension."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
## Encounter type
**Encounter type is required.** ICD-10-CM coding guidelines differ significantly between inpatient and outpatient settings. For instance, in the outpatient setting, one should only code established diagnoses. In the inpatient setting, one should also include all diagnoses that are uncertain, even those that are ruled out. Use `icd10cm-inpatient` for inpatient cases and `icd10cm-outpatient` for outpatient cases. The correct guideline set is applied automatically based on the suffix.
# SKS Diagnosis Codes
Source: https://docs.corti.ai/coding/icd-10-dk
Danish modification of ICD-10. The standard for diagnosis coding in Denmark
**Danish modification of ICD-10.** Maintained by Sundhedsdatastyrelsen (the Danish Health Data Authority). The standard classification for diagnoses across Danish hospitals and healthcare services.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# ICD-10-GM
Source: https://docs.corti.ai/coding/icd-10-gm
German Modification of ICD-10. The standard for diagnosis coding in Germany
The German modification of ICD-10, maintained by the BfArM. Required for diagnosis coding across all German healthcare settings and the basis for DRG-based reimbursement. Around 14,000 alphanumeric codes (3–5 characters, e.g., `E11.90`).
## Quick start
System identifiers: `icd10gm-inpatient` · `icd10gm-outpatient`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10gm-inpatient"],
"context": [{"type": "text", "text": "Patient stationär aufgenommen mit akuter Herzinsuffizienz und Typ-2-Diabetes."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["icd10gm-inpatient"],
context: [{ type: "text", text: "Patient stationär aufgenommen mit akuter Herzinsuffizienz und Typ-2-Diabetes." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["icd10gm-inpatient"],
"context": [{"type": "text", "text": "Patient stationär aufgenommen mit akuter Herzinsuffizienz und Typ-2-Diabetes."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
We have only evaluated this implementation on data from a limited set of specialties. This may not generalize to all hospitals. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api\&use_case=I'm%20using%20ICD-10-GM%20%28Germany%29%20via%20the%20Corti%20Medical%20Coding%20API%20and%20would%20like%20to%20share%20feedback%20on%20my%20experience.) to share feedback or report issues.
## Encounter type
**Encounter type is required.** ICD-10-GM coding guidelines differ significantly between inpatient and outpatient settings. For instance, in the outpatient setting, one should only code established diagnoses. In the inpatient setting, one should also include all diagnoses that are uncertain, even those that are ruled out. Use `icd10gm-inpatient` for inpatient cases and `icd10gm-outpatient` for outpatient cases. The correct guideline set is applied automatically based on the suffix.
## Inpatient procedures
For German inpatient episodes, ICD-10-GM is paired with [OPS](/coding/ops) for procedure coding. Use `icd10gm-inpatient` for diagnoses and `ops` for procedures in the same request.
# ICD-10
Source: https://docs.corti.ai/coding/icd-10-int
The international version of ICD-10 maintained by the WHO
The original ICD-10 published by the WHO, containing around 11,000 codes across five hierarchical levels. Most countries have built national modifications on top of this foundation. Use this system for countries where a national modification is not yet available — it will still produce useful results.
## Quick start
System identifiers: `icd10int-inpatient` · `icd10int-outpatient`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10int-inpatient"],
"context": [{"type": "text", "text": "Patient admitted with community-acquired pneumonia and type 2 diabetes mellitus."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["icd10int-inpatient"],
context: [{ type: "text", text: "Patient admitted with community-acquired pneumonia and type 2 diabetes mellitus." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["icd10int-inpatient"],
"context": [{"type": "text", "text": "Patient admitted with community-acquired pneumonia and type 2 diabetes mellitus."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
## Encounter type
**Encounter type is required.** ICD-10 coding guidelines differ significantly between inpatient and outpatient settings. For instance, in the outpatient setting, one should only code established diagnoses. In the inpatient setting, one should also include all diagnoses that are uncertain, even those that are ruled out. Use `icd10int-inpatient` for inpatient cases and `icd10int-outpatient` for outpatient cases. The correct guideline set is applied automatically based on the suffix.
## Language support
Works best in English and major European languages, with strong performance across dozens of languages. No translation step is required; the model processes the input text directly.
# ICD-10-NL
Source: https://docs.corti.ai/coding/icd-10-nl
Dutch modification of ICD-10. The standard for diagnosis coding in the Netherlands
**ICD-10-NL.** Maintained by Nictiz together with the WHO-FIC collaborating centre. The diagnosis classification used across Dutch hospitals and healthcare providers, often reported alongside DBC billing groups.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# ICD-10-NO
Source: https://docs.corti.ai/coding/icd-10-no
Norwegian ICD-10 with national extensions. The standard for diagnosis coding in Norwegian healthcare
**Norwegian ICD-10.** Maintained by Direktoratet for e-helse. The Norwegian edition of ICD-10 with national extensions, used across Norwegian hospitals and primary care, paired with NCSP and NCMP for procedure coding.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# ICD-10-PCS
Source: https://docs.corti.ai/coding/icd-10-pcs
ICD-10 Procedure Coding System. Used exclusively for inpatient hospital procedures in the United States
The US inpatient procedure coding system, developed by CMS. Required for every procedure on inpatient hospital claims. Over 78,000 seven-character alphanumeric codes, where each character position has a distinct classification axis covering surgical, imaging, and therapeutic procedures. Use alongside [ICD-10-CM](/coding/icd-10-cm) for diagnosis coding. For outpatient procedures, use [CPT](/coding/cpt) instead.
## Quick start
System identifier: `icd10pcs`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10pcs"],
"context": [{"type": "text", "text": "Patient underwent laparoscopic appendectomy for acute appendicitis."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["icd10pcs"],
context: [{ type: "text", text: "Patient underwent laparoscopic appendectomy for acute appendicitis." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["icd10pcs"],
"context": [{"type": "text", "text": "Patient underwent laparoscopic appendectomy for acute appendicitis."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
ICD-10-PCS is for **inpatient procedures only**. For outpatient procedures and physician services, use [CPT](/coding/cpt).
# ICD-10-SE
Source: https://docs.corti.ai/coding/icd-10-se
Swedish modification of ICD-10. The standard for diagnosis coding in Sweden
**Swedish modification of ICD-10.** Maintained by Socialstyrelsen (the National Board of Health and Welfare). The standard classification for diagnoses across Swedish healthcare.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# NHS ICD-10
Source: https://docs.corti.ai/coding/icd-10-uk
United Kingdom modification of ICD-10. The standard for diagnosis coding in the NHS
The UK modification of ICD-10, maintained by NHS England. Around 16,000 alphanumeric codes (4–6 characters; codes shorter than four characters are padded with `X`) covering diagnoses across NHS inpatient and outpatient settings.
## Quick start
System identifiers: `icd10uk-inpatient` · `icd10uk-outpatient`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10uk-inpatient"],
"context": [{"type": "text", "text": "Patient admitted with COPD exacerbation and type 2 diabetes mellitus."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["icd10uk-inpatient"],
context: [{ type: "text", text: "Patient admitted with COPD exacerbation and type 2 diabetes mellitus." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["icd10uk-inpatient"],
"context": [{"type": "text", "text": "Patient admitted with COPD exacerbation and type 2 diabetes mellitus."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
## Encounter type
**Encounter type is required.** ICD-10 coding guidelines differ significantly between inpatient and outpatient settings. For instance, in the outpatient setting, one should only code established diagnoses. In the inpatient setting, one should also include all diagnoses that are uncertain, even those that are ruled out. Use `icd10uk-inpatient` for inpatient cases and `icd10uk-outpatient` for outpatient cases. The correct guideline set is applied automatically based on the suffix.
## Inpatient procedures
For UK inpatient episodes, NHS ICD-10 is paired with [OPCS-4](/coding/opcs-4) for procedure coding. Use `icd10uk-inpatient` for diagnoses and `opcs4` for procedures in the same request.
## Known issues
In England, there are certain secondary codes that one should always code, even if they do not directly impact patient care during the encounter. We have not implemented this list yet.
# ICD-11
Source: https://docs.corti.ai/coding/icd-11
ICD-11. The World Health Organization successor to ICD-10, adopted by a growing number of countries
**ICD-11.** The 11th revision of the International Classification of Diseases, maintained by the WHO. Structurally different from ICD-10 and designed for digital use; adoption is underway in several countries, with more transitioning over the coming years.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# Core Concepts
Source: https://docs.corti.ai/coding/introduction
Understand the key fields in every Code Prediction response
The Code Prediction API converts unstructured clinical contexts (e.g., encounter notes, discharge summaries, transcripts) into structured medical codes. Every response contains two top-level lists, `codes` and `candidates`, each holding code objects with `system`, `code`, `display`, `evidences`, and `alternatives`.
```json theme={null}
{
"codes": [
{
"system": "icd10cm-outpatient",
"code": "E11.649",
"display": "Type 2 diabetes mellitus with hypoglycemia without coma",
"evidences": [{ "contextIndex": 0, "text": "occasional mild hypoglycemia", "start": 90, "end": 118 }],
"alternatives": [{ "code": "E11.65", "display": "Type 2 diabetes mellitus with hyperglycemia" }]
}
],
"candidates": [
{
"system": "icd10cm-outpatient",
"code": "Z79.4",
"display": "Long-term (current) use of insulin",
"evidences": [...],
"alternatives": [...]
}
]
}
```
## The top level
Deciding whether a medical entity should be coded is a difficult and often subjective decision. Every diagnosis, symptom, or health context in a clinical note should not be coded.
For instance, a symptom should only be coded if it is not commonly associated with one of the patient's diagnoses. A chronic condition should only be coded if it affects patient care.
Therefore, two lists are returned at the top level: `codes` and `candidates`:
* `codes` contains the medical entities that the model confidently predicts *should be coded*.
* `candidates` contains entities that are clinically relevant but not strictly required; optional codes that may warrant human review.
**Recommendation**: When building an interface for medical coding, include both lists but visualize their contents differently. Or, to present all codes indicated by a clinical note without concern for billing rules, then merge the two lists into one display.
## Each code object
Each list contains several code objects. Each object represents one medical entity (condition, symptom, health context, medication, or procedure, depending on the coding system).
* `system` is the coding system the code belongs to (e.g. `icd10cm-outpatient`, `cpt`).
* `code` is the code that best describes the entity.
* `display` is the name of that code (often called code description).
* `evidences` contains all the spans of text where the medical entity is mentioned. Each evidence object includes `contextIndex` (the index of the context item in your request array), `text` (the relevant text snippet), and `start`/`end` (0-based character offsets into the context text, inclusive and exclusive respectively).
* `alternatives` contains other codes that could also describe the medical entity — codes the system considered but deemed less relevant than the one in `code`.
## Sequencing
For coding systems where order affects reimbursement — such as ICD-10-CM, ICD-10-GM, CIM-10-FR, NHS ICD-10, and their paired procedure systems — the API returns codes in the correct sequence. The principal diagnosis or principal procedure appears first, followed by secondary diagnoses and additional procedures in order of clinical significance.
This means you can use the position of a code in the `codes` array directly, without needing to re-sort the results.
Sequencing is currently in alpha for supported systems. See the [feature matrix](/coding/coding-systems) for availability by coding system.
# LOINC
Source: https://docs.corti.ai/coding/loinc
Logical Observation Identifiers Names and Codes. The international standard for identifying laboratory tests, clinical observations, and documents
**LOINC (Logical Observation Identifiers Names and Codes).** Maintained by the Regenstrief Institute. The de facto international standard for identifying laboratory and clinical observations. Used worldwide in EHRs, lab systems, and HL7/FHIR interfaces.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# NCSP / NCMP
Source: https://docs.corti.ai/coding/ncsp-ncmp
Nordic Classification of Surgical Procedures and Nordic Classification of Medical Procedures. The Norwegian standards for procedure coding
**NCSP / NCMP.** Maintained by Direktoratet for e-helse. NCSP (Nordic Classification of Surgical Procedures) and NCMP (Nordic Classification of Medical Procedures) are the Norwegian national classifications for surgical and medical procedures, used alongside ICD-10 for inpatient episode coding.
This coding system is in early alpha. Access is available to select partners. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to discuss your use case and request access.
# OPCS-4
Source: https://docs.corti.ai/coding/opcs-4
OPCS Classification of Interventions and Procedures, Version 4. The standard for procedure coding in the NHS
The UK procedure classification, maintained by NHS England. Required for all NHS inpatient procedure coding alongside [NHS ICD-10](/coding/icd-10-uk). Around 10,000 alphanumeric codes structured as a letter followed by two digits with optional decimal subdivision (e.g., `H01.1`). No encounter-type suffix required.
## Quick start
System identifier: `opcs4`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["opcs4"],
"context": [{"type": "text", "text": "Patient underwent total hip replacement under general anaesthesia."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["opcs4"],
context: [{ type: "text", text: "Patient underwent total hip replacement under general anaesthesia." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["opcs4"],
"context": [{"type": "text", "text": "Patient underwent total hip replacement under general anaesthesia."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
# OPS
Source: https://docs.corti.ai/coding/ops
Operationen- und Prozedurenschlüssel. The mandatory procedure classification for inpatient billing in Germany
The German procedure classification, maintained by the BfArM. Required for all inpatient and day-case procedures alongside [ICD-10-GM](/coding/icd-10-gm) and the basis for DRG-based reimbursement. Around 17,000 codes structured as chapter-group-detail with optional decimal (e.g., `5-470.0`). No encounter-type suffix required.
## Quick start
System identifier: `ops`
```bash curl theme={null}
curl -X POST "https://api.$environment.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer " \
-H "Tenant-Name: " \
-H "Content-Type: application/json" \
-d '{
"system": ["ops"],
"context": [{"type": "text", "text": "Patient erhielt eine laparoskopische Cholezystektomie bei akuter Cholezystitis."}]
}'
```
```ts JavaScript theme={null}
const response = await client.codes.predict({
system: ["ops"],
context: [{ type: "text", text: "Patient erhielt eine laparoskopische Cholezystektomie bei akuter Cholezystitis." }],
});
```
```python Python theme={null}
import requests
response = requests.post(
"https://api.$environment.corti.app/v2/tools/coding/",
headers={
"Authorization": "Bearer ",
"Tenant-Name": "",
},
json={
"system": ["ops"],
"context": [{"type": "text", "text": "Patient erhielt eine laparoskopische Cholezystektomie bei akuter Cholezystitis."}],
},
)
print(response.json())
```
[API reference →](/api-reference/codes/predict-codes)
We have only evaluated this implementation on data from a limited set of specialties. This may not generalize to all hospitals. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api\&use_case=I'm%20using%20OPS%20%28Germany%29%20via%20the%20Corti%20Medical%20Coding%20API%20and%20would%20like%20to%20share%20feedback%20on%20my%20experience.) to share feedback or report issues.
# Corti Symphony for Medical Coding
Source: https://docs.corti.ai/coding/overview
Learn about the medical coding endpoints in the Corti API
Corti Symphony for Medical Coding converts unstructured clinical text into structured medical codes for revenue cycle management, health statistics, and more. See the [supported systems table below](#endpoint-functionality) for all available coding systems and countries. Can't find what you need? [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api).
## Medical Coding Endpoints
Convert unstructured clinical text into structured medical codes. Returns primary predictions, evidence spans, alternative codes, and additional code predictions.
### Endpoint Functionality
**Stable** Production-ready AI, evaluated on large, diverse datasets. **Beta** Fully functional, with ongoing rapid improvements. **Alpha** Early access only. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to request access.
Click wherever it appears to request early access to that feature.
Interested in a country not listed? [Contact us](https://www.corti.ai/contact-us) to discuss your use case.
For a description of each feature, see [Code Prediction](/coding/introduction).
# Quickstart & First Request
Source: https://docs.corti.ai/coding/quickstart
Get started with the Corti Medical Coding API
Completing this quickstart will get you a working code prediction response from the Corti Medical Coding API. You can then explore the full API and use cases from there.
## Prerequisites
* **A Corti Console account** — sign up at the [Corti Console](https://console.corti.app/) to get started.
* **An API client** — your tenant name, `clientId`, and `clientSecret` are all found in your API client configuration. See [Creating API clients](https://docs.corti.ai/authentication/creating_clients) to set one up.
POST a clinical note to the code prediction endpoint. The `system` field determines which classification is applied.
Your environment (`eu` or `us`) is set in your API client — see [Creating API clients](https://docs.corti.ai/authentication/creating_clients).
| Parameter | Type | Required | Description |
| --------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `system` | `string[]` | Yes | One or more coding systems. Up to 4 per request. See [Coding Systems](/coding/coding-systems) for all options and recommended pairings. |
| `context` | `object[]` | Yes | The clinical input to code. Each item is a `text` string or a `documentId` referencing a document stored in Corti. |
| `filter` | `object` | No | Restrict predictions to specific codes or categories. See [Code Prediction](/coding/introduction) for details. |
```bash title="JavaScript" theme={null}
npm install @corti/sdk
# or
yarn add @corti/sdk
# or
pnpm add @corti/sdk
```
```bash title="C# .NET" theme={null}
dotnet add package Corti.Sdk
# Alternatively, in your .csproj:
#
```
```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const ENVIRONMENT = "";
const TENANT = "";
const client = new CortiClient({
environment: ENVIRONMENT,
tenantName: TENANT,
auth: {
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
},
});
```
```csharp title="C# .NET" theme={null}
using Corti;
// Replace these with your values
const string CLIENT_ID = "";
const string CLIENT_SECRET = "";
const string ENVIRONMENT = "";
const string TENANT = "";
var client = new CortiClient(
tenantName: TENANT,
environment: ENVIRONMENT,
auth: CortiClientAuth.ClientCredentials(
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET)
);
```
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-inpatient"],
context: [
{
type: "text",
text: "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmInpatient],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin."
}
]
}'
```
The SDK handles OAuth 2.0 token acquisition and refresh automatically using your `clientId` and `clientSecret` — no need to manage bearer tokens manually.
A successful response returns three top-level fields:
* **`codes`** — the model's high-confidence predictions. Each entry includes the `code`, a human-readable `display`, `evidences` (the exact passages from the note that support the prediction, with `contextIndex`, `text`, and `start`/`end` character offsets), and `alternatives` (other codes the model considered for the same finding).
* **`candidates`** — lower-confidence codes worth reviewing. Intended for coder review rather than automatic assignment.
* **`usageInfo`** — credits consumed for this request.
```json theme={null}
{
"codes": [
{
"system": "icd10cm-outpatient",
"code": "E11.649",
"display": "Type 2 diabetes mellitus with hypoglycemia without coma",
"evidences": [
{
"contextIndex": 0,
"text": "occasional mild hypoglycemia",
"start": 90,
"end": 118
}
],
"alternatives": [
{
"code": "E11.65",
"display": "Type 2 diabetes mellitus with hyperglycemia"
}
]
},
{
"system": "icd10cm-outpatient",
"code": "M17.11",
"display": "Primary osteoarthritis, right knee",
"evidences": [
{
"contextIndex": 0,
"text": "bilateral knee pain consistent with osteoarthritis",
"start": 174,
"end": 224
}
]
}
],
"candidates": [
{
"system": "icd10cm-outpatient",
"code": "Z79.4",
"display": "Long-term (current) use of insulin"
}
],
"usageInfo": {
"creditsConsumed": 1.5
}
}
```
Most US encounters require more than one coding system. Pass multiple values in `system` — for example, pairing ICD-10-CM with CPT for an outpatient visit. The response includes codes from all requested systems in the same `codes` array, each tagged with its `system` value.
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-outpatient", "cpt"],
context: [
{
type: "text",
text: "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmOutpatient, CommonCodingSystemEnum.Cpt],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "text",
"text": "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "text",
"text": "Subjective: 58-year-old male presents for routine diabetes management. HbA1c is 7.2%. Reports occasional mild hypoglycemia. Currently on metformin 1000mg twice daily. Also notes bilateral knee pain worsening over past 3 months.\n\nAssessment and Plan: 1. Type 2 diabetes mellitus — well controlled on current regimen, continue metformin, recheck HbA1c in 3 months. 2. Bilateral knee osteoarthritis — refer to orthopedics, start topical diclofenac. 3. Hypoglycemia episodes — counseled on timing of meals relative to medication."
}
]
}'
```
See [Coding Systems](/coding/coding-systems) for recommended pairings by encounter type.
## Next steps
Learn about the endpoint, supported features, and coding system options.
Understand the request schema, response fields, evidence spans, and filtering options.
Encounter coding, CDI review, HCC capture, and revenue cycle workflows.
Please [contact us](mailto:help@corti.ai) if you need help getting started or run into issues with your first request.
# Revenue Cycle
Source: https://docs.corti.ai/coding/revenue-cycle
Surface missed codes and revenue leakage at scale with risk adjustment sweeps and retrospective audits
An implementation guide for revenue cycle teams and the engineering teams building coding audit and risk adjustment tools.
From HCC capture sweeps across a patient panel to retrospective audits on historical claims, the Medical Coding API surfaces missed codes and revenue leakage at scale — without requiring coders to re-read every note from scratch.
## Before Building
Revenue cycle coding workflows fall into two broad categories, and they serve different business objectives.
**HCC capture for risk adjustment** focuses on ensuring every relevant Hierarchical Condition Category is documented and coded at least once per year per patient. This directly affects RAF scores and per-member-per-month revenue in Medicare Advantage and value-based care programs. The API processes outpatient encounter notes and surfaces ICD-10-CM codes that map to HCC categories.
**Retrospective under-coding detection** focuses on finding revenue leakage from historical encounters — missed secondary diagnoses, under-coded severity, or documentation that supported a higher DRG but wasn't captured at the time. The API re-processes historical notes and compares against original claims.
Many organizations run both. HCC sweeps are typically annual campaigns tied to risk adjustment deadlines. Retrospective audits are ongoing quality programs.
The API tells you what codes the documentation supports. To find what was *missed*, you need to compare against what was actually billed.
Plan how you will:
* Pull original claim data (billed codes) for comparison against API results
* Map ICD-10-CM codes to HCC categories using CMS HCC mapping tables
* Identify the delta — codes the API found in the documentation that were not on the original claim
The comparison logic lives in your integration layer, not in the API. The API's job is to extract codes from text. Your system's job is to determine which of those codes represent revenue opportunities.
Not every missed code is worth pursuing. Define how you will rank and triage findings.
Consider:
* **HCC weight**: Higher-weight HCC categories have more revenue impact. Prioritize candidates that map to high-weight categories.
* **DRG impact**: For inpatient retrospective audits, a missed MCC that shifts the DRG is worth more than a missed secondary diagnosis that doesn't.
* **Volume**: A commonly missed code across hundreds of encounters may represent more total value than a rare high-weight miss.
* **Actionability**: Some findings require physician outreach for documentation addenda. Others may be codeable from existing documentation. Prioritize findings that can be acted on without additional physician burden.
### Success Metrics
The percentage of documentable HCCs that are actually coded and submitted. This is the primary metric for risk adjustment programs.
Measure:
* HCC capture rate before and after API integration
* Net new HCCs identified per patient per year
* RAF score improvement attributable to recaptured HCCs
* Revenue impact (per-member-per-month change)
Track capture rate by HCC category to identify which condition types your providers most frequently under-document.
For retrospective audits, measure the actual revenue recovered from findings the API surfaced.
Measure:
* Total revenue recovered from late charges, corrective coding, and DRG upgrades
* Revenue per encounter reviewed
* Cost of review (coder time) vs. revenue recovered — this is your ROI
Not every finding converts to revenue. Track the conversion funnel: API finding → coder review → confirmed under-code → submitted correction → payment received.
The API should make auditors more productive by focusing their attention on the encounters most likely to have findings.
Measure:
* Encounters reviewed per auditor per day
* Hit rate (percentage of reviewed encounters with actionable findings)
* Time per encounter review
Use the API to pre-screen encounters and rank them by likely impact. Auditors review the highest-ranked encounters first, improving hit rate and making better use of limited review capacity.
Over time, the patterns you find in retrospective audits should feed back into provider education and documentation improvement.
Measure:
* Trending under-coded conditions by specialty or provider
* Repeat findings for the same condition type across audit cycles
* Reduction in findings per provider over time (indicates learning)
Use evidence spans from the API to create targeted education materials — showing physicians exactly where in their notes a condition was mentioned but not documented at codeable specificity.
***
## Implementation
### HCC Capture — Risk Adjustment Sweeps
Process outpatient encounter notes to surface ICD-10-CM codes with HCC mappings. Both `codes` and `candidates` are valuable — a chronic condition mentioned casually in a note may appear only in `candidates` but still qualify for HCC capture with physician confirmation.
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-outpatient"],
context: [
{
type: "text",
text: "Assessment and Plan: 1. COPD — stable on current inhalers, continue tiotropium and PRN albuterol. FEV1 52% predicted on last PFTs. 2. CHF with reduced EF — last echo showed EF 30%, on guideline-directed therapy with carvedilol, lisinopril, and spironolactone. Euvolemic today. 3. Chronic kidney disease stage 3b — GFR 38, stable. Monitoring potassium with spironolactone. 4. Former smoker — quit 2 years ago, counseled on continued cessation.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmOutpatient],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Assessment and Plan: 1. COPD — stable on current inhalers, continue tiotropium and PRN albuterol. FEV1 52% predicted on last PFTs. 2. CHF with reduced EF — last echo showed EF 30%, on guideline-directed therapy with carvedilol, lisinopril, and spironolactone. Euvolemic today. 3. Chronic kidney disease stage 3b — GFR 38, stable. Monitoring potassium with spironolactone. 4. Former smoker — quit 2 years ago, counseled on continued cessation.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-outpatient"],
"context": [
{
"type": "text",
"text": "Assessment and Plan: 1. COPD — stable on current inhalers, continue tiotropium and PRN albuterol. FEV1 52% predicted on last PFTs. 2. CHF with reduced EF — last echo showed EF 30%, on guideline-directed therapy with carvedilol, lisinopril, and spironolactone. Euvolemic today. 3. Chronic kidney disease stage 3b — GFR 38, stable. Monitoring potassium with spironolactone. 4. Former smoker — quit 2 years ago, counseled on continued cessation.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-outpatient"],
"context": [
{
"type": "text",
"text": "Assessment and Plan: 1. COPD — stable on current inhalers, continue tiotropium and PRN albuterol. FEV1 52% predicted on last PFTs. 2. CHF with reduced EF — last echo showed EF 30%, on guideline-directed therapy with carvedilol, lisinopril, and spironolactone. Euvolemic today. 3. Chronic kidney disease stage 3b — GFR 38, stable. Monitoring potassium with spironolactone. 4. Former smoker — quit 2 years ago, counseled on continued cessation."
}
]
}'
```
**Integration pattern:**
1. Process encounter notes — for annual sweeps, run all outpatient encounters for the measurement period
2. Map each returned ICD-10-CM code to HCC categories using CMS mapping tables
3. Cross-reference against previously submitted claims for the same patient in the current plan year
4. Codes that map to HCC categories and were *not* already submitted are your capture candidates
5. Prioritize by HCC weight — high-weight categories first
6. Use evidence spans to show reviewers exactly which section of the note mentions the condition
7. Route confirmed findings for physician outreach or coding amendment
### Retrospective Under-Coding Detection
Re-process historical encounter notes and compare the API's output against the original billed codes to identify revenue leakage.
```ts title="JavaScript" theme={null}
const response = await client.codes.predict({
system: ["icd10cm-inpatient"],
context: [
{
type: "text",
text: "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
},
],
});
```
```csharp title="C# .NET" theme={null}
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmInpatient],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
},
],
});
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin.",
}
],
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-inpatient"],
"context": [
{
"type": "text",
"text": "Discharge Summary: 82-year-old male admitted with acute STEMI, treated with primary PCI to LAD with drug-eluting stent placement. Hospital course complicated by cardiogenic shock requiring vasopressors for 48 hours. Also managed acute on chronic systolic heart failure (EF 25%), type 2 diabetes with peripheral neuropathy, and stage 3 CKD. Discharged to skilled nursing facility on dual antiplatelet therapy, beta blocker, ACE inhibitor, and insulin."
}
]
}'
```
**Integration pattern:**
1. Run historical notes through the API
2. Compare returned `codes` against the original claim — codes present in the API response but absent from the claim are under-coding candidates
3. Review `candidates` for additional findings — these may represent conditions documented but not at sufficient specificity for the original coder to capture
4. Rank findings by DRG impact (for inpatient) or HCC weight (for outpatient)
5. Use evidence spans on a stratified sample for validation before acting on results at scale
6. Store evidence spans for any codes promoted to late charges — these form the documentation support trail for payer queries
### Validating Results Before Acting at Scale
Before rolling out findings to coders or physicians, validate the API's accuracy on a representative sample.
| Step | Action |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| 1. Sample | Pull a stratified sample of encounters (by specialty, payer, encounter type) |
| 2. Shadow run | Process each note through the API and compare against original claims |
| 3. Expert review | Have experienced coders review a subset of API findings to confirm accuracy |
| 4. Measure | Calculate precision (what percentage of API findings are valid) and recall (what percentage of known under-codes does the API catch) |
| 5. Calibrate | Adjust your prioritization thresholds based on validation results |
This step is especially important for retrospective audits where findings may trigger claim amendments — you need confidence in the results before taking action.
***
## Tying It All Together
Revenue cycle workflows build on the [encounter coding](/coding/encounter-coding) foundation by adding a comparison layer: what the documentation supports vs. what was actually billed. The delta represents your revenue opportunity.
Start with the highest-value workflow for your organization — typically HCC sweeps for Medicare Advantage plans or retrospective audits for inpatient facilities — validate on a sample, and scale from there.
Please [contact us](mailto:help@corti.ai) if you need help with risk adjustment sweeps, retrospective audits, or processing at scale.
# SNOMED CT
Source: https://docs.corti.ai/coding/snomed-ct
Systematized Nomenclature of Medicine, Clinical Terms. The most comprehensive clinical terminology system worldwide
**Systematized Nomenclature of Medicine, Clinical Terms (SNOMED CT).** Maintained by SNOMED International. The most comprehensive multilingual clinical terminology, covering diagnoses, procedures, findings, and clinical concepts. Used in over 40 countries for electronic health records and clinical documentation.
Symphony for SNOMED CT currently outputs three types of SNOMED concept: findings, disorders, and procedures. On the SNOMED CT Entity Linking Challenge it achieves a micro F1 score of 54.8%.
SNOMED CT prediction has been evaluated only on the International edition using American English clinical notes. The model may not perform equally well in other languages or country editions. We encourage you to evaluate it against your own data and use case. If you notice anything that doesn't look right, please [let us know](https://www.corti.ai/contact-us?products=medical-coding,api) so we can improve it.
## Upcoming improvements
This is our first release of SNOMED CT, and we will continuously improve it. We are currently working on the following improvements:
1. Reducing over-coding of procedures that aren't clinically relevant to the encounter.
2. Predicting a negation attribute, so each coded finding or disorder indicates whether it is affirmed or denied. For example, "the patient denies fever" will carry a negation attribute rather than being coded simply as "fever".
3. Predicting the subject of each code, so you can distinguish who a condition applies to. For example, "diabetes" will indicate whether it applies to the patient or to a family member.
4. Predicting temporality, so each code indicates whether a condition is current or historical. For example, distinguishing an active myocardial infarction from a past one.
We have only evaluated this implementation on data from a limited set of specialties. This may not generalize to all hospitals. [Contact us](https://www.corti.ai/contact-us?products=medical-coding,api) to share feedback or report issues.
# Building Your Ambient Scribe
Source: https://docs.corti.ai/get_started/ambient-scribe
Implementation guide for building an ambient scribe application
An implementation handbook for product and engineering teams building ambient clinical documentation using the Corti platform.
Modeled after structured use-case guides, this document is designed to help you move from concept → workflow → implementation → integration.
## Getting Started
Before writing a single line of code, align on the fundamentals:
Be explicit about *who* this scribe is for and *what* problem it solves. Is it primary care SOAP notes? Specialty consult documentation? Urgent care throughput optimization?
The shape of your clinical output — structure, tone, length, required fields — will vary significantly based on specialty and workflow. A narrowly defined initial use case leads to faster iteration and stronger provider trust.
Decide whether documentation should update live during the visit or generate after the encounter ends:
* Real-time systems improve transparency, allow in-visit correction, and plan ahead for in consultation agents but if network is unstable (or non-existent) it may make for a more difficult first use case.
* Post-encounter generation can simplify UX and solve for offline periods, but you can lose the ability to intervene if user audio is poor quality.
Your choice affects architecture, infrastructure requirements, and provider behavior.
Ambient is the new kid on the block and it solves for a lot with your user base. Some specialties or user groups are also used to using other classic speech technologies like dictation.
Corti offers an API endpoint to support dictation workflows in addition to APIs for building out an ambient scribe. Choosing whether you support this from the start will help you to design the UX in an intuitive way so providers know when Ambient is the right way to go or if they want to go full Dictation. Design for the behaviors you want to drive.
Ambient scribes are most powerful when inside existing clinical workflows (we don’t want to change workflows, we want to support them!).
* Determine what systems you’ll pull context from (e.g. EHR demographics, scheduling system appointment reason) and where documentation will be written back (e.g. EHR note, After Visit Summary).
* Clarify whether you need deep EHR embedding, background API write-back, or a lightweight copy/paste workflow. Integration scope will heavily influence build complexity and timeline.
Clinicians must remain the final authority on documentation. Define how users will review extracted facts, edit generated sections, and approve the final note.
* Should providers be able to listen back to their cases?
* Will edits to documents be logged for your team to track common changes to then adjust prompts?
Designing thoughtful review controls builds trust, supports compliance, and improves long-term accuracy through feedback loops.
### Establish your Success Metrics
Determining the best way to measure success for your scribe can be difficult. The true measure of success is workflow transformation. Before launch, define how you will quantify impact — operationally, clinically, and experientially.
Provider trust and comfort are the leading indicators of long-term adoption.
Measure:
* Overall satisfaction score (CSAT or NPS-style survey)
* Adoption Rates
Ambient tools fail not because they are inaccurate, but because they are cognitively burdensome or unpredictable. Regular pulse surveys (2–4 weeks post-rollout) help detect friction early.
If charting time is currently tracked, this becomes a powerful ROI metric.
Measure:
* Average documentation time per encounter
* After-hours charting ("pajama time")
Even a 20–30% reduction in post-visit documentation time materially improves provider well-being and operational efficiency. Remember, it takes time to see some of these impacts as new tools take time to learn.
Ambient tools often shift clinician attention back to the patient.
Measure:
* Patient-reported perception of provider attentiveness
* Visit quality ratings
Improved patient satisfaction can be a secondary but meaningful outcome of successful ambient implementation.
Tracking the behaviors of end user modification can be a great proxy metric for time savings and even provider trust:
Measure:
* % of sections edited
* Average word-level modification rate
* Most frequently rewritten sections
Don’t be afraid of seeing the edits though! Edits show adoption of tools. What you should focus on is where are the trends in edits and where are the outliers.
***
## The Corti API Basics
The interaction is the central hub for managing conversational sessions, letting you create and update interactions that drive clinical AI workflows.
Real-time, stateless speech-to-text over WebSocket designed to power fluid dictation experiences with reliable medical language recognition.
Extract and retrieve clinically relevant facts from interactions to enhance insight and decision support.
Create and manage AI-driven agents that automate contextual messaging and task workflows with experts registry support.
Live WebSocket interaction streaming that concurrently produces transcripts and clinical facts to support ambient documentation workflows.
Define reusable document structures that ensure clarity and consistency in generated outputs.
Upload and organize audio recordings tied to interactions to fuel downstream transcription and document generation.
Generate polished clinical documents from transcripts and templates for notes, summaries, or referrals.
Convert uploaded recordings into structured, usable text to support review and documentation.
***
## How to Implement Your Ambient Scribe
### 1. Map Your Ambient Workflows
Ambient scribing is not just speech to text + summarization. It is a **clinical workflow system**.
Before building, map the end-to-end experience:
#### Questions to Align On
* Is this **in-person**, **virtual**, or both?
* Should facts be generated live? Or just documents at the end of the visit?
* How should providers:
* Review extracted facts?
* Edit generated documents?
* Approve final documentation?
* What documentation needs do your users have?
* Predefined structured SOAP notes?
* Specialty specific templates?
* User managed templates?
#### Visualize Your Core Workflows
To illustrate the concept with a hypothetical EHR, they may have made the following decisions for their design:
| Question | Answer | Justification |
| ------------------------------------------------------------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Is this in-person, virtual, or both? | Both | The below workflow doesn’t highlight this, but this would impact the UI design for sharing audio either from an attached microphone or a browser tab. |
| Should facts be generated live? Or just documents at the end of the visit? | Live | We’re using the Streams endpoint which is optimized for real time fact generation. |
| How should providers review facts? | In/Post Consultation | In the workflow, we’re presenting facts to providers to edit before submitting for document generation. |
| How should providers edit generated documents and approve final documentation? | Edit in app | The workflow shows the document being presented to the end user after generation. They should make necessary edits before exiting the chart or saving the document. |
| What documentation needs do your users have? | Corti Standard Template List | In the workflow, you’ll see calling the List Templates endpoint which will return the Corti standard list. |
### 2. Determine Audio Capture Strategy
Ambient systems are only as strong as their audio layer. Corti provides multiple capture paths, including browser-based capture via the [JS SDK](/sdk/js/overview).
#### Option A: Realtime Scribe | Browser-Based Capture (JS SDK)
Real time audio capture is a game changer in the clinical world. This is important for two key reasons:
1. **Builds trust** - by capturing live audio, you can bring facts to clinicians live in the consultation. It brings trust to the provider to see the facts extracting in real time and knowing the scribe is following along.
2. **Intercepts issues** - with live audio capture, you can use Corti’s Audio Health events to intercept areas where the audio being received isn’t clear. It’s easier to tell a user the audio isn’t clear in the session rather than after so they can correct it sooner.
This is ideal for:
* Web-based EHRs
* Telehealth platforms
* Embedded scribe widgets
```ts title="JavaScript" expandable theme={null}
import fs from "fs";
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const ACCESS_TOKEN = "";
const INTERACTION_ID = "";
const client = new CortiClient({
auth: {
accessToken: ACCESS_TOKEN,
},
});
let socket;
try {
// Step 1: Connect and send config — SDK waits for CONFIG_ACCEPTED before resolving
socket = await client.stream.connect({
id: INTERACTION_ID,
configuration: {
transcription: {
primaryLanguage: "en",
diarize: false,
isMultichannel: false,
participants: [{ channel: 0, role: "multiple" }],
},
mode: {
type: "facts", // or "transcription" if you don't need facts
outputLocale: "en",
},
},
});
console.log("✅ Connected — session ready");
socket.on("message", (msg) => {
switch (msg.type) {
case "transcript":
// Segments can arrive out of order across speakers — order by time.start
[...msg.data]
.sort((a, b) => a.time.start - b.time.start)
.forEach((seg) => {
console.log(`🗣 [${seg.time.start}s → ${seg.time.end}s] ${seg.transcript}`);
});
break;
case "facts":
msg.fact.forEach((fact) => {
console.log(`💡 Fact [${fact.group}]: ${fact.text}`);
});
break;
case "flushed":
console.log("🔄 Buffer flushed");
break;
case "usage":
console.log(`💳 Credits used: ${msg.credits}`);
break;
case "ENDED":
console.log("🏁 Session ended — server closing socket");
break;
case "error":
console.error("❌ Runtime error:", msg.error);
break;
}
});
socket.on("close", (code, reason) => {
console.log(`🔌 Connection closed [${code}]: ${reason}`);
});
socket.on("error", (err) => console.error("🚨 Connection error:", err.message));
// Step 2: Start sending audio now that config is accepted
sendAudio();
} catch (err) {
// CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
console.error("❌ Failed to connect:", err);
throw err;
}
// --- Audio sending ---
function sendAudio() {
const AUDIO_FILE = "./sample.webm"; // swap with your audio file path
if (!fs.existsSync(AUDIO_FILE)) {
console.warn("⚠️ No audio file found — sending silence simulation");
simulateAudioAndEnd();
return;
}
const audioBuffer = fs.readFileSync(AUDIO_FILE);
const CHUNK_SIZE = 8192; // ~250–500ms chunks recommended
let offset = 0;
console.log(`🎙 Streaming ${audioBuffer.length} bytes of audio...`);
const interval = setInterval(() => {
if (offset >= audioBuffer.length) {
clearInterval(interval);
console.log("✅ All audio sent");
endSession();
return;
}
socket.sendAudio(audioBuffer.slice(offset, offset + CHUNK_SIZE));
offset += CHUNK_SIZE;
}, 300); // send a chunk every 300ms
}
function simulateAudioAndEnd() {
setTimeout(() => endSession(), 2000);
}
// --- Optional: flush the audio buffer mid-session ---
function flushBuffer() {
socket.sendFlush({ type: "flush" });
console.log("📤 Sent flush");
}
// --- End the session ---
function endSession() {
socket.sendEnd({ type: "end" });
console.log("📤 Sent end — waiting for ENDED...");
}
```
```csharp title="C# .NET" expandable theme={null}
using Corti;
// Replace these with your values
const string ACCESS_TOKEN = "";
const string INTERACTION_ID = "";
var client = new CortiClient(
auth: CortiClientAuth.Bearer(accessToken: ACCESS_TOKEN)
);
// Interaction must be created via REST before opening a stream
const string interactionId = INTERACTION_ID;
var stream = await client.CreateStreamApiAsync(interactionId);
// Register handlers before connecting
stream.StreamTranscriptMessage.Subscribe(msg =>
{
foreach (var seg in msg.Data)
Console.WriteLine($"🗣 [{seg.Time.Start}s → {seg.Time.End}s] {seg.Transcript}");
});
stream.StreamFactsMessage.Subscribe(msg =>
{
foreach (var fact in msg.Fact)
Console.WriteLine($"💡 Fact [{fact.Group}]: {fact.Text}");
});
stream.StreamFlushedMessage.Subscribe(_ =>
Console.WriteLine("🔄 Buffer flushed"));
stream.StreamUsageMessage.Subscribe(msg =>
Console.WriteLine($"💳 Credits used: {msg.Credits}"));
stream.StreamEndedMessage.Subscribe(_ =>
// Server closes the connection after sending "ENDED" — no need to close manually
Console.WriteLine("🏁 Session ended — server closing socket"));
stream.StreamErrorMessage.Subscribe(msg =>
Console.Error.WriteLine($"❌ Server error: {msg.Error.Title}"));
stream.ExceptionOccurred.Subscribe(ex =>
Console.Error.WriteLine($"🚨 Connection error: {ex.Message}"));
stream.Closed.Subscribe(info =>
Console.WriteLine($"🔌 Connection closed [{info.Code}]: {info.Reason}"));
try
{
// Step 1: Connect and send config — ConnectAsync waits for CONFIG_ACCEPTED before returning
await stream.ConnectAsync(new StreamConfig
{
Transcription = new StreamConfigTranscription
{
PrimaryLanguage = "en",
diarize = false,
IsMultichannel = false,
Participants = new[]
{
new StreamConfigParticipant { Channel = 0, Role = StreamConfigParticipantRole.Multiple },
},
},
Mode = new StreamConfigMode
{
Type = StreamConfigModeType.Facts, // or StreamConfigModeType.Transcription
OutputLocale = "en",
},
});
Console.WriteLine("✅ Connected — session ready");
// Step 2: Start sending audio now that config is accepted
const string audioFile = "./sample.webm"; // swap with your audio file path
const int chunkSize = 8192; // ~250–500ms chunks recommended
if (!File.Exists(audioFile))
{
Console.WriteLine("⚠️ No audio file found — sending silence simulation");
await Task.Delay(2000);
await stream.Send(new StreamEndMessage());
}
else
{
var audioBytes = await File.ReadAllBytesAsync(audioFile);
Console.WriteLine($"🎙 Streaming {audioBytes.Length} bytes of audio...");
for (int i = 0; i < audioBytes.Length; i += chunkSize)
{
var chunk = audioBytes.AsMemory(i, Math.Min(chunkSize, audioBytes.Length - i));
await stream.Send(chunk.ToArray());
await Task.Delay(300); // send a chunk every 300ms
}
Console.WriteLine("✅ All audio sent");
// Signal end of audio stream
await stream.Send(new StreamEndMessage());
Console.WriteLine("📤 Sent end — waiting for ENDED...");
}
}
catch (Exception ex)
{
// CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
Console.Error.WriteLine($"❌ Failed to connect: {ex.Message}");
throw;
}
```
```javascript Sample code expandable theme={null}
import WebSocket from "ws";
import fs from "fs";
// Replace these with your values
const ACCESS_TOKEN = "";
const ENVIRONMENT = "";
const INTERACTION_ID = ""; // must be created via REST first
const TENANT = "";
const WSS_URL = `wss://api.${ENVIRONMENT}.corti.app/v2/interactions/${INTERACTION_ID}/streams?tenant-name=${TENANT}&token=Bearer%20${ACCESS_TOKEN}`;
const ws = new WebSocket(WSS_URL);
ws.on("open", () => {
console.log("✅ WebSocket connected");
// Step 1: Send config immediately (must be within 10 seconds)
const config = {
type: "config",
configuration: {
transcription: {
primaryLanguage: "en",
diarize: false,
isMultichannel: false,
participants: [
{ channel: 0, role: "multiple" }
]
},
mode: {
type: "facts", // or "transcription" if you don't need facts
outputLocale: "en"
}
}
};
ws.send(JSON.stringify(config));
console.log("📤 Sent config");
});
ws.on("message", (data) => {
// Audio binary frames come back as Buffer — skip those
if (Buffer.isBuffer(data) && !isJson(data)) return;
const message = JSON.parse(data.toString());
console.log("📨 Received:", JSON.stringify(message, null, 2));
switch (message.type) {
case "CONFIG_ACCEPTED":
console.log("✅ Config accepted — session:", message.sessionId);
// Step 2: Start sending audio now that config is accepted
sendAudio();
break;
case "CONFIG_DENIED":
case "CONFIG_MISSING":
case "CONFIG_NOT_PROVIDED":
case "CONFIG_TIMEOUT":
console.error("❌ Config error:", message);
ws.close();
break;
case "transcript":
// Segments can arrive out of order across speakers — order by time.start
[...message.data]
.sort((a, b) => a.time.start - b.time.start)
.forEach((seg) => {
console.log(`🗣 [${seg.time.start}s → ${seg.time.end}s] ${seg.transcript}`);
});
break;
case "facts":
message.fact.forEach((fact) => {
console.log(`💡 Fact [${fact.group}]: ${fact.text}`);
});
break;
case "flushed":
console.log("🔄 Buffer flushed");
break;
case "usage":
console.log(`💳 Credits used: ${message.credits}`);
break;
case "ENDED":
console.log("🏁 Session ended — server closing socket");
// ws closes automatically after this
break;
case "error":
console.error("❌ Runtime error:", message.error);
break;
}
});
ws.on("close", (code, reason) => {
console.log(`🔌 Connection closed [${code}]: ${reason}`);
});
ws.on("error", (err) => {
console.error("🚨 WebSocket error:", err.message);
});
// --- Audio sending ---
function sendAudio() {
const AUDIO_FILE = "./sample.webm"; // swap with your audio file path
if (!fs.existsSync(AUDIO_FILE)) {
console.warn("⚠️ No audio file found — sending silence simulation");
simulateAudioAndEnd();
return;
}
const audioBuffer = fs.readFileSync(AUDIO_FILE);
const CHUNK_SIZE = 8192; // ~250–500ms chunks recommended
let offset = 0;
console.log(`🎙 Streaming ${audioBuffer.length} bytes of audio...`);
const interval = setInterval(() => {
if (ws.readyState !== WebSocket.OPEN) {
clearInterval(interval);
return;
}
if (offset >= audioBuffer.length) {
clearInterval(interval);
console.log("✅ All audio sent");
endSession();
return;
}
const chunk = audioBuffer.slice(offset, offset + CHUNK_SIZE);
ws.send(chunk); // send raw binary — no JSON wrapping
offset += CHUNK_SIZE;
}, 300); // send a chunk every 300ms
}
function simulateAudioAndEnd() {
// Demo: just wait a moment then end
setTimeout(() => endSession(), 2000);
}
// --- Optional: flush the audio buffer mid-session ---
function flushBuffer() {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "flush" }));
console.log("📤 Sent flush");
}
}
// --- End the session ---
function endSession() {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "end" }));
console.log("📤 Sent end — waiting for ENDED...");
}
}
// Helper: check if a Buffer looks like JSON
function isJson(buf) {
try {
JSON.parse(buf.toString());
return true;
} catch {
return false;
}
}
```
A complete end-to-end script — create an interaction, open the stream, pipe audio in, handle transcript and facts `message` events, and close the session.
```js title="JavaScript" theme={null}
import { createReadStream } from "node:fs";
import { randomUUID } from "node:crypto";
import { CortiClient } from "@corti/sdk";
const client = new CortiClient({
environment: "",
tenantName: "",
auth: {
clientId: "",
clientSecret: "",
},
});
// Stand-in for live capture: a real ambient scribe streams microphone audio as
// it's produced (e.g. from getUserMedia / an audio device). Here we read a
// recorded file and pace the chunks below to approximate real-time delivery.
const audioPath = "sample.mp3";
const now = new Date();
const { interactionId } = await client.interactions.create({
assignedUserId: randomUUID(),
encounter: {
identifier: randomUUID(),
status: "planned",
type: "first_consultation",
period: {
startedAt: now,
endedAt: now,
},
title: "Consultation",
},
});
if (!interactionId) throw new Error("Missing interactionId");
const streamSocket = await client.stream.connect({
id: interactionId,
configuration: {
transcription: {
primaryLanguage: "en",
diarize: false,
isMultichannel: false,
participants: [{ channel: 0, role: "multiple" }],
},
mode: { type: "facts", outputLocale: "en" },
},
});
streamSocket.on("message", (message) => {
if (message.type === "facts") {
console.log("Facts:", message);
} else if (message.type === "transcript") {
console.log("Transcript:", message);
} else if (message.type === "error") {
console.error("Error:", message);
} else if (message.type === "ENDED") {
console.log("ENDED");
}
});
// ~8 KB ≈ 250–500 ms of audio; pace sends to mimic a live microphone
const CHUNK_SIZE = 8192;
const CHUNK_INTERVAL_MS = 300;
const audioStream = createReadStream(audioPath, { highWaterMark: CHUNK_SIZE });
for await (const chunk of audioStream) {
streamSocket.sendAudio(chunk);
await new Promise((resolve) => setTimeout(resolve, CHUNK_INTERVAL_MS));
}
streamSocket.sendEnd({ type: "end" });
```
```csharp title="C# .NET" theme={null}
using Corti;
var client = new CortiClient(
tenantName: "",
environment: "",
auth: CortiClientAuth.ClientCredentials(
clientId: "",
clientSecret: "")
);
// Stand-in for live capture: a real ambient scribe streams microphone audio as
// it's produced. Here we read a recorded file and pace the chunks below to
// approximate real-time delivery.
var audioPath = "sample.mp3";
var now = DateTime.UtcNow;
var interaction = await client.Interactions.CreateAsync(
new InteractionsCreateRequest
{
AssignedUserId = Guid.NewGuid().ToString(),
Encounter = new InteractionsEncounterCreateRequest
{
Identifier = Guid.NewGuid().ToString(),
Status = InteractionsEncounterStatusEnum.Planned,
Type = InteractionsEncounterTypeEnum.FirstConsultation,
Period = new InteractionsEncounterPeriod
{
StartedAt = now,
EndedAt = now,
},
Title = "Consultation",
},
}
);
await using var stream = await client.CreateStreamApiAsync(interaction.InteractionId);
stream.StreamFactsMessage.Subscribe(message =>
{
Console.WriteLine(message);
});
stream.StreamErrorMessage.Subscribe(message =>
{
Console.Error.WriteLine($"Error: {message.Error.Title} ({message.Error.Status})");
});
stream.StreamEndedMessage.Subscribe(_ =>
{
Console.WriteLine("ENDED");
});
await stream.ConnectAsync(new StreamConfig
{
Transcription = new StreamConfigTranscription
{
PrimaryLanguage = "en",
diarize = false,
IsMultichannel = false,
Participants = new List
{
new() { Channel = 0, Role = StreamConfigParticipantRole.Multiple },
},
},
Mode = new StreamConfigMode
{
Type = StreamConfigModeType.Facts,
OutputLocale = "en",
},
});
// ~8 KB ≈ 250–500 ms of audio; pace sends to mimic a live microphone
await using var audioStream = File.OpenRead(audioPath);
var buffer = new byte[8192];
int bytesRead;
while ((bytesRead = await audioStream.ReadAsync(buffer)) > 0)
{
if (bytesRead == buffer.Length)
{
await stream.Send(buffer);
}
else
{
var chunk = new byte[bytesRead];
Buffer.BlockCopy(buffer, 0, chunk, 0, bytesRead);
await stream.Send(chunk);
}
await Task.Delay(300);
}
await stream.Send(new StreamEndMessage());
```
```py title="Python" theme={null}
import os
import json
import time
import uuid
import requests
import websocket
from urllib.parse import quote
from datetime import datetime, timezone
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
CLIENT_ID = os.getenv("CLIENT_ID")
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
TENANT_NAME = os.getenv("TENANT_NAME")
ENVIRONMENT = os.getenv("ENVIRONMENT")
def get_access_token():
print("🔐 Step 1 — Authenticating...")
url = f"https://auth.{ENVIRONMENT}.corti.app/realms/{TENANT_NAME}/protocol/openid-connect/token"
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": "openid"
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, data=payload, headers=headers)
if response.ok:
print("✅ Authenticated")
return response.json()["access_token"]
raise Exception(f"Authentication failed: {response.status_code} - {response.text}")
def create_interaction(token):
print("🔗 Step 2 — Creating interaction session...")
url = f"https://api.{ENVIRONMENT}.corti.app/v2/interactions"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Tenant-Name": TENANT_NAME
}
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
payload = {
"assignedUserId": str(uuid.uuid4()),
"encounter": {
"identifier": str(uuid.uuid4()),
"status": "planned",
"type": "first_consultation",
"period": {
"startedAt": now,
"startedAtTzoffset": "+00:00",
"endedAt": now,
"endedAtTzoffset": "+00:00"
},
"title": "Consultation"
}
}
response = requests.post(url, json=payload, headers=headers)
if response.ok:
print("✅ Interaction session created")
return response.json()
raise Exception(f"Interaction creation failed: {response.status_code} - {response.text}")
def stream_audio(ws, path):
# Stand-in for live capture: a real ambient scribe streams microphone audio
# as it's produced. Here we read a recorded file and pace the chunks
# (~8 KB ≈ 250–500 ms) to approximate real-time delivery.
print("🎧 Step 4 — Streaming audio...")
chunk_size = 8192
with open(path, "rb") as f:
while chunk := f.read(chunk_size):
ws.send(chunk, opcode=websocket.ABNF.OPCODE_BINARY)
time.sleep(0.3)
ws.send(json.dumps({"type": "end"}))
print("✅ Audio stream completed")
def connect_and_stream_audio(ws_url, audio_path):
print("🌐 Step 3 — Connecting to WebSocket and sending config...")
transcript_lines = []
def on_open(ws):
config = {
"type": "config",
"configuration": {
"transcription": {
"primaryLanguage": "en",
"diarize": False,
"isMultichannel": False,
"participants": [{"channel": 0, "role": "multiple"}]
},
"mode": {"type": "facts", "outputLocale": "en"}
}
}
ws.send(json.dumps(config))
print("✅ Configuration sent")
def on_message(ws, message):
msg = json.loads(message)
msg_type = msg.get("type").upper()
if msg_type == "CONFIG_ACCEPTED":
print("🟢 Config accepted by server")
stream_audio(ws, audio_path)
elif msg_type == "TRANSCRIPT":
print("📄Step 5 -Transcript received:")
content = " ".join([x["transcript"] for x in msg["data"]])
if content:
transcript_lines.append(content)
elif msg_type == "FACTS":
print("📄Step 5 - Facts received:")
print(json.dumps(msg, indent=2))
elif msg_type == "ENDED":
print("📌 Step 6 — Session ended by server")
if transcript_lines:
print("\n📝 Final Transcript:")
for line in transcript_lines:
print(line)
else:
print("⚠️ No transcript received.")
ws.close()
def on_close(ws, code, reason):
print("✅ WebSocket closed")
websocket.enableTrace(False)
ws = websocket.WebSocketApp(
ws_url,
on_open=on_open,
on_message=on_message,
on_error=lambda ws, err: print(f"[ERROR] {err}"),
on_close=on_close
)
ws.run_forever()
def main(audio_path):
if not os.path.isfile(audio_path):
raise FileNotFoundError(f"Missing file: {audio_path}")
token = get_access_token()
interaction = create_interaction(token)
print("\n📦 Final interaction response:")
print(json.dumps(interaction, indent=2))
ws_url = interaction.get("websocketUrl")
if not ws_url:
raise ValueError("Missing WebSocket URL in response")
ws_url += f"&token={quote(f'Bearer {token}')}"
connect_and_stream_audio(ws_url, audio_path)
if __name__ == "__main__":
audio_file_path = "PATH_TO_FILE.wav"
main(audio_file_path)
```
#### Option B: Async Scribe | External Capture + Send Audio
Sometimes conditions aren’t prime for real time audio transmission. That could be due to existing architecture constraints or because your customer base may not have reliable access to internet in the work that they do.
```ts title="JavaScript" expandable theme={null}
import { createReadStream } from "fs";
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const ACCESS_TOKEN = "";
const client = new CortiClient({
auth: {
accessToken: ACCESS_TOKEN,
},
});
// ─── STEP 1 · Create Interaction ────────────────────────────────────────────
const { interactionId } = await client.interactions.create({
encounter: {
identifier: crypto.randomUUID(),
status: "planned",
type: "first_consultation",
period: { startedAt: new Date().toISOString() },
},
});
console.log("✅ Interaction created:", interactionId);
// ─── STEP 2 · Upload Recording ───────────────────────────────────────────────
const { recordingId } = await client.recordings.upload(
createReadStream("recording.mp3", { autoClose: true }),
interactionId
);
console.log("✅ Recording uploaded:", recordingId);
// ─── STEP 3 · Generate Transcript ───────────────────────────────────────────
const transcript = await client.transcripts.create(interactionId, {
recordingId,
primaryLanguage: "en",
diarize: true, // separate speakers
isMultichannel: false,
});
console.log("✅ Transcript generated");
// ─── STEP 4 · Extract Facts ─────────────────────────────────────────────────
const context = [
{
type: "text" as const,
text: (transcript.transcripts ?? []).map((t) => t.text).join(" "),
},
];
const { facts } = await client.facts.extract({
context,
outputLanguage: "en",
});
console.log(`✅ Facts extracted: ${facts.length} found`);
facts.forEach((fact) => {
console.log(`💡 [${fact.group}]: ${fact.text}`);
});
```
```csharp title="C# .NET" expandable theme={null}
using Corti;
// Replace these with your values
const string ACCESS_TOKEN = "";
var client = new CortiClient(
auth: CortiClientAuth.Bearer(accessToken: ACCESS_TOKEN)
);
// ─── STEP 1 · Create Interaction ────────────────────────────────────────────
var interaction = await client.Interactions.CreateAsync(
new InteractionsCreateRequest
{
Encounter = new InteractionsEncounterCreateRequest
{
Identifier = Guid.NewGuid().ToString(),
Status = InteractionsEncounterStatusEnum.Planned,
Type = InteractionsEncounterTypeEnum.FirstConsultation,
Period = new InteractionsEncounterPeriod { StartedAt = DateTime.UtcNow },
},
}
);
Console.WriteLine($"✅ Interaction created: {interaction.InteractionId}");
// ─── STEP 2 · Upload Recording ───────────────────────────────────────────────
await using var audioStream = File.OpenRead("recording.mp3");
var recording = await client.Recordings.UploadAsync(interaction.InteractionId, audioStream);
Console.WriteLine($"✅ Recording uploaded: {recording.RecordingId}");
// ─── STEP 3 · Generate Transcript ───────────────────────────────────────────
var transcript = await client.Transcripts.CreateAsync(
interaction.InteractionId,
new TranscriptsCreateRequest
{
RecordingId = recording.RecordingId,
PrimaryLanguage = "en",
Diarize = true, // separate speakers
IsMultichannel = false,
}
);
Console.WriteLine("✅ Transcript generated");
// ─── STEP 4 · Extract Facts ─────────────────────────────────────────────────
var context = new[]
{
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = string.Join(" ", (transcript.Transcripts ?? Enumerable.Empty()).Select(t => t.Text)),
},
};
var factsResponse = await client.Facts.ExtractAsync(new FactsExtractRequest
{
Context = context,
OutputLanguage = "en",
});
Console.WriteLine($"✅ Facts extracted: {factsResponse.Facts.Count()} found");
foreach (var fact in factsResponse.Facts)
{
Console.WriteLine($"💡 [{fact.Group}]: {fact.Text}");
}
```
```python title="Python" expandable theme={null}
# Corti API – Async Workflow (Python)
# 1. Create Interaction 2. Upload Recording 3. Generate Transcript 4. Extract Facts
import requests
import uuid
from datetime import datetime, timezone
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
BASE_URL = f"https://api.{ENVIRONMENT}.corti.app/v2"
HEADERS = {
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
}
# ─── STEP 1 · Create Interaction ────────────────────────────────────────────
now = datetime.now(timezone.utc).isoformat()
res = requests.post(f"{BASE_URL}/interactions", headers=HEADERS, json={
"encounter": {
"identifier": str(uuid.uuid4()),
"status": "planned",
"type": "first_consultation",
"period": {"startedAt": now},
},
})
res.raise_for_status()
interaction_id = res.json()["interactionId"]
print(f"✅ Interaction created: {interaction_id}")
# ─── STEP 2 · Upload Recording ──────────────────────────────────────────────
upload_headers = {
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/octet-stream",
}
with open("recording.mp3", "rb") as f:
res = requests.post(
f"{BASE_URL}/interactions/{interaction_id}/recordings/",
headers=upload_headers,
data=f,
)
res.raise_for_status()
recording_id = res.json()["recordingId"]
print(f"✅ Recording uploaded: {recording_id}")
# ─── STEP 3 · Generate Transcript ───────────────────────────────────────────
res = requests.post(
f"{BASE_URL}/interactions/{interaction_id}/transcripts/",
headers=HEADERS,
json={
"recordingId": recording_id,
"primaryLanguage": "en",
"diarize": True, # separate speakers
"isMultichannel": False,
},
)
res.raise_for_status()
transcript = res.json()
print("✅ Transcript generated")
# ─── STEP 4 · Extract Facts ─────────────────────────────────────────────────
transcript_text = " ".join(
t["text"] for t in (transcript.get("transcripts") or [])
)
res = requests.post(
f"{BASE_URL}/tools/extract-facts",
headers=HEADERS,
json={
"context": [{"type": "text", "text": transcript_text}],
"outputLanguage": "en",
},
)
res.raise_for_status()
facts = res.json().get("facts", [])
print(f"✅ Facts extracted: {len(facts)} found")
for fact in facts:
print(f"💡 [{fact.get('group')}]: {fact.get('text')}")
```
```javascript Raw JavaScript expandable theme={null}
// Corti API – Async Workflow
// 1. Create Interaction 2. Upload Recording 3. Generate Transcript 4. Extract Facts
// Replace these with your values
const ENVIRONMENT = "";
const TENANT = "";
const TOKEN = "";
const BASE_URL = `https://api.${ENVIRONMENT}.corti.app/v2`;
const headers = {
"Authorization": `Bearer ${TOKEN}`,
"Tenant-Name": TENANT,
"Content-Type": "application/json",
};
// ─── STEP 1 · Create Interaction ────────────────────────────────────────────
async function createInteraction(): Promise {
const res = await fetch(`${BASE_URL}/interactions`, {
method: "POST",
headers,
body: JSON.stringify({
encounter: {
identifier: crypto.randomUUID(),
status: "planned",
type: "first_consultation",
period: { startedAt: new Date().toISOString() },
},
}),
});
if (!res.ok) throw new Error(`Create interaction failed: ${res.status}`);
const data = await res.json();
const interactionId: string = data.interactionId;
console.log("✅ Interaction created:", interactionId);
return interactionId;
}
// ─── STEP 2 · Upload Recording (full file as octet-stream) ──────────────────
async function uploadRecording(
interactionId: string,
audioBuffer: ArrayBuffer // full recording file contents
): Promise {
const res = await fetch(`${BASE_URL}/interactions/${interactionId}/recordings/`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Tenant-Name": TENANT,
"Content-Type": "application/octet-stream",
},
body: audioBuffer,
});
if (!res.ok) throw new Error(`Upload recording failed: ${res.status}`);
const data = await res.json();
const recordingId: string = data.recordingId;
console.log("✅ Recording uploaded:", recordingId);
return recordingId;
}
// ─── STEP 3 · Generate Transcript ───────────────────────────────────────────
async function createTranscript(
interactionId: string,
recordingId: string
): Promise {
const res = await fetch(`${BASE_URL}/interactions/${interactionId}/transcripts/`, {
method: "POST",
headers,
body: JSON.stringify({
recordingId,
primaryLanguage: "en",
diarize: true, // separate speakers
isMultichannel: false,
}),
});
if (!res.ok) throw new Error(`Create transcript failed: ${res.status}`);
const data = await res.json();
console.log("✅ Transcript generated");
return data;
}
// ─── STEP 4 · Extract Facts ─────────────────────────────────────────────────
async function extractFacts(transcriptData: any): Promise
### 3. Use Facts to Keep Providers in the Loop
You see a lot about FactsR in our documentation. We’re proud of what we’ve built because we’ve found it to be a tool that reduces provider review time before document generation, increases provider adoption, reduces hallucinations in generated documentation.
Do you have to use facts for your application? *No.*
Do we recommend it from our experience? *Absolutely.*
#### Why Add Facts?
Many Corti customers give their end users the ability to include relevant information from other data sources. For example, some organizations will opt to insert the patient’s problem list from the EHR as a fact to ensure inclusion in post consultation documentation even though they may not discuss each item in the consultation (or they want it to drive an in consultation agentic workflow!).
Similarly, providers may want to dictate facts after a consultation or simply type in additional facts to add to the clinical context for the final document.
```ts title="JavaScript" theme={null}
// Replace these with your values
const INTERACTION_ID = "";
await client.facts.create(INTERACTION_ID, {
facts: [
{
text: "Patient has a history of hypertension.",
group: "other",
},
],
});
```
```csharp title="C# .NET" theme={null}
// Replace these with your values
const string INTERACTION_ID = "";
await client.Facts.CreateAsync(
INTERACTION_ID,
new FactsCreateRequest
{
Facts = new List
{
new() { Text = "Patient has a history of hypertension.", Group = "other" },
},
}
);
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
INTERACTION_ID = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/interactions/{INTERACTION_ID}/facts",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"facts": [
{
"text": "Patient has a history of hypertension.",
"group": "other",
}
]
},
)
response.raise_for_status()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
INTERACTION_ID=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/interactions/${INTERACTION_ID}/facts" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"facts": [
{ "text": "Patient has a history of hypertension.", "group": "other" }
]
}'
```
#### Why Remove Facts?
Corti’s fact extraction will extract all medically relevant facts in a consultation. While this is great to make sure that all information is presented to the provider, not all facts may be relevant for all of the different documents you may generate (e.g. a referral letter might not need corti-emergency-situation-details facts).
We have found giving users the ability to deselect facts keeps them in the loop and gives them more control over the documentation being generated.
```ts title="JavaScript" theme={null}
// Replace these with your values
const FACT_ID = "";
const INTERACTION_ID = "";
await client.facts.update(INTERACTION_ID, FACT_ID, {
isDiscarded: true,
});
```
```csharp title="C# .NET" theme={null}
// Replace these with your values
const string FACT_ID = "";
const string INTERACTION_ID = "";
await client.Facts.UpdateAsync(
INTERACTION_ID,
FACT_ID,
new FactsUpdateRequest { IsDiscarded = true }
);
```
```python title="Python" theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
FACT_ID = ""
INTERACTION_ID = ""
TENANT = ""
TOKEN = ""
response = requests.patch(
f"https://api.{ENVIRONMENT}.corti.app/v2/interactions/{INTERACTION_ID}/facts/{FACT_ID}",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={"isDiscarded": True},
)
response.raise_for_status()
```
```bash title="cURL" theme={null}
# Replace these with your values
ENVIRONMENT=""
FACT_ID=""
INTERACTION_ID=""
TENANT=""
TOKEN=""
curl -X PATCH "https://api.${ENVIRONMENT}.corti.app/v2/interactions/${INTERACTION_ID}/facts/${FACT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{ "isDiscarded": true }'
```
### 4. Determine Your Document Management Strategy
Corti supports multiple approaches to documentation generation. We recommend selecting based on your maturity, product goals, and need for speed.
#### Corti's Recommended Document Strategies
| Approach | Description | Best for | Benefits |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Standard Templates** | The fastest path to getting an ambient scribe in front of your users! | Fast MVPs, Pilot Programs, Out of the box configurations | Predefined templates for structured documentation |
| **Section Assembly** | A more flexible path that lets you (or your users) to slice and dice Corti's standard sections. | Gradual Customization, Specialty Support, Product Differentiation | Specialty support and giving users the ability to assemble their own templates using standard sections. |
| **Full Customization** | Use section level overrides to give you our your end users the ability to further prompt templates to give a fully custom feel while still using Corti's clinical guardrails. | Enterprise Deployments, Deep EHR-aligned formatting | Either tuning sections to having your own custom org templates OR giving users the ability to further prompt templates to create their own custom templates. |
### 5. Integrate With Other Systems
Ambient scribing becomes powerful when integrated bi-directionally.
#### Pull Context Before the Interaction
A common practice is organizations will build such that specific data points are able to be incorporated in the context of their document generation.
Improve quality by pre-loading:
* Chief complaint
* Appointment reason
* Patient demographics
* Medication lists
* Past medical history
Inject these (where relevant) as context before generation to improve accuracy and relevance.
#### Push Outputs After the Interaction
Send:
* Structured sections
* Final narrative note
Into:
* EHR systems
* Practice management software
* Billing systems
* Quality tracking tools
***
## Tying It All Together: Best Practices for Ambient Success
* Always keep clinician in control
* Design for action - don’t maximize content on the screen, maximize what you want actioned
Happy building!
# Clinical Documentation Improvement (Outpatient)
Source: https://docs.corti.ai/get_started/cdi-outpatient
Ensure high accuracy coding by adding CDI validation checks in your workflows.
An implementation handbook for product and engineering teams incorporating coding workflows into your solution using the Corti platform.
Modeled after structured use-case guides, this document is designed to help you move from concept → workflow → implementation → integration.
## Before Building on Corti
Before writing a single line of code, align on the fundamentals:
Start by clearly defining what “improvement” means in your context. CDI is not just about better notes—it’s about accuracy, completeness, and downstream impact.
Common CDI goals include:
* Improving documentation specificity (e.g., laterality, acuity, severity)
* Reducing missing or ambiguous clinical details
* Supporting more accurate coding and reimbursement
* Ensuring compliance and audit readiness
Most teams begin with outpatient workflows due to their lower complexity and more standardized documentation.
CDI depends heavily on the quality and timing of your input data. Corti allows flexibility in how you source documentation context.
Common input sources include:
* Real-time transcript (via streaming or dictation)
* Extracted clinical facts (recommended for structured CDI insights)
* Draft clinical notes (pre-signature)
* Finalized notes (post-review)
You should also decide:
* Do you want to guide documentation in real time or retrospectively?
* Should CDI operate on raw transcripts, structured facts, or composed documents?
Many teams find success starting with facts-first workflows, where structured clinical facts drive CDI suggestions rather than raw transcript alone.
CDI is most effective when it fits naturally into clinical workflows without disrupting care.
Common intervention points include:
* During the encounter (real-time suggestions)
* During note creation (inline guidance while documenting)
* At note completion (pre-signature review)
* Post-encounter (retrospective CDI review workflows)
Earlier intervention improves documentation quality, while later intervention often improves compliance and auditability. Your design should balance both.
Unlike coding, CDI outputs are not just predictions—they are suggestions, gaps, and improvements.
Common CDI outputs include:
* Missing detail suggestions (e.g., “Specify type of heart failure”)
* Clarification prompts (e.g., “Is this condition acute or chronic?”)
* Contradiction detection across documentation
* Structured fact enrichment
* Documentation quality scoring (optional)
Design your outputs to be clear, minimal, and directly actionable for clinicians.
CDI is inherently collaborative between AI, clinicians, and sometimes coders or CDI specialists.
Common workflow patterns include:
* Provider-in-the-loop (real-time or during documentation)
* CDI specialist review (retrospective validation and queries)
You should define:
* Who owns the final documentation?
* When and how feedback loops occur
* How CDI insights feed into coding and quality programs
### Establish your Success Metrics
Clinical Documentation Improvement efforts are often rooted in driving revenue outcomes for your customers or your organization. At its core, CDI is about ensuring the clinical story is complete, accurate, and usable across workflows. It impacts everything from patient care to coding, compliance, and analytics.
CDI is fundamentally about ensuring the clinical story is fully captured.
Measure:
* CDI suggestion acceptance rate – Percentage of suggestions accepted by clinicians
* Missing detail rate - Frequency of incomplete documentation (e.g., unspecified diagnoses)
* Reduction in unspecified codes – Decrease in vague or non-specific documentation over time
A strong CDI workflow should lead to more complete, structured, and clinically accurate documentation.
Beyond completeness, CDI ensures that documentation is internally consistent and clinically sound.
Measure:
* Error correction rate – Frequency of corrections made based on CDI suggestions
* Contradiction rate – Conflicting statements within a note (e.g., acute vs chronic)
* Audit findings – Reduction in documentation-related audit issues
Improving accuracy builds trust across clinicians, coders, and compliance teams.
Clinical Documentation Improvement directly influences coding quality and downstream reimbursement.
Measure:
* Increase in average reimbursement per encounter
* Reduction in undercoding or missed specificity
* Denial rate related to documentation gaps
Even small improvements in documentation specificity can have significant financial impact at scale.
# The Corti API Basics
Before we jump into building, we find it important to establish a shared language for the API endpoints we may reference later. Here's a quick crash course with links out for further reading.
The interaction is the central hub for managing conversational sessions, letting you create and update interactions that drive clinical AI workflows.
Real-time, stateless speech-to-text over WebSocket designed to power fluid dictation experiences with reliable medical language recognition.
Extract and retrieve clinically relevant facts from interactions to enhance insight and decision support.
Predict diagnosis and procedure codes to increase support and accuracy of your coding program.
Live WebSocket interaction streaming that concurrently produces transcripts and clinical facts to support ambient documentation workflows.
Define reusable document structures that ensure clarity and consistency in generated outputs.
Create and manage AI-driven agents that automate contextual messaging and task workflows with experts registry support.
Upload and organize audio recordings tied to interactions to fuel downstream transcription and document generation.
Generate polished clinical documents from transcripts and templates for notes, summaries, or referrals.
Convert uploaded recordings into structured, usable text to support review and documentation.
## Integrating Coding — Coding Endpoint vs Coding Agents
When using Corti to integrate Coding into your workflows, most organizations use one or two primary approaches in Corti: using the Predict Codes endpoint or using a Coding Expert within an agent. Both are powerful but it helps to know when to use each.
### Using Predict Codes
The Predict Codes endpoint is best when you are building a coding assembly line. You send it context, and it gives you back codes, along with supporting evidence. It’s predictable. It’s structured. And it’s easy to plug into downstream systems.
If you’re building something where codes are the output this is usually the right place to start. You always know what you’re getting back, and you can rely on that shape in your application.
### Using a Coding Agent
A Coding Agent is useful when coding is not the end goal, but part of something larger. This includes reviewing documentation, generating summaries, supporting prior auth, or anything where codes inform the process rather than define it.
It also opens the door to combining coding with other capabilities (think Agent/Expert Stacking). You can bring in clinical references, external data, or additional logic and let the agent tie it all together.
In this guide, we use a Coding Expert inside an agent for CDI workflows.
CDI is not just about generating codes, it’s about understanding where documentation lacks specificity and guiding the provider to fix it. That requires reasoning, context, and flexibility, which are better suited for an agent than a fixed endpoint.
Once documentation is complete, you can still use the Predict Codes endpoint downstream to generate structured codes.
## How to Incorporate Clinical Documentation Into your Workflows
### Map Your Coding Workflows
Before building, map how documentation is created, reviewed, and finalized in your system today. CDI should feel like a natural extension of that process, not a separate step or interruption.
The most effective CDI implementations meet clinicians where they already work. You'll want to determine the best time (and place) to insert CDI to provide timely, actionable guidance that improves both documentation quality and downstream outcomes. For CDI programs getting started, we typically recommend starting with something lightweight following the model of:
| Stage | Objective |
| ------------- | ------------------------------- |
| Detection | Agent identifies gap |
| Action | User or CDI specialist responds |
| Resolution | Documentation updated |
| Re-evaluation | System validates outcome |
Before building, map the end-to-end experience:
#### Questions to Align On
Clinical Documentation Improvement can take many forms depending on the type of care, user base, and the intended objectives of your CDI program. The questions below can help to best configure your CDI workflow(s):
* What care settings are you supporting? Inpatient? Outpatient? Emergency department? Specialty workflows?
* Who is the primary user of CDI outputs? Provider? CDI specialist? Coder?
* Should CDI operate in real-time or as a batch process?
* Real-time (inline suggestions as documentation happens)?
* Near real-time (triggered on note save/update)?
* Batch processing (e.g., periodically scanning open encounters)?
* What input context should CDI use?
* Transcript?
* Structured clinical facts?
* Draft note?
* Final note?
* When should CDI intervene in the workflow?
* During the encounter (real-time)?
* During documentation (while the note is being written or during inpatient encounter)?
* At note completion (pre-signature or at signature)?
* Post-encounter (retrospective review)?
#### Visualize Your Core Workflows
To illustrate the concept with a hypothetical EHR, they may have made the following decisions for their design:
| Question | Answer | Justification |
| ------------------------------------------------------ | ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| What care settings are you supporting? | Outpatient | In this example workflow, we want to support an ambulatory EHR workflow. |
| Who is the primary user of CDI outputs? | Provider User | We want to present CDI recommendations directly to the provider in the in office workflow. |
| Should CDI operate in real-time or as a batch process? | Near Real-Time | We want CDI suggestions to present in the context of the encounter before it is closed. |
| What input context should CDI use? | Encounter Note + Selected Codes | We will use a generated note by the clinician as well as the manually selected ICD-10 codes for the encounter. |
| When should CDI intervene in the workflow? | Upon Note Save | We want CDI suggestions to present to the provider as they save their outpatient visit note. |
*Note: The Corti CDI Agent includes the Coding Expert which leverages the same model as the coding endpoints.*
## Stage 1 - Detection - Identify the Gap
At the core of your CDI workflow is your agent. This is where your logic lives—how documentation is interpreted, what gaps are identified, and how feedback is generated.
Unlike traditional rule-based systems, Corti agents allow you to combine clinical reasoning, coding expertise, and structured workflows into a single orchestrated experience.
### Define Your Agent System Prompt with Intent
Most of the behavior of your agent will come from the system prompt. This is where you define how it thinks, what it’s allowed to do, and how it communicates.
In practice, strong CDI agents tend to follow a consistent pattern. They read the chart, extract key elements, and then look for where specificity is missing or where something doesn’t line up. From there, they generate queries, but only when there is enough evidence to support it.
What matters is clarity. The more explicit you are about constraints (don’t infer, don’t lead, always cite evidence), the more reliable your outputs will be. (I always remember the lesson in science class about how to guide someone how to make a peanut butter and jelly sandwich)
### Don't Forget to Call in the Experts
One of the advantages of Corti’s agentic framework is that you can bring in specialized Experts—coding, clinical references, guidelines, calculators.
The key is that your agent should orchestrate, not delegate.
Helps identify specificity gaps and coding-relevant documentation issues
Retrieve current medical information from the public web while enforcing control over where that information comes from.
Create your own clinical reference expert to call in your preferred source!
But the agent should always be the final authority. If an Expert suggests something that isn’t supported by the chart, it should be ignored. This is what keeps the system compliant and audit-safe.
### Create and Test the Agent
When you have a prompt, jump into the Corti Console for a quick and easy way to test your new Agent. You'll be able to then quickly get the output code to create the agent for future use. Here's what that looks like for Corti's out of the box CDI Agent:
```python title="Python" expandable theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/agentic/agents",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={
"name": "Clinical Documentation Improvement (CDI) Agent",
"description": "Identify documentation gaps in clinical charts and generate compliant provider queries to improve coding accuracy",
"connectors": [
{"type": "registry", "name": "pubmed-expert"},
{"type": "registry", "name": "web-search-expert"},
{"type": "registry", "name": "medical-calculator-expert"},
{"type": "registry", "name": "coding-expert"},
],
"systemPrompt": (
"You are the CDI Documentation and Query Orchestrator, a specialized agent within the Corti Agentic Framework. "
"Your purpose is to analyze clinical chart excerpts, identify documentation gaps relevant to Clinical Documentation Improvement (CDI), "
"and generate compliant provider queries.\n\n"
"\n"
"Use only information explicitly present in the provided chart excerpt for patient-specific statements. "
"Never infer missing facts or assume clinical findings that are not documented.\n"
"Do not provide treatment advice under any circumstances.\n"
"All queries must be non-leading, clinically supported, and framed as requests for clarification.\n"
"\n\n"
"\n"
"Structure your response with the following sections: Encounter Summary, Documentation Gaps, "
"Proposed Provider Queries, Coding Specificity Checklist, Risk Flags, and Specialist Trace.\n"
""
),
},
)
response.raise_for_status()
agent_id = response.json()["id"]
```
```bash title="cURL" expandable theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"name": "Clinical Documentation Improvement (CDI) Agent",
"description": "Identify documentation gaps in clinical charts and generate compliant provider queries to improve coding accuracy",
"connectors": [
{ "type": "registry", "name": "pubmed-expert" },
{ "type": "registry", "name": "web-search-expert" },
{ "type": "registry", "name": "medical-calculator-expert" },
{ "type": "registry", "name": "coding-expert" }
],
"systemPrompt": "You are the CDI Documentation and Query Orchestrator..."
}'
```
### Determine Your Context Input
CDI effectiveness is tightly tied to when you run it. The same context that works for a retrospective review will not work for real-time guidance, and vice versa.
Start by aligning your context to your workflow timing:
* If you’re running CDI during the encounter, your inputs will typically be a combination of transcript and early structured facts. This enables early detection of missing specificity, but you should expect incomplete context.
* If you’re running CDI during documentation or on note save, draft notes paired with structured facts tend to produce the most actionable suggestions. At this stage, clinician intent is clearer, and gaps can still be corrected before sign-off.
* If you’re running CDI post-encounter or as a batch process, finalized notes become the primary input. This is where completeness, compliance, and audit readiness matter most—especially for CDI specialist workflows.
Most production systems end up using a hybrid approach depending on encounter type, target user group, and CDI objectives.
### Assembling Context for CDI
Unlike more rigid endpoints, Corti’s agentic workflows do not require heavily structured inputs. This gives you flexibility, but it also means you should be intentional about how you construct your context.
A simple and effective pattern is to concatenate multiple sources of context into a single, well-labeled input string. The goal is not just to pass data, but to provide context for the context!
```ts title="JavaScript" expandable theme={null}
// Assemble CDI context from multiple sources
const draftNote = `
Assessment:
Congestive heart failure.
Plan:
Continue diuretics and monitor fluid status.
`;
const facts = `
- Diagnosis: Heart failure
- Symptoms: Shortness of breath, edema
- Labs: BNP elevated
`;
const transcriptExcerpt = `
Patient reports worsening shortness of breath over the past week,
difficulty lying flat, and swelling in both legs.
`;
const metadata = `
Encounter type: Outpatient
Specialty: Cardiology
`;
// Concatenate with clear section labels
const combinedContext = `
=== DRAFT NOTE ===
${draftNote.trim()}
=== STRUCTURED FACTS ===
${facts.trim()}
=== TRANSCRIPT EXCERPT ===
${transcriptExcerpt.trim()}
=== ENCOUNTER METADATA ===
${metadata.trim()}
`;
```
```csharp title="C# .NET" expandable theme={null}
// Assemble CDI context from multiple sources
var draftNote = """
Assessment:
Congestive heart failure.
Plan:
Continue diuretics and monitor fluid status.
""";
var facts = """
- Diagnosis: Heart failure
- Symptoms: Shortness of breath, edema
- Labs: BNP elevated
""";
var transcriptExcerpt = """
Patient reports worsening shortness of breath over the past week,
difficulty lying flat, and swelling in both legs.
""";
var metadata = """
Encounter type: Outpatient
Specialty: Cardiology
""";
// Concatenate with clear section labels
var combinedContext = $"""
=== DRAFT NOTE ===
{draftNote.Trim()}
=== STRUCTURED FACTS ===
{facts.Trim()}
=== TRANSCRIPT EXCERPT ===
{transcriptExcerpt.Trim()}
=== ENCOUNTER METADATA ===
{metadata.Trim()}
""";
```
```python title="Python" expandable theme={null}
# Assemble CDI context from multiple sources
draft_note = """
Assessment:
Congestive heart failure.
Plan:
Continue diuretics and monitor fluid status.
""".strip()
facts = """
- Diagnosis: Heart failure
- Symptoms: Shortness of breath, edema
- Labs: BNP elevated
""".strip()
transcript_excerpt = """
Patient reports worsening shortness of breath over the past week,
difficulty lying flat, and swelling in both legs.
""".strip()
metadata = """
Encounter type: Outpatient
Specialty: Cardiology
""".strip()
# Concatenate with clear section labels
combined_context = f"""
=== DRAFT NOTE ===
{draft_note}
=== STRUCTURED FACTS ===
{facts}
=== TRANSCRIPT EXCERPT ===
{transcript_excerpt}
=== ENCOUNTER METADATA ===
{metadata}
""".strip()
```
*Note: The above code sample allows for dynamic use of different input types from transcripts, FactsR, encounter notes, and other encounter metadata extracted from the EHR. Depending on where in the workflow this is leveraged, only a subset may be needed/available*
### Pass the Context into a CDI Agent
Once your context is assembled, you pass it directly into your CDI agent as the message input.
Using your CDI agent setup:
```python title="Python" theme={null}
import uuid
import requests
# Replace these with your values
AGENT_ID = ""
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/agentic/agents/{AGENT_ID}/a2a/message:send",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
"A2A-Version": "1.0",
},
json={
"message": {
"role": "ROLE_USER",
"parts": [{"text": combined_context}],
"messageId": str(uuid.uuid4()),
}
},
)
response.raise_for_status()
result = response.json()
```
```bash title="cURL" theme={null}
# Replace these with your values
AGENT_ID=""
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/agentic/agents/${AGENT_ID}/a2a/message:send" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "" }],
"messageId": "msg.0192f4c8-5f8d-7e61-924d-6fb09b5ead73"
}
}'
```
## Stage 2 - Action - User Response
CDI only works if something changes. At this stage, your agent that you built has returned structured output. This could be documentation gaps, supporting evidence, or proposed queries. The job now is simple: present that back to the provider in a way that they can act on quickly.
In an outpatient workflow, this is not a deep review step. It’s a quick moment during documentation where the provider decides whether to adjust the chart before moving on.
### Driving Provider Response
Most interactions at this stage fall into a few simple patterns.
Sometimes the provider will update their documentation directly. A gap like “heart failure without specificity” turns into a quick edit in the note to clarify type or acuity.
Other times, the provider may accept a suggestion conceptually but reword it to match their documentation style. The important part is that the missing detail gets added, not that the exact phrasing is preserved.
In some cases, the provider will ignore the suggestion entirely. This will happen (and when it does, it's useful to know). Not every gap is relevant, and these signals help refine the system over time. Track them!
### Keep the interaction lightweight
The biggest risk at this stage is slowing the provider down.
This should feel like a quick pass, not a task. Think within the lines of:
* suggestions are visible but not overwhelming
* edits happen directly in the note
* no separate workflow or queue is required
If it takes more than a few seconds to understand or act on a suggestion, it’s probably too heavy for this moment.
## Stage 3 - Resolution - Update the Source of Truth
Once the provider makes a change, think back to your workflow to consider where else those changes need to propagate. This is where CDI moves from suggestion → actual system impact.
### What Might Need Updating
When a provider updates their documentation, a few things should happen behind the scenes.
The most immediate is the clinical document itself. The note now reflects the clarified diagnosis, added specificity, or corrected detail.
From there, you can optionally update structured layers:
* The Corti Document. Did you use Corti to generate the note? If so you should update the document.
* Downstream codes. With the improved specificity, you may need to retrigger automated coding from the document.
#### Updating the Corti Document
If using a document generated from Corti as part of your context, you'll want to make sure that you commit any updates to the document back to the original document ID. This will make sure that you have consistency through your workflows and your documents if they're stored both in Corti as well as in your EHR.
```ts title="JavaScript" theme={null}
// Replace these with your values
const DOCUMENT_ID = "";
const INTERACTION_ID = "";
await client.documents.update(INTERACTION_ID, DOCUMENT_ID);
```
```csharp title="C# .NET" theme={null}
// Replace these with your values
const string DOCUMENT_ID = "";
const string INTERACTION_ID = "";
await client.Documents.UpdateAsync(
INTERACTION_ID,
DOCUMENT_ID,
new DocumentsUpdateRequest()
);
```
```python title="Python" theme={null}
import requests
# Replace these with your values
DOCUMENT_ID = ""
ENVIRONMENT = ""
INTERACTION_ID = ""
TENANT = ""
TOKEN = ""
response = requests.patch(
f"https://api.{ENVIRONMENT}.corti.app/v2/interactions/{INTERACTION_ID}/documents/{DOCUMENT_ID}",
headers={
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
},
json={},
)
response.raise_for_status()
```
```bash title="cURL" theme={null}
# Replace these with your values
DOCUMENT_ID=""
ENVIRONMENT=""
INTERACTION_ID=""
TENANT=""
TOKEN=""
curl -X PATCH "https://api.${ENVIRONMENT}.corti.app/v2/interactions/${INTERACTION_ID}/documents/${DOCUMENT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{}'
```
#### Update Downstream Codes
Now that your document has the desired specificity, it's time to make sure any updates to codes are made.
If using Corti for assistance in coding (this could be either from an [Encounter Based Coding Solution](/get_started/encounter-coding) or just the [Predict Codes endpoint](/api-reference/codes/predict-codes)), make sure you introduce a trigger to update codes upon document updates.
If your providers are manually selecting codes, we recommend considering prompting the provider to review codes with the added specificity.
## Stage 4 - Re-Evaluation - Close the Loop
Once updates are made, it’s worth taking one final pass.
This step mirrors what you did in detection (just with better input thanks to your solution!). The updated note, any other updated context, and (optionally) updated codes now represent the most complete version of the encounter.
At this point, you can:
* re-run the agent to confirm gaps were resolved
* catch anything newly introduced during edits
* ensure the final note is complete before sign-off
In most outpatient workflows, this doesn’t need to be visible to the provider. It can run quietly in the background as a final check.
## Tying it All Together: A Complete CDI Loop in your Solution
When you really think about it, CDI isn’t a feature, it’s a loop.
You start with raw clinical context. Your agent identifies what’s missing. The provider makes a quick correction. That correction flows back through your system (into the document, into context, into coding) and then gets verified before the note is finalized.
Nothing extra. No separate workflow. Just a tighter, more complete clinical story every time an encounter is documented.
Happy building!
# Building Your Dictation Solution
Source: https://docs.corti.ai/get_started/dictation
Dictation implementation guide
An implementation handbook for product and engineering teams building dictation tools using the Corti platform.
Modeled after structured use-case guides, this document is designed to help you move from concept → workflow → implementation → integration.
## Before Building on Corti
Before writing a single line of code, align on the fundamentals:
Be explicit about who this dictation experience is for and what problem it solves.
Is it for:
Primary care note creation?
Specialist assessment dictation?
Referral letters and follow-up summaries?
Procedure notes or discharge documentation?
The shape of your clinical output (structure, editing needs, and final destination) will vary significantly depending on the workflow. A narrowly defined initial use case leads to faster iteration and stronger provider trust.
Ambient and dictation solve different problems.
Ambient is ideal when you want the system to listen to a clinical conversation and assist in generating documentation from the encounter. Dictation is ideal when the provider wants to directly control the exact content, wording, structure, and pace of documentation.
Most organizations will want both. Design the UX intentionally so providers understand:
when to start an ambient workflow
when to switch into dictation
when to use dictation to supplement or correct ambient-generated content
Clear boundaries between these modes reduce confusion and drive better adoption.
Dictation is most powerful when it sits directly inside existing documentation workflows.
Determine where providers will dictate, where text should appear, and where the final text should be written back.
For some products, this means deep embedding inside an EHR note editor. For others, it may mean a mobile workflow, a browser-based dictation window, or a lightweight copy/paste experience. Integration scope will heavily influence build complexity and timeline.
Clinicians should always remain the final authority on documentation.
Define how users will review dictated text, correct transcription errors, and ultimately approve final documentation
Think carefully about edit controls, cursor placement, undo patterns, and how providers recover from mistakes. A strong dictation experience is not only about recognition quality. It is also about making correction feel fast and low-friction.
### Establish your Success Metrics
Identifying the best way to measure success for your dictation workflow can be difficult. The true measure of success is not just transcript quality, it is whether providers document faster, with less friction, and with more confidence.
Before launch, define how you will quantify impact operationally, experientially, and behaviorally.
Provider trust and comfort are the leading indicators of long-term adoption.
Measure:
Overall satisfaction score (CSAT or NPS-style survey)
Adoption Rates
Dictation tools fail when they feel unpredictable, overly rigid, or too expensive to correct. Regular pulse surveys can help detect friction early.
If charting time is currently tracked, this becomes one of the clearest ROI metrics.
Measure:
Average documentation time per note
After-hours charting ("pajama time")
Time spent typing vs speaking
A reduction in manual documentation time can materially improve provider experience and throughput.
Ambient tools often shift clinician attention back to the patient.
Measure:
Patient-reported perception of provider attentiveness
Visit quality ratings
Improved patient satisfaction can be a secondary but meaningful outcome of successful ambient implementation.
Track how frequently providers modify dictated text and where those edits occur.
Measure:
Word Error Rate (WER)
Percentage of dictated text accepted with minimal changes
Edits are a normal part of adoption. What matters is identifying the trends, the repeated friction points, and the outliers.
Providers notice immediately when the system struggles with medications, diagnoses, anatomy, and specialty phrasing..
Measure:
Medical Term Recall (WER)
Custom vocabulary success in target specialties
For clinical dictation, terminology performance is not a “nice to have.” It is central to trust.
# The Corti API Basics
The interaction is the central hub for managing conversational sessions, letting you create and update interactions that drive clinical AI workflows.
Real-time, stateless speech-to-text over WebSocket designed to power fluid dictation experiences with reliable medical language recognition.
Extract and retrieve clinically relevant facts from interactions to enhance insight and decision support.
Create and manage AI-driven agents that automate contextual messaging and task workflows with experts registry support.
Live WebSocket interaction streaming that concurrently produces transcripts and clinical facts to support ambient documentation workflows.
Define reusable document structures that ensure clarity and consistency in generated outputs.
Upload and organize audio recordings tied to interactions to fuel downstream transcription and document generation.
Generate polished clinical documents from transcripts and templates for notes, summaries, or referrals.
Convert uploaded recordings into structured, usable text to support review and documentation.
# How to Implement Your Dictation Tool
## 1. Map Your Dictation Workflows
Dictation is not just ASR in a microphone. It is part of your clinical workflow system.
Before building, map the end-to-end experience:
### Questions to Align On
* Is this for desktop, mobile, or both?
* Is the provider dictating into a free-text editor, a sectioned note, or a template-based form?
* How should providers navigate through the chart when dictating?
* How should providers:
* * start and stop dictation?
* * review dictated text?
* * correct errors quickly?
* * approve final documentation?
### Sense Check Your Core Workflows with a Diagram
To illustrate the concept with a hypothetical EHR, they may have made the following decisions for their design:
| Question | Answer | Justification |
| ---------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Is this desktop, mobile, or both? | Both | Providers may dictate in different environments and expect a consistent workflow. |
| Is the provider dictating into a free-text editor, a sectioned note, or a template-based form? | All | We want to build a lightweight dictation component that is available throughout the chart |
| How should providers navigate through the chart when dictating? | Voice Commands | We want to minimize clicks in the system and we want to build a flexible tool that allows providers to use commands to move sections. |
| How should providers review output? | In-editor, in real time | Providers should be able to correct text as it appears rather than waiting until the end. |
## 2. Perfect Your Audio Stream
As with any speech tool, good audio is paramount to its success. The first thing to focus on is a straightforward workflow that gets crisp, clean audio straight from your users to Corti for immediate transcription.
For dictation workflows, real time audio capture is a must have. We find it important for a number of reasons:
Builds trust - by capturing live audio, clinicians see their dictations in real time. It's key to efficiency and trust.
Intercepts issues - with live audio capture, you can use Corti’s Audio Health events to intercept areas where the audio being received isn’t clear. It’s easier to tell a user the audio isn’t clear in the session rather than after so they can correct it sooner.
```ts title="JavaScript" expandable theme={null}
import fs from "fs";
import { CortiClient } from "@corti/sdk";
// Replace these with your values
const ACCESS_TOKEN = "";
const client = new CortiClient({
auth: {
accessToken: ACCESS_TOKEN,
},
});
let socket;
try {
// Step 1: Connect and send config — SDK waits for CONFIG_ACCEPTED before resolving
socket = await client.transcribe.connect({
configuration: {
primaryLanguage: "en",
automaticPunctuation: true,
formatting: {
numbers: "numerals_above_nine",
measurements: "abbreviated",
},
},
});
console.log("✅ Connected — session ready");
socket.on("message", (msg) => {
switch (msg.type) {
case "transcript":
if (msg.data.isFinal) {
console.log(`🗣 [${msg.data.start}s → ${msg.data.end}s] ${msg.data.text}`);
} else {
console.log(`💬 Interim: ${msg.data.text}`);
}
break;
case "command":
console.log(`🎙 Command detected [${msg.data.id}]:`, msg.data.variables);
break;
case "flushed":
console.log("🔄 Buffer flushed");
break;
case "usage":
console.log(`💳 Credits used: ${msg.credits}`);
break;
case "ended":
console.log("🏁 Session ended — server closing socket");
break;
case "error":
console.error("❌ Runtime error:", msg.error);
break;
}
});
socket.on("close", (code, reason) => {
console.log(`🔌 Connection closed [${code}]: ${reason}`);
});
socket.on("error", (err) => console.error("🚨 Connection error:", err.message));
// Step 2: Start sending audio now that config is accepted
sendAudio();
} catch (err) {
// CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
console.error("❌ Failed to connect:", err);
throw err;
}
// --- Audio sending ---
function sendAudio() {
const AUDIO_FILE = "./sample.webm"; // swap with your audio file path
if (!fs.existsSync(AUDIO_FILE)) {
console.warn("⚠️ No audio file found — sending silence simulation");
simulateAudioAndEnd();
return;
}
const audioBuffer = fs.readFileSync(AUDIO_FILE);
const CHUNK_SIZE = 8192; // ~250–500ms per chunk
for (let i = 0; i < audioBuffer.length; i += CHUNK_SIZE) {
socket.sendAudio(audioBuffer.slice(i, i + CHUNK_SIZE));
}
// Signal end of audio stream
socket.sendEnd({ type: "end" });
console.log("📤 Audio sent — end signal dispatched");
}
function simulateAudioAndEnd() {
socket.sendAudio(Buffer.alloc(8192));
socket.sendEnd({ type: "end" });
}
```
```csharp title="C# .NET" expandable theme={null}
using Corti;
// Replace these with your values
const string ACCESS_TOKEN = "";
var client = new CortiClient(
auth: CortiClientAuth.Bearer(accessToken: ACCESS_TOKEN)
);
var transcribe = await client.CreateTranscribeApiAsync();
// Register handlers before connecting
transcribe.TranscribeTranscriptMessage.Subscribe(msg =>
{
if (msg.Data.IsFinal)
Console.WriteLine($"🗣 [{msg.Data.Start}s → {msg.Data.End}s] {msg.Data.Text}");
else
Console.WriteLine($"💬 Interim: {msg.Data.Text}");
});
transcribe.TranscribeCommandMessage.Subscribe(msg =>
Console.WriteLine($"🎙 Command detected [{msg.Data.Id}]: {msg.Data.Variables}"));
transcribe.TranscribeFlushedMessage.Subscribe(_ =>
Console.WriteLine("🔄 Buffer flushed"));
transcribe.TranscribeUsageMessage.Subscribe(msg =>
Console.WriteLine($"💳 Credits used: {msg.Credits}"));
transcribe.TranscribeEndedMessage.Subscribe(_ =>
// Server closes the connection after sending "ended" — no need to close manually
Console.WriteLine("🏁 Session ended — server closing socket"));
transcribe.TranscribeErrorMessage.Subscribe(msg =>
Console.Error.WriteLine($"❌ Server error: {msg.Error.Title}"));
transcribe.ExceptionOccurred.Subscribe(ex =>
Console.Error.WriteLine($"🚨 Connection error: {ex.Message}"));
transcribe.Closed.Subscribe(info =>
Console.WriteLine($"🔌 Connection closed [{info.Code}]: {info.Reason}"));
try
{
// Step 1: Connect and send config — ConnectAsync waits for CONFIG_ACCEPTED before returning
await transcribe.ConnectAsync(new TranscribeConfig
{
PrimaryLanguage = "en",
AutomaticPunctuation = true,
Formatting = new TranscribeFormatting
{
Numbers = TranscribeFormattingNumbers.NumeralsAboveNine,
Measurements = TranscribeFormattingMeasurements.Abbreviated,
},
});
Console.WriteLine("✅ Connected — session ready");
// Step 2: Start sending audio now that config is accepted
const string audioFile = "./sample.webm"; // swap with your audio file path
const int chunkSize = 8192; // ~250–500ms per chunk
if (!File.Exists(audioFile))
{
Console.WriteLine("⚠️ No audio file found — sending silence simulation");
await transcribe.Send(new byte[chunkSize]);
await transcribe.Send(new TranscribeEndMessage());
}
else
{
var audioBytes = await File.ReadAllBytesAsync(audioFile);
for (int i = 0; i < audioBytes.Length; i += chunkSize)
{
var chunk = audioBytes.AsMemory(i, Math.Min(chunkSize, audioBytes.Length - i));
await transcribe.Send(chunk.ToArray());
}
// Signal end of audio stream
await transcribe.Send(new TranscribeEndMessage());
Console.WriteLine("📤 Audio sent — end signal dispatched");
}
}
catch (Exception ex)
{
// CONFIG_DENIED, CONFIG_TIMEOUT, or connection failure
Console.Error.WriteLine($"❌ Failed to connect: {ex.Message}");
throw;
}
```
```javascript Raw JavaScript expandable theme={null}
import WebSocket from "ws";
import fs from "fs";
// Replace these with your values
const ACCESS_TOKEN = "";
const ENVIRONMENT = "";
const TENANT = "";
const CHUNK_SIZE = 8192; // ~250–500ms per chunk
const WSS_URL = `wss://api.${ENVIRONMENT}.corti.app/audio-bridge/v2/transcribe?tenant-name=${TENANT}&token=Bearer%20${ACCESS_TOKEN}`;
const ws = new WebSocket(WSS_URL);
ws.on("open", () => {
console.log("✅ WebSocket connected");
// Step 1: Send config immediately (must be within 10 seconds)
const config = {
type: "config",
configuration: {
primaryLanguage: "en",
automaticPunctuation: true,
formatting: {
numbers: "numerals_above_nine",
measurements: "abbreviated",
},
},
};
ws.send(JSON.stringify(config));
console.log("📤 Sent config");
});
ws.on("message", (data) => {
// Audio binary frames come back as Buffer — skip those
if (Buffer.isBuffer(data) && !isJson(data)) return;
const message = JSON.parse(data.toString());
console.log("📨 Received:", JSON.stringify(message, null, 2));
switch (message.type) {
case "CONFIG_ACCEPTED":
console.log("✅ Config accepted — session:", message.sessionId);
// Step 2: Start sending audio now that config is accepted
sendAudio();
break;
case "CONFIG_DENIED":
case "CONFIG_TIMEOUT":
console.error("❌ Config error:", message);
ws.close();
break;
case "transcript":
if (message.data.isFinal) {
console.log(`🗣 [${message.data.start}s → ${message.data.end}s] ${message.data.text}`);
} else {
console.log(`💬 Interim: ${message.data.text}`);
}
break;
case "command":
console.log(`🎙 Command detected [${message.data.id}]:`, message.data.variables);
break;
case "flushed":
console.log("🔄 Buffer flushed");
break;
case "usage":
console.log(`💳 Credits used: ${message.credits}`);
break;
case "ended":
console.log("🏁 Session ended — server closing socket");
break;
case "error":
console.error("❌ Runtime error:", message.error);
break;
}
});
ws.on("close", (code, reason) => {
console.log(`🔌 Connection closed [${code}]: ${reason}`);
});
ws.on("error", (err) => {
console.error("🚨 WebSocket error:", err.message);
});
// --- Audio sending ---
function sendAudio() {
const AUDIO_FILE = "./sample.webm"; // swap with your audio file path
if (!fs.existsSync(AUDIO_FILE)) {
console.warn("⚠️ No audio file found — sending silence simulation");
simulateAudioAndEnd();
return;
}
const audioBuffer = fs.readFileSync(AUDIO_FILE);
for (let i = 0; i < audioBuffer.length; i += CHUNK_SIZE) {
ws.send(audioBuffer.slice(i, i + CHUNK_SIZE));
}
// Signal end of audio stream
ws.send(JSON.stringify({ type: "end" }));
console.log("📤 Audio sent — end signal dispatched");
}
function simulateAudioAndEnd() {
// Send a short silence buffer then end the session
ws.send(Buffer.alloc(CHUNK_SIZE));
ws.send(JSON.stringify({ type: "end" }));
}
function isJson(buffer) {
try {
JSON.parse(buffer.toString());
return true;
} catch {
return false;
}
}
```
## 3. Define your Dictation Commands
Dictation is so much more than just simple speech to text. When implemented well, dictation allows for providers to execute fully hands free workflows. They can jump sections in the system they are working with, select specific text, delete text, etc.
### Define Supported Dictation Commands
Tying this back to the questions to consider, It's important to understand what system(s) you are building into and what/how they support:
1. First identify which systems you're integrating into - Is it a desktop application? A mobile app? Both?
2. Next, map out the commands you will be supporting - Read more tips/best practices [here](https://docs.corti.ai/stt/best-practices-commands)
3. If building into multiple platforms, identify how commands may need to vary based on system the provider is working from.
We have a whole page dedicated to setting up Dictation Commands [here](https://docs.corti.ai/stt/commands)
Additionally, read more about Dictation Command Best Practices [here](https://docs.corti.ai/stt/best-practices-commands)
For an example, here is a code snippet for a command to delete text. The command includes a defined list of words that can be recognized for the `delete_range` variable. Your application can define different delete actions for each of the options!
```javascript Command config: Delete text icon="square-js" expandable theme={null}
commands: [
{
id: "delete_range",
phrases: ["delete {delete_range}"],
variables: [
{
key: "delete_range",
type: "enum",
enum: ["everything", "the last word", "the last sentence", "that"]
}
]
}
]
```
```javascript Server response theme={null}
{
"type": "command",
"data": {
"id": "delete_range",
"variables": {
"delete_range": "that"
},
"rawTranscriptText": "Delete that.",
"start": 7.19,
"end": 8.01
}
}
```
## 4. Determine Your Punctuation Strategy
Most dictation tools on the market today have punctuation support baked into their solution. To make sure that we keep feature parity with legacy approaches to dictation, we offer a vast number of punctuation commands for providers to use (We also can use our models to formate/punctuate dictations instead!). We recommend consistency in your approach here. There are few things more frustrating than trying to dictate commands into a field that doesn't support it!
| Parameter | Description | Recommendation |
| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- |
| `spokenPunctuation` | Enable users to control when punctuation is inserted in the document output, having punctuation symbols and line breaks added to the document instead of transcribed text, as defined below. | Use for dictation and transcription workflows (not available for `/streams`) |
| `automaticPunctuation` | STT model automatically inserts limited punctuation (e.g., period, comma, question mark) based on context and dictation cadence. | Use for conversational transcript workflows (on by default for `/streams`) |
If supporting spoken punctuation, make sure you know what we support out of the box below:
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :---------------------------------------------------------------------------------------------------------------------------------- |
| Period | `.` | "period", "full stop", "dot" |
| Comma | `,` | "comma" |
| New line | `\n` | "new line", "next line" |
| New paragraph | `\n\n` | "new paragraph", "next paragraph" |
| Exclamation mark | `!` | "exclamation mark", "exclamation point" |
| Question mark | `?` | "question mark" |
| Colon | `:` | "colon" |
| Semicolon | `;` | "semicolon" |
| Hyphen | `-` | "hyphen", "dash" |
| Slash | `/` | "slash", "forward slash" |
| Quotation marks | `" "` | "open quote", "open quotation" "close quote", "close quotation" |
| Parentheses | `( )` | "open parenthesis", "open paren", "open bracket(s)" "close parenthesis", "closed paren", "close bracket(s)", "end bracket(s)" |
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :------------------------------------------------------------------------------------- |
| Period | `.` | "punktum", "punktom", "og punktum" |
| Comma | `,` | "komma" |
| New line | `\n` | "ny linje", "næste linje", "og næste linje", "og ny linje" |
| New paragraph | `\n\n` | "nyt afsnit", "ny afsnit", "og nyt afsnit", "og ny afsnit" |
| Exclamation mark | `!` | "udråbstegn" |
| Question mark | `?` | "spørgsmålstegn" |
| Colon | `:` | "kolon" |
| Semicolon | `;` | "semikolon" |
| Hyphen | `-` | "bindestreg" |
| Slash | `/` | "skråstreg" |
| Quotation marks | `" "` | "åbn anførselstegn", "anførselstegn" "luk anførselstegn", "anførselstegn slut" |
| Parentheses | `( )` | "parentes begynd", "parentes", "parentes start" "parentes slut", "parentes stop" |
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :--------------------------------------------------------------------------------------------------------------- |
| Period | `.` | "punt", "dot", "puntje", "eindpunt" |
| Comma | `,` | "komma" |
| New line | `\n` | "nieuwe regel", "volgende regel" |
| New paragraph | `\n\n` | "nieuwe alinea", "volgende alinea" |
| Exclamation mark | `!` | "uitroepteken" |
| Question mark | `?` | "vraagteken" |
| Colon | `:` | "dubbele punt" |
| Semicolon | `;` | "puntkomma" |
| Hyphen | `-` | "koppelteken", "streepje", "minteken" |
| Slash | `/` | "schuine streep", "slash" |
| Quotation marks | `" "` | "open aanhalingsteken", "open quote", "open citaat" "sluit aanhalingsteken", "sluit quote", "sluit citaat" |
| Parentheses | `( )` | "open haakje", "open haak", "open ronde haakje" "sluit haakje", "sluit haak", "sluit ronde haakje" |
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :------------------------------------------------------------------------------------------------ |
| Period | `.` | "point" |
| Comma | `,` | "virgule" |
| New line | `\n` | "à la ligne" |
| New paragraph | `\n\n` | "nouveau paragraphe", "paragraphe" |
| Exclamation mark | `!` | "point d'exclamation" |
| Question mark | `?` | "point d'interrogation" |
| Colon | `:` | "deux points" |
| Semicolon | `;` | "point virgule" |
| Hyphen | `-` | "tiret", "trait d'union" |
| Slash | `/` | "slash", "barre oblique" |
| Quotation marks | `" "` | "ouvrir les guillemets" "fermer les guillemets" |
| Parentheses | `( )` | "ouvrir la parenthèse", "parenthèse ouvrante" "fermer la parenthèse", "parenthèse fermante" |
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :-------------------------------------------------------------- |
| Period | `.` | "Punkt" |
| Comma | `,` | "Komma" |
| New line | `\n` | "neue Zeile" |
| New paragraph | `\n\n` | "neuer Absatz" |
| Exclamation mark | `!` | "Ausrufezeichen" |
| Question mark | `?` | "Fragezeichen" |
| Colon | `:` | "Doppelpunkt" |
| Semicolon | `;` | "Semikolon", "Strichpunkt" |
| Hyphen | `-` | "Bindestrich" |
| Slash | `/` | "Schrägstrich" |
| Quotation marks | `" "` | "Anführungszeichen öffnen" "Anführungszeichen schliessen" |
| Parentheses | `( )` | "Klammer auf" "Klammer zu" |
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :----------------------------------------------------------------------------------- |
| Period | `.` | "pont", "pontjel" |
| Comma | `,` | "vessző" |
| New line | `\n` | "új sor", "következő sor" |
| New paragraph | `\n\n` | "új bekezdés" |
| Exclamation mark | `!` | "felkiáltójel" |
| Question mark | `?` | "kérdőjel" |
| Colon | `:` | "kettőspont" |
| Semicolon | `;` | "pontosvessző" |
| Hyphen | `-` | "kötőjel", "mínusz jel" |
| Slash | `/` | "perjel", "törtvonal" |
| Quotation marks | `" "` | "idézőjel", "idézet nyitása" "idézőjel zár", "idézet zárása", "idézőjel bezár" |
| Parentheses | `( )` | "zárójel", "zárójel nyit", "nyitó zárójel" "zárójel zár", "zárójel bezár" |
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Period | `.` | "punktum", "prikk", "full stopp" |
| Comma | `,` | "komma" |
| New line | `\n` | "ny linje", "neste linje" |
| New paragraph | `\n\n` | "nytt avsnitt", "neste avsnitt" |
| Exclamation mark | `!` | "utropstegn", "utrop" |
| Question mark | `?` | "spørsmålstegn" |
| Colon | `:` | "kolon" |
| Semicolon | `;` | "semikolon" |
| Hyphen | `-` | "bindestrek", "strek", "dash" |
| Slash | `/` | "skråstrek", "slash", "fremoverskråstrek" |
| Quotation marks | `" "` | "åpne anførselstegn", "åpne sitat" "lukk anførselstegn", "lukk sitat" |
| Parentheses | `( )` | "åpne parentes", "åpne paren", "åpne bracket", "åpne parenteser" "lukk parentes", "lukk paren", "slutt parentes", "lukk parenteser", "slutt bracket" |
| Punctuation | | Spoken forms supported |
| :--------------- | :----: | :-------------------------------------------------------------------------------------------------------------------- |
| Period | `.` | "punkt" |
| Comma | `,` | "kommatecken", "komma" |
| New line | `\n` | "ny rad" |
| New paragraph | `\n\n` | "nytt stycke" |
| Exclamation mark | `!` | "utropstecken" |
| Question mark | `?` | "frågetecken" |
| Colon | `:` | "kolon" |
| Semicolon | `;` | "semikolon" |
| Hyphen | `-` | "bindestreck" |
| Slash | `/` | "snedstreck" |
| Quotation marks | `" "` | "citattecken", "öppna citattecken", "citationstecken", "citat", "start citat" "slut citat", "stäng citattecken" |
| Parentheses | `( )` | "start parentes", "öppna parentes" "slutparentes", "stäng parentes" |
Punctuation is key to making your dictations readable and easily actionable. We recommend sharing tip sheets with providers so they know the various punctuations available along with the spoken commands you choose to support.
## Tying It All Together
Between medical grade speech to text , the configurable spoken command support, and punctuation capabilities, there's a lot to piece together. But using the above steps and considerations, you should have a good idea of how to piece tha puzzle together in the best way for your organziation and your platform(s). As a refresher, in the above we discussed:
* Start with great audio - This means optimizing the stream as well as microphones.
* Design Time Saving Commands - Workflows like navigation and editing can save providers time (and clicks) if you design dictation to help.
* Add Punctuation Support - This helps your data to look clean and make it more easily actionable by other users.
# Automate Encounter Based Coding
Source: https://docs.corti.ai/get_started/encounter-coding
Automatically generate diagnosis codes and procedure codes from clinical encounter context.
An implementation handbook for product and engineering teams incorporating coding workflows into your solution using the Corti platform.
Modeled after structured use-case guides, this document is designed to help you move from concept → workflow → implementation → integration.
## Before Building on Corti
Before writing a single line of code, align on the fundamentals:
Start by defining which encounter types your solution will support. The encounter type will help define the needed output as well as guide you for what available context can be used for coding.
Common options include:
* Outpatient visits (most common starting point)
* Inpatient encounters
* Emergency department visits
* Telehealth consultations
Most teams begin with outpatient workflows due to their lower complexity and more standardized documentation.
Corti's Predict Codes endpoint is stateless and allows you to take control of what context you (and our users) provide for code prediction. Most organizations look to use one of the following:
Options include:
* Final clinical note (recommended) - Provides the most structured and complete context. It typically has multiple human in the loop points to ensure accuracy.
* Transcript (raw conversation) - Enables earlier coding but may introduce noise.
* Hybrid approach - Use transcript for early predictions and the final note for finalized coding.
You should also consider the workflow. Some common considerations:
* Should the provider explicitly select context (e.g. allow the provider to select a note)
* Should your solution automatically uses the latest available version of the note?
Corti's API allows for flexibility in when and how you code an encounter. Coding should be intuitive in your workflows. Common workflow triggers include:
* On Note Submission - Ensures the note is complete and reviewed before coding.
* On Demand - Give the provider/user the ability to trigger code prediction when they need to.
* At encounter completion - Automatically trigger coding once the session ends.
Help providers understand why a code was predicted. Not only does this help with provider trust, but this becomes an early guide to Clinical Documentation Integrity efforts.
Corti recommends using the evidences returned by the endpoint to surface the model rationale for a predicted code.
Plan your workflows and determine how clinicians and coders interact with the generated codes.
Common options include:
* Provider-in-the-loop - Clinicians review and adjust codes before submission.
* Coder review workflow- Codes are sent to professional coders for validation.
* Fully automated (with audit) - Codes are auto-submitted but monitored for compliance.
### Establish your Success Metrics
Accuracy in initial predictions of codes is a heavy indicator of the success of both the deployed model and the quality of the input to the model.
Measure:
Code accuracy rate - Percentage of codes accepted without changes
Coder override rate - How often human coders modify or replace suggested codes
Decreasing the time from the encounter date to a billed encounter allows for more efficient coding workflow
Measure:
Time from note completion → coded encounter
Reduction in manual coding time per encounter
Turnaround time for billing readiness
Providers wear a lot of hats and often coding is one of those hats. Helping them to code fast allows them to focus more time to be with patients.
Measure:
Time spent on coding per encounter
Number of manual edits per encounter
Documentation-to-coding workflow interruptions
Rejected claims or undercoded encounters cost a practice significant revenue. Ensuring coding accuracy helps to make sure providers are paid for the services they provide.
Measure:
Denial rate related to coding errors
Reduction in undercoding or missed codes
Change in average reimbursement per encounter
# The Corti API Basics
The interaction is the central hub for managing conversational sessions, letting you create and update interactions that drive clinical AI workflows.
Real-time, stateless speech-to-text over WebSocket designed to power fluid dictation experiences with reliable medical language recognition.
Extract and retrieve clinically relevant facts from interactions to enhance insight and decision support.
Predict diagnosis and procedure codes to increase support and accuracy of your coding program.
Live WebSocket interaction streaming that concurrently produces transcripts and clinical facts to support ambient documentation workflows.
Define reusable document structures that ensure clarity and consistency in generated outputs.
Create and manage AI-driven agents that automate contextual messaging and task workflows with experts registry support.
Upload and organize audio recordings tied to interactions to fuel downstream transcription and document generation.
Generate polished clinical documents from transcripts and templates for notes, summaries, or referrals.
Convert uploaded recordings into structured, usable text to support review and documentation.
## How to Implement Encounter Coding
### 1. Map Your Coding Workflows
Encounter coding is not just code generation. It is a clinical and revenue cycle workflow.
Before building, map the end-to-end experience:
#### Questions to Align On
* When should coding happen?
* On note submission?
* On demand by the provider?
* Automatically at encounter completion?
* What input context should be used for coding?
* Final clinical note?
* Transcript (raw conversation)?
* Something else?
* Who controls the input?
* Does the provider explicitly choose what gets coded (e.g., select a note)?
* Or does the system automatically use the latest available context?
* How should providers interact with suggested codes?
* How should model rationale be surfaced?
#### Visualize Your Core Workflows
To illustrate the concept with a hypothetical EHR, they may have made the following decisions for their design:
| Question | Answer | Justification |
| --------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| When should coding happen? | At Note Submission | In this example workflow, the note is generated using Corti's documents endpoint, edits are made, and the note is submitted. |
| What input context should be used for coding? | Encounter Note | We will use a fixed note template with the note content editable by the provider as context. |
| Who controls the input? | Fixed Note Template | To minimize variance across providers, we have a single note template used as context for codes. |
| How should providers interact with suggested codes? | Opt In | While not explicitly pictured in the workflow, the example workflow allows providers to select from suggested codes as well as search other codes. |
| How should model rationale be surfaced? | Presented to provider | In addition to showing suggested codes, my solution will show evidences behind the codes as well. |
For the purposes of the full workflow diagram, we're representing document generation using Corti's Document Generation capabilities.
## Determine Your Context Input
Corti’s coding endpoint supports two context input types: `text` and `documentId`.
This gives you flexibility in how you pass clinical context into the coding workflow. Some organizations prefer open, lightweight text-based input, while others rely on structured documents already created in Corti.
Before building, decide which model fits your workflow best.
### Code by Text
Use `text` when you want full control over what context is passed for code prediction.
This is typically the best fit when:
* You want to give providers flexibility to combine multiple pieces of clinical context into a single request
* You want to pass selected parts of a transcript, note, or supporting context
* You want to send a final note from your own system
Text input is often the most flexible option because it allows you to shape the exact context provided to the model. This is especially useful when your source data does not already exist as a Corti document, or when you want to further curate the content before prediction.
**Formatting text for open context passing**
You'll need to concatenate all desired context to pass into the Predict Codes endpoint.
```ts title="JavaScript" expandable theme={null}
// Multiple sources of clinical context
const finalNote = `
Assessment:
Acute otitis media of the right ear.
Plan:
Start amoxicillin for 7 days. Follow up if symptoms worsen.
`.trim();
const transcriptExcerpt = `
Patient reports right ear pain for 3 days, mild fever at home,
and decreased hearing on the right side.
`.trim();
const supportingContext = `
Encounter type: outpatient
Specialty: family medicine
`.trim();
// 1) Concatenate multiple contexts into a single text string
const combinedContext = [
"FINAL NOTE",
finalNote,
"TRANSCRIPT EXCERPT",
transcriptExcerpt,
"SUPPORTING CONTEXT",
supportingContext,
].join("\n\n");
// 2) Pass the combined text into Predict Codes
const response = await client.codes.predict({
system: ["icd10cm-outpatient", "cpt"],
context: [{ type: "text", text: combinedContext }],
});
```
```csharp title="C# .NET" expandable theme={null}
// Multiple sources of clinical context
var finalNote = """
Assessment:
Acute otitis media of the right ear.
Plan:
Start amoxicillin for 7 days. Follow up if symptoms worsen.
""".Trim();
var transcriptExcerpt = """
Patient reports right ear pain for 3 days, mild fever at home,
and decreased hearing on the right side.
""".Trim();
var supportingContext = """
Encounter type: outpatient
Specialty: family medicine
""".Trim();
// 1) Concatenate multiple contexts into a single text string
var combinedContext = string.Join(
"\n\n",
"FINAL NOTE",
finalNote,
"TRANSCRIPT EXCERPT",
transcriptExcerpt,
"SUPPORTING CONTEXT",
supportingContext
);
// 2) Pass the combined text into Predict Codes
var response = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmOutpatient, CommonCodingSystemEnum.Cpt],
Context =
[
new CommonTextContext
{
Type = new CommonTextContext.TypeLiteral(),
Text = combinedContext,
},
],
});
```
```python title="Python" expandable theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
TENANT = ""
TOKEN = ""
# Multiple sources of clinical context
final_note = """
Assessment:
Acute otitis media of the right ear.
Plan:
Start amoxicillin for 7 days. Follow up if symptoms worsen.
""".strip()
transcript_excerpt = """
Patient reports right ear pain for 3 days, mild fever at home,
and decreased hearing on the right side.
""".strip()
supporting_context = """
Encounter type: outpatient
Specialty: family medicine
""".strip()
# 1) Concatenate multiple contexts into a single text string
combined_context = "\n\n".join(
[
"FINAL NOTE",
final_note,
"TRANSCRIPT EXCERPT",
transcript_excerpt,
"SUPPORTING CONTEXT",
supporting_context,
]
)
# 2) Pass the combined text into Predict Codes
headers = {
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
}
payload = {
"system": ["icd10cm-outpatient", "cpt"],
"context": [{"type": "text", "text": combined_context}],
}
response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers=headers,
json=payload,
)
response.raise_for_status()
result = response.json()
print(result)
```
```bash title="cURL" expandable theme={null}
# Replace these with your values
ENVIRONMENT=""
TENANT=""
TOKEN=""
curl -X POST "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Tenant-Name: ${TENANT}" \
-H "Content-Type: application/json" \
-d '{
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "text",
"text": "FINAL NOTE\n\nAssessment:\nAcute otitis media of the right ear.\n\nPlan:\nStart amoxicillin for 7 days. Follow up if symptoms worsen.\n\nTRANSCRIPT EXCERPT\n\nPatient reports right ear pain for 3 days, mild fever at home, and decreased hearing on the right side.\n\nSUPPORTING CONTEXT\n\nEncounter type: outpatient\nSpecialty: family medicine"
}
]
}'
```
### Code By documentId
Use `documentId` when you want code prediction to be based on a document that already exists in Corti.
This is typically the best fit when:
* Your workflow already creates documents in Corti
* You want coding to run against the same document used elsewhere in the workflow
* You want a more controlled and traceable document-based process
This approach is a strong fit for workflows where note generation, document review, and coding all happen within the same Corti-powered experience.
**Creating a document to get the document ID**
Corti's text generation endpoints allow for flexibility in creating a document specific to your users needs. Because of the more custom feel of created documents, many organizations find it perfect to create a document both for clinical documentation as well as for coding purposes.
Below you'll find sample code for our example case of an organization generating an HPI note for coding.
```ts title="JavaScript" expandable theme={null}
// 1) Generate a document to get its id
const document = await client.documents.create(interactionId, {
templateKey: "corti-h-and-p",
outputLanguage: "en",
context: [
{
type: "string",
data: `Chief complaint: Chest pain.
History of present illness:
54-year-old patient with intermittent chest discomfort for 2 days,
worse with exertion, associated with mild shortness of breath.
Past medical history:
Hypertension, hyperlipidemia.
Medications:
Lisinopril, atorvastatin.
Allergies:
No known drug allergies.
Physical exam:
Patient appears comfortable. Heart regular rate and rhythm.
Lungs clear to auscultation.
Assessment:
Chest pain, rule out cardiac etiology.
Plan:
Obtain ECG, troponins, chest x-ray, and monitor closely.`,
},
],
});
// 2) Pass that documentId into Predict Codes
const codes = await client.codes.predict({
system: ["icd10cm-outpatient", "cpt"],
context: [{ type: "documentId", documentId: document.id }],
});
```
```csharp title="C# .NET" expandable theme={null}
// 1) Generate a document to get its id
var document = await client.Documents.CreateAsync(
interactionId,
new DocumentsCreateRequestWithTemplateKey
{
TemplateKey = "corti-h-and-p",
OutputLanguage = "en",
Context =
[
new DocumentsContextWithString
{
Type = DocumentsContextWithStringType.String,
Data =
"Chief complaint: Chest pain.\n\nHistory of present illness:\n54-year-old patient with intermittent chest discomfort for 2 days,\nworse with exertion, associated with mild shortness of breath.\n\nPast medical history:\nHypertension, hyperlipidemia.\n\nMedications:\nLisinopril, atorvastatin.\n\nAllergies:\nNo known drug allergies.\n\nPhysical exam:\nPatient appears comfortable. Heart regular rate and rhythm.\nLungs clear to auscultation.\n\nAssessment:\nChest pain, rule out cardiac etiology.\n\nPlan:\nObtain ECG, troponins, chest x-ray, and monitor closely.",
},
],
}
);
// 2) Pass that documentId into Predict Codes
var codes = await client.Codes.PredictAsync(new CodesGeneralPredictRequest
{
System = [CommonCodingSystemEnum.Icd10CmOutpatient, CommonCodingSystemEnum.Cpt],
Context =
[
new CommonDocumentIdContext
{
Type = CommonDocumentIdContextType.DocumentId,
DocumentId = document.Id,
},
],
});
```
```python title="Python" expandable theme={null}
import requests
# Replace these with your values
ENVIRONMENT = ""
INTERACTION_ID = ""
TENANT = ""
TOKEN = ""
headers = {
"Authorization": f"Bearer {TOKEN}",
"Tenant-Name": TENANT,
"Content-Type": "application/json",
}
document_payload = {
"templateKey": "corti-h-and-p",
"outputLanguage": "en",
"context": [
{
"type": "string",
"data": """
Chief complaint: Chest pain.
History of present illness:
54-year-old patient with intermittent chest discomfort for 2 days,
worse with exertion, associated with mild shortness of breath.
Past medical history:
Hypertension, hyperlipidemia.
Medications:
Lisinopril, atorvastatin.
Allergies:
No known drug allergies.
Physical exam:
Patient appears comfortable. Heart regular rate and rhythm.
Lungs clear to auscultation.
Assessment:
Chest pain, rule out cardiac etiology.
Plan:
Obtain ECG, troponins, chest x-ray, and monitor closely.
""".strip(),
}
],
}
# 1) Generate a document to get its id
document_response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/interactions/{INTERACTION_ID}/documents",
headers=headers,
json=document_payload,
)
document_response.raise_for_status()
document = document_response.json()
document_id = document["id"]
# 2) Pass that documentId into Predict Codes
coding_payload = {
"system": ["icd10cm-outpatient", "cpt"],
"context": [
{
"type": "documentId",
"documentId": document_id,
}
],
}
codes_response = requests.post(
f"https://api.{ENVIRONMENT}.corti.app/v2/tools/coding/",
headers=headers,
json=coding_payload,
)
codes_response.raise_for_status()
codes_result = codes_response.json()
print(codes_result)
```
```bash title="cURL" expandable theme={null}
# Replace these with your values
ENVIRONMENT=""
INTERACTION_ID=""
TENANT=""
TOKEN=""
# 1) Generate a document to get its id
DOCUMENT_ID=$(curl -s --request POST \
--url "https://api.${ENVIRONMENT}.corti.app/v2/interactions/${INTERACTION_ID}/documents" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Tenant-Name: ${TENANT}" \
--header "Content-Type: application/json" \
--data '{"templateKey": "corti-h-and-p", "outputLanguage": "en", "context": [{"type": "string", "data": "Chief complaint: Chest pain..."}]}' \
| jq -r '.id')
# 2) Pass that documentId into Predict Codes
curl --request POST \
--url "https://api.${ENVIRONMENT}.corti.app/v2/tools/coding/" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Tenant-Name: ${TENANT}" \
--header "Content-Type: application/json" \
--data "{\"system\": [\"icd10cm-outpatient\", \"cpt\"], \"context\": [{\"type\": \"documentId\", \"documentId\": \"${DOCUMENT_ID}\"}]}"
```
## Filter Codes and Code Groups
For some healthcare applications, having the full set of tens of thousands of ICD-10 codes to be able to search from is needed. For many healthcare applications, your users will only interact with a subset of codes within the broader code set. For example, an orthopedic specialty physician would likely not use K21.9 (Gastro-esophageal reflux disease (GERD) without esophagitis) in the diagnosis of their patients.
Symphony for Medical Coding supports a restricted code system mode where you pass a predefined list of codes and the model will only return predictions from within that set. Similarly, you can rule by omission and explicitly tell Symphony for Medical Coding to NOT use specific codes or categories.
Use the filter.include attribute to pass a string of codes/categories to exclusively search across these for your coding solution.
Use the filter.exclude attribute to pass a string of codes/categories to NOT search across these for your coding solution.
### Why Use Code Filtering?
There's a few quick reasons why organizations use these attributes when configuring their solution:
1. Increased Accuracy - Many organizations manage a list of specific codes that their end users use. This can easily be passed to Corti to ensure the codes that you receive from Corti is on the allowed list.
2. Provider Trust - Providers will adopt a solution more readily if they know the solution acts within the bounds of their practice.
The model still applies full clinical reasoning and coding rules within that scope, so you get the same accuracy and auditability as the full system, just bounded to the codes that matter for your use case. When your code set changes, there is no retraining required.
## Retrieve Codes
Once you’ve submitted a request to the coding endpoint, you can control how much information is returned based on your workflow needs (and based on what your users want to see).
Corti allows you to retrieve:
* Final codes only
* Codes with candidates (alternatives)
* Codes with or without evidences (rationale)
Before building, decide how much detail your users need and where it will be used.
### Codes vs Candidates
You can choose between returning only the finalized codes or including additional candidate suggestions.
* Codes only (recommended for production workflows)
* * Returns the most likely ICD-10 and CPT codes, optimized for downstream use (e.g., billing or EHR integration).
* Codes + candidates
* * Returns additional possible codes considered by the model.
* * Helpful for more complex visit types.
Consider the users that you are presenting codes to, who is associating diagnosis codes with procedure codes, and who needs to be actionable in your coding workflows.
### Evidences (Model Rationale)
You can also choose whether to retrieve evidences alongside predicted codes. Evidences link each predicted code to the parts of the input (text or document) that support it.
Corti recommends presenting evidences in provider coding workflows to help with code explainability and support clinical documentation improvement (CDI) efforts.
Choosing the Right Output
Your output configuration should align with your workflow. Corti recommends extracting the following based on your users and workflows:
| Workflow | Codes | Candidates | Evidences |
| --------------------------- | :--------------------------: | :--------------------------: | :--------------------------: |
| Provider Facing Workflows | | | |
| Coder Review Workflows | | | |
| Automated Billing Workflows | | | |
## Tying it All Together: Building Encounter Coding into your Solution
Corti’s coding capabilities give you a flexible foundation to embed intelligent, workflow-aware coding (remember, [we code like humans!](https://arxiv.org/abs/2509.05378)) directly into your product.
By aligning on workflows, context inputs, and output configurations, you can design a coding experience that fits naturally into your clinical and billing processes—whether that’s provider-assisted, coder-reviewed, or fully automated.
From here, you can:
* Choose how coding fits into your encounter lifecycle
* Define how context is passed (text or document-based)
* Configure outputs based on your users and workflows
Continue to the API reference and implementation guides to start integrating coding into your application.
# Create an account in the Corti Console
Source: https://docs.corti.ai/get_started/getaccess
Sign up, start your free trial, and set up your first project
The Corti Console is your self-service portal for signing up, managing projects, and generating the credentials your application uses to authenticate. This page gets you from sign-up to a ready-to-build project. From there, you'll create your first API client.
Create your account and start with a \$50 trial credit.
## Set up your account
Go to the [sign-up page](https://console.corti.app/signup) and register with your work email. Verify your email address, then sign in.
A project is your workspace: it holds your clients, usage, and team members. Create your first project to unlock a **\$50 trial credit** so you can start building right away.
When you create a project, you choose its **region** (EU or US) for data residency. All API clients created under the project inherit this region - you can't mix regions within a single project.
Use a separate project for each region, environment, or team you want to bill and monitor independently. Each project is locked to a single region at creation.
## What you can do in the Console
Your project is the home base for your integration. From here you can:
* **Create API clients**: generate the credentials your application uses to authenticate.
* **Test in the Studio playground**: experiment with endpoints and copy ready-made code samples in the browser.
* **Monitor token utilization**: track how much of your credit balance each project consumes.
* **Collaborate with your team**: invite teammates to share a project's clients and usage.
* **Manage billing**: view your credit balance and purchase additional token credits.
## Next step
With a project in place, create the client credentials your application needs to make its first authenticated call.
Generate your client credentials, then make your first call to the Corti API.
Managing users, tenants, or credentials programmatically? See the [Administration API](/about/admin-api).
For support or questions, please [contact us](mailto:help@corti.ai).
# Tracking credit consumption
Source: https://docs.corti.ai/get_started/tracking-credit-consumption
Attribute Corti API credit spend to your own customers, features, or workflows using an application-layer wrapper.
Every Corti endpoint that consumes credits reports how many it used in its response. Corti has no visibility into which of your customers, features, or workflows a request belongs to, so if you need to bill customers, attribute cost to a feature, or set per-customer budget alerts, capture that context yourself at the point you make each call. This page covers the pattern we recommend: an **application-layer wrapper** around every credit-consuming call, the same approach commonly used to attribute LLM API spend.
For how credits are priced per product, see [Corti pricing](https://www.corti.ai/pricing).
## How Corti reports usage
The field to read depends on how you call the endpoint:
| Product | How you call it | Where usage appears |
| ------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Text Generation, Medical Coding, fact extraction, batch transcripts | REST request/response | `usageInfo.creditsConsumed` in the response body |
| Speech to Text (real-time) | WebSocket (`/transcribe`, `/streams`) | `credits` field on `usage` and `delta_usage` messages |
| Corti Models | REST, OpenAI-compatible | `usage.prompt_tokens` / `completion_tokens` (or `input_tokens` / `output_tokens`): tokens, not credits. See [Computing cost for Corti Models](#computing-cost-for-corti-models) below |
| Corti Assistant (embedded) | Client-side event | `creditsConsumed` on the [`account.creditsConsumed`](/assistant/events/generated/account/creditsConsumed) event |
| Agentic Framework | REST (`message:send`) | `task.metadata.credits` in the response body |
`usageInfo.creditsConsumed` is the same shape on every REST endpoint that returns it. See the response documentation for [Guided Synthesis](/textgen/documents-guided-synthesis#response-shape) and [Medical Coding](/coding/how-it-works), or the schema on the [Extract Facts](/api-reference/facts/extract-facts) and [Create Transcript](/api-reference/transcripts/create-transcript) reference pages.
The Agentic Framework reports credits differently: `task.metadata.credits` on the `message:send` response, not a top-level `usageInfo` object. This shape isn't in the OpenAPI spec yet, but the field is present on every message response and safe to read.
For real-time streaming, `delta_usage` is an approximate running total sent after each `flush`; the final `usage` message sent after `end` is authoritative. See [Speech to Text usage messages](/api-reference/transcribe#usage) (the same shape applies to `/streams`).
## Computing cost for Corti Models
Corti Models follows the OpenAI-compatible spec, so it returns token counts, not a credits figure. Use [`@pydantic/genai-prices`](https://github.com/pydantic/genai-prices) ([JavaScript/TypeScript on npm](https://www.npmjs.com/package/@pydantic/genai-prices), [Python on PyPI](https://pypi.org/project/genai-prices/)) to compute the USD cost from the response's `usage` object and the [documented per-model rates](/models/models#pricing).
Corti's models aren't in the library's bundled catalog, so register them as a custom provider with the current rates. The library handles the per-million-token math and the cached-input discount (90% off the standard input rate) automatically — you just map the OpenAI-format usage fields to the library's `Usage` shape:
| OpenAI Chat Completions | genai-prices `Usage` |
| ---------------------------------------------- | -------------------- |
| `usage.prompt_tokens` (includes cached tokens) | `input_tokens` |
| `usage.completion_tokens` | `output_tokens` |
| `usage.prompt_tokens_details.cached_tokens` | `cache_read_tokens` |
For the **Responses API**, map `usage.input_tokens` / `usage.output_tokens` instead. For **embeddings**, pass `input_tokens` only — the library skips output cost when no output rate is set.
```ts title="JavaScript" expandable theme={null}
import OpenAI from "openai";
import { calcPrice, type Provider } from "@pydantic/genai-prices";
// Replace these with your values
const API_KEY = "";
const MODEL = "corti-s1-mini";
// Corti Models pricing in USD per million tokens. Corti's models aren't in the
// genai-prices catalog, so register them as a custom provider.
// See /models/models#pricing for current rates.
const cortiProvider: Provider = {
id: "corti",
name: "Corti Models",
api_pattern: "corti\\.app",
models: [
{ id: "corti-s1", match: { equals: "corti-s1" }, prices: { input_mtok: 2.0, cache_read_mtok: 0.2, output_mtok: 8.0 } },
{ id: "corti-s1-instant", match: { equals: "corti-s1-instant" }, prices: { input_mtok: 2.0, cache_read_mtok: 0.2, output_mtok: 8.0 } },
{ id: "corti-s1-mini", match: { equals: "corti-s1-mini" }, prices: { input_mtok: 1.0, cache_read_mtok: 0.1, output_mtok: 4.0 } },
{ id: "corti-s1-mini-instant", match: { equals: "corti-s1-mini-instant" }, prices: { input_mtok: 1.0, cache_read_mtok: 0.1, output_mtok: 4.0 } },
{ id: "corti-s1-embedding", match: { equals: "corti-s1-embedding" }, prices: { input_mtok: 0.03 } },
],
};
const client = new OpenAI({
baseURL: "https://ai.eu.corti.app/v1",
apiKey: API_KEY,
});
const response = await client.chat.completions.create({
model: MODEL,
messages: [{ role: "user", content: "Summarize this encounter." }],
});
// Map the OpenAI-format usage to genai-prices Usage. prompt_tokens includes
// cached tokens; cache_read_tokens is the cached subset priced at the discount.
const usage = {
input_tokens: response.usage?.prompt_tokens,
output_tokens: response.usage?.completion_tokens,
cache_read_tokens: response.usage?.prompt_tokens_details?.cached_tokens,
};
const result = calcPrice(usage, response.model, { provider: cortiProvider });
if (result) {
console.log(`$${result.total_price.toFixed(6)} USD`);
}
```
```python title="Python" expandable theme={null}
from decimal import Decimal
from openai import OpenAI
from genai_prices import Usage
from genai_prices.types import ModelPrice
# Replace these with your values
API_KEY = ""
MODEL = "corti-s1-mini"
# Corti Models pricing in USD per million tokens.
# See /models/models#pricing for current rates.
CORTI_MODELS_PRICING = {
"corti-s1": ModelPrice(input_mtok=Decimal("2.0"), cache_read_mtok=Decimal("0.2"), output_mtok=Decimal("8.0")),
"corti-s1-instant": ModelPrice(input_mtok=Decimal("2.0"), cache_read_mtok=Decimal("0.2"), output_mtok=Decimal("8.0")),
"corti-s1-mini": ModelPrice(input_mtok=Decimal("1.0"), cache_read_mtok=Decimal("0.1"), output_mtok=Decimal("4.0")),
"corti-s1-mini-instant": ModelPrice(input_mtok=Decimal("1.0"), cache_read_mtok=Decimal("0.1"), output_mtok=Decimal("4.0")),
"corti-s1-embedding": ModelPrice(input_mtok=Decimal("0.03")),
}
client = OpenAI(base_url="https://ai.eu.corti.app/v1", api_key=API_KEY)
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "Summarize this encounter."}],
)
# Map the OpenAI-format usage to genai-prices Usage. prompt_tokens includes
# cached tokens; cache_read_tokens is the cached subset priced at the discount.
details = response.usage.prompt_tokens_details
usage = Usage(
input_tokens=response.usage.prompt_tokens,
output_tokens=response.usage.completion_tokens,
cache_read_tokens=details.cached_tokens if details else None,
)
price = CORTI_MODELS_PRICING[response.model].calc_price(usage)
print(f"${price['total_price']} USD")
```
A future release of `GET /models` will return each model's per-token pricing directly, so you can look up rates dynamically instead of hardcoding them in the custom provider. For now, refer to the [documented rates](/models/models#pricing).
## The application-layer wrapper pattern
Treat every credit-consuming request as a billing event you tag, measure, and log yourself. Corti doesn't expose which customer or feature a past request belonged to after the fact, only an aggregate total is queryable retroactively, and only under the conditions described in [Limitations](#limitations) below. Capture attribution at request time instead of trying to reconstruct it later.
Rather than adding tracking code at every call site, wrap the calls once:
1. **Tag** the request with your own metadata: which customer or end-user it's for, and which feature or workflow triggered it.
2. **Call** the Corti endpoint as usual.
3. **Read** the credits or tokens consumed from the response (see the table above for which field).
4. **Log** one structured event per request to your own datastore: request ID, timestamp, feature, customer ID, credits consumed, latency, and status.
```ts title="JavaScript" expandable theme={null}
// Replace these with your values
const CUSTOMER_ID = "";
const FEATURE = "";
const INTERACTION_ID = "";
const TEMPLATE_ID = "";
async function trackCreditUsage(feature, customerId, requestFn) {
const requestId = crypto.randomUUID();
const startedAt = Date.now();
try {
const result = await requestFn();
// Write to your own store: a database row, a warehouse insert, or a structured log line.
await logUsageEvent({
requestId,
feature,
customerId,
creditsConsumed: result.usageInfo.creditsConsumed,
latencyMs: Date.now() - startedAt,
status: "ok",
});
return result;
} catch (error) {
await logUsageEvent({
requestId,
feature,
customerId,
creditsConsumed: 0,
latencyMs: Date.now() - startedAt,
status: "error",
errorCode: error.status ?? "unknown",
});
throw error;
}
}
const result = await trackCreditUsage(FEATURE, CUSTOMER_ID, () =>
client.documents.generate({
outputLanguage: "en-US",
interactionId: INTERACTION_ID,
templateRef: { templateId: TEMPLATE_ID },
})
);
```
```csharp title="C# .NET" expandable theme={null}
using Corti;
// Replace these with your values
const string CUSTOMER_ID = "";
const string FEATURE = "";
const string INTERACTION_ID = "";
const string TEMPLATE_ID = "";
async Task TrackCreditUsageAsync(
string feature,
string customerId,
Func> requestFn)
{
var requestId = Guid.NewGuid();
var startedAt = DateTimeOffset.UtcNow;
try
{
var result = await requestFn();
// Write to your own store: a database row, a warehouse insert, or a structured log line.
await LogUsageEventAsync(new
{
RequestId = requestId,
Feature = feature,
CustomerId = customerId,
CreditsConsumed = result.UsageInfo.CreditsConsumed,
LatencyMs = (DateTimeOffset.UtcNow - startedAt).TotalMilliseconds,
Status = "ok",
});
return result;
}
catch (Exception ex)
{
await LogUsageEventAsync(new
{
RequestId = requestId,
Feature = feature,
CustomerId = customerId,
CreditsConsumed = 0,
LatencyMs = (DateTimeOffset.UtcNow - startedAt).TotalMilliseconds,
Status = "error",
ErrorMessage = ex.Message,
});
throw;
}
}
var result = await TrackCreditUsageAsync(FEATURE, CUSTOMER_ID, () =>
client.Documents.GenerateAsync(
new GuidedDocumentsGenerateByTemplateRef
{
OutputLanguage = "en-US",
InteractionId = INTERACTION_ID,
TemplateRef = new GuidedTemplateRef { TemplateId = TEMPLATE_ID },
}));
```
Apply the same pattern to WebSocket streaming by logging from the `usage` message instead of a REST response:
```ts title="JavaScript" theme={null}
// Replace these with your values
const CUSTOMER_ID = "";
const FEATURE = "";
socket.on("message", (msg) => {
if (msg.type === "usage") {
logUsageEvent({
feature: FEATURE,
customerId: CUSTOMER_ID,
creditsConsumed: msg.credits,
status: "ok",
});
}
});
```
```csharp title="C# .NET" theme={null}
// Replace these with your values
const string CUSTOMER_ID = "";
const string FEATURE = "";
transcribe.TranscribeUsageMessage.Subscribe(message =>
{
LogUsageEvent(new
{
Feature = FEATURE,
CustomerId = CUSTOMER_ID,
CreditsConsumed = message.Credits,
Status = "ok",
});
});
```
### What to record per event
| Field | Why |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A request or correlation ID | Deduplicates retries so you don't double-count credits. Use your own generated ID, and also store the `interactionId` when the call is tied to an [interaction](/api-reference/interactions/create-interaction). |
| Timestamp | Time-series queries and monthly rollups. |
| Feature or workflow | The product surface that triggered the call (e.g. `encounter-summary`, `dictation-note`). Lets you attribute spend by feature, not just by customer. |
| Customer or end-user ID | Your own identifier, not Corti's. Required for per-customer billing or budget alerts. |
| Credits consumed (or tokens, for Corti Models) | The number you'll bill or alert on. |
| Status / error code | Failed requests can still consume partial credits; don't drop them from your log. |
Reject events with missing metadata instead of defaulting to "unknown". An unattributed cost event is harder to fix after the fact than a wrapper call site you forgot to update.
## Aggregate and alert
Once events land in your own store, attribution is a query, not an integration problem:
* Sum credits by customer ID and time window for chargeback or invoicing.
* Sum by feature to see which parts of your product are most expensive to run.
* Alert when a customer's rolling spend crosses a threshold you define.
## Corti Assistant: the event is the wrapper
If you're integrating Corti Assistant rather than calling the Corti API directly, you don't need to build a wrapper. The embedded widget already emits an [`account.creditsConsumed`](/assistant/events/generated/account/creditsConsumed) event after every stream, transcription, or document generation. Listen for it and log the same fields you would from a wrapper: `creditsConsumed`, `reason`, and `interactionId` are all in the public (non-confidential) payload, so you don't need to handle patient data to attribute cost.
```ts theme={null}
corti.addEventListener("account.creditsConsumed", (event) => {
const { creditsConsumed, reason, interactionId } = event.detail;
logUsageEvent({ creditsConsumed, reason, interactionId, customerId: currentCustomerId });
});
```
See [Corti Assistant events](/assistant/events) for the full event system and transport options (Web Component, postMessage, or window API).
## Limitations
* **Corti Models** doesn't return a credits figure directly; see [Computing cost for Corti Models](#computing-cost-for-corti-models) above.
* The [Admin API](/about/admin-api) exposes an aggregate consumption endpoint (total credits for one of your Embedded Assistant end-users over a time window), but it reports a total, not a per-request or per-feature breakdown. Treat it as a cross-check against your own totals, not a substitute for wrapper-level attribution.
# Welcome to the Corti API
Source: https://docs.corti.ai/get_started/welcome
The AI platform for industry developers
The Corti API brings AI into any application: speech to text, text generation, agentic workflows, medical coding, and an embeddable assistant. It's a single platform built for industry use, so you can add production-grade AI to your product without building the models, infrastructure, or compliance yourself.
## Core capabilities
| **Capability** | **What it does** | **Learn more** |
| :------------------------------------------- | :------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------- |
| [**Corti Models**](/models/welcome) | Access frontier LLMs hosted entirely on EU infrastructure through an OpenAI-compatible API. | [API reference](/api-reference/corti-models/chat/create-chat-completion) |
| [**Agentic Framework**](/agentic/overview) | Build AI agents and automate multi-step workflows. | [API reference](/agentic/agents/create-agent) |
| [**Speech to Text**](/stt/overview) | Transcribe speech in real time or from recordings, with industry-leading medical term accuracy. | [API reference](/api-reference/transcribe) |
| [**Text Generation**](/textgen/overview) | Generate structured documents and extract facts with controllable, reliable LLM output. | [API reference](/api-reference/documents-classic/generate-document) |
| [**Medical Coding**](/coding/overview) | Return structured medical codes with precision. | [API reference](/api-reference/codes/predict-codes) |
| [**Embedded Assistant**](/assistant/welcome) | Embed a production-ready ambient scribing experience that turns conversations into documentation. | [API reference](/assistant/api-reference) |
## Get started
[Create an account in the Corti Console](/get_started/getaccess) to get your API credentials and \$50 in free trial credits. From there, authenticate your first request and install an official SDK to make your first API call in minutes.
Sign up in the Corti Console and get your API credentials.
Set up OAuth and authorize your first requests.
Install an official SDK and make your first API call.
Use Claude Code, Cursor, or Codex with our SDKs — or connect your coding agent to Corti Models for EU-hosted LLMs.
## Official SDKs
The fastest way to integrate the Corti API is through our official SDKs. They handle authentication, token refresh, WebSocket connections, pagination, retries, and error handling, so you can focus on building your application.
Full-featured SDK for Node.js and browser environments. Supports all REST endpoints, real-time WebSocket streaming, and every authentication flow.
```bash theme={null}
npm install @corti/sdk
```
Production-ready SDK for .NET 8+, .NET Framework 4.6.2+, and .NET Standard 2.0. Full API coverage with async/await and WebSocket support.
```bash theme={null}
dotnet add package Corti.Sdk
```
TypeScript and Python SDK for building multi-agent systems on the Agentic Framework. Create agents, manage contexts, and compose workflows with typed connectors. Currently in private preview — [contact us](https://www.corti.ai/contact-us?products=agent-sdk,api\&use_case=I'd%20like%20access%20to%20the%20Agent%20SDK%20%28private%20preview%29.%20Please%20get%20in%20touch%20to%20discuss%20my%20use%20case.) for access.
## Corti Models
Corti Models gives you access to cutting-edge LLMs, embedding models, and more through an OpenAI API-compatible platform. Deploy on a sovereign EU cluster with no external dependencies, ensuring every prompt and workload stays within European infrastructure. Sovereign hosting is available, with GDPR compliance built in by default. [**Get started with Corti Models**](/models/welcome).
### More resources
| Resource | Description |
| :--------------------------------------------- | :----------------------------------------------------------------------------------- |
| [Release notes](/release-notes/overview/) | Upcoming changes and recent API, language model, and app updates. |
| [Support center ↗](mailto:help@corti.ai) | Help articles and documentation, contact the Corti team, and manage support tickets. |
| [Compliance & Trust ↗](https://trust.corti.ai) | Detailed compliance standards and security certifications. |
[Contact us](mailto:help@corti.ai) if you need more information about the Corti API.
# Get started coding with Corti Models
Source: https://docs.corti.ai/models/ai-coding-agent
Install an agent, run the wizard, code on EU-hosted models.
Connect your terminal AI coding agent to Corti's EU-hosted language models. Install a supported agent, run the Corti CLI, and start coding — every prompt is processed on Corti's EU infrastructure.
The Corti CLI writes a `corti` provider entry into each selected agent's
config and manages a `.env` block with your credentials. Each config
references `CORTI_BEARER` rather than embedding the secret itself.
## Before you start
You'll need:
* A terminal on macOS, Linux, or Windows
* [Node.js](https://nodejs.org) 18+ installed (required to run the Corti CLI via `npx`)
* A [Corti Console](https://console.corti.app) account (free to create)
The Corti CLI supports the following agents. Install one (or more) using your preferred method.
```bash Install theme={null}
curl -fsSL https://opencode.ai/install | bash
```
```bash Verify theme={null}
opencode --version
```
Learn more at [opencode.ai](https://opencode.ai).
Download the desktop UI for macOS, Linux, or Windows.
Get the installer at opencode.ai/download.
```bash Install theme={null}
curl -fsSL https://forgecode.dev/cli | sh
```
```bash Verify theme={null}
forge --version
```
Learn more at [forgecode.dev](https://forgecode.dev).
```bash Install theme={null}
curl -fsSL https://charm.land/install.sh | bash
```
```bash Verify theme={null}
crush --version
```
Learn more at [charm.land/crush](https://charm.land/).
```bash Install theme={null}
curl -fsSL https://pi.dev/install.sh | sh
```
```bash Verify theme={null}
pi --version
```
Learn more at [pi.dev](https://pi.dev).
Run the interactive setup to authenticate and configure the Corti provider for your installed agents:
```bash theme={null}
npx @corti/cli models init
```
`npx` requires [Node.js](https://nodejs.org) 18+. Download it from [nodejs.org](https://nodejs.org) or install via a version manager like [fnm](https://fnm.vercel.app) or [nvm](https://github.com/nvm-sh/nvm).
The wizard opens by asking how you want to sign in:
```text theme={null}
◆ How do you want to sign in?
│ ▸ ● Log in with a browser
│ ○ Paste an API key
│ ○ Enter client credentials
└ ↑↓ move · enter select
```
Press **Enter** to accept the default, **Log in with a browser**. The CLI shows a one-time code and opens the Corti Console. Approve the sign-in there, and the terminal continues on its own.
If the browser can't open automatically, for example over SSH, the CLI prints a URL for you to open instead.
1. In the Console, open [Corti Models](https://console.corti.app/corti-models) and generate your personal API key.
2. In the CLI, choose **Paste an API key** and paste it.
The value is masked on entry and stored as `CORTI_BEARER`.
Client credentials still work as well, mainly for headless and CI runs.
Once you're signed in, the wizard:
1. Verifies the connection and probes `/models` so you can pick a default model.
2. Detects installed agents and pre-selects them (toggle with Space).
3. Lets you choose scope: Global (`~/.config`), Project (current directory), or both.
4. Shows a plan and asks you to confirm before writing anything.
Press **Enter** to accept the defaults at each step.
The CLI writes a `corti` provider entry into each selected agent's config file, plus a managed `.env` block with your credentials:
* **ForgeCode** — `~/.forge/.forge.toml`
* **OpenCode** — `~/.config/opencode/opencode.json` (or `.jsonc`)
* **Crush** — `~/.config/crush/crush.json`
* **Pi** — `~/.pi/agent/models.json`
If you let the CLI update your shell startup file, open a new terminal and skip ahead.
Otherwise, load the `.env` manually. Global scope writes it to `~/.env`, project scope to `.env` where you ran the CLI.
```bash bash / zsh theme={null}
set -a; . ~/.env; set +a
```
```fish fish theme={null}
for l in (cat ~/.env); string match -q '*=*' -- $l; and set -gx (string split -m 1 = -- $l); end
```
```powershell PowerShell theme={null}
Get-Content "$HOME\.env" | ForEach-Object {
$line = $_.Trim()
if ($line -and -not $line.StartsWith('#')) {
$name, $value = $line -split '=', 2
Set-Item -Path "Env:$($name.Trim())" -Value $value.Trim().Trim('"').Trim("'")
}
}
```
```batch cmd theme={null}
for /f "usebackq eol=# tokens=1* delims==" %A in ("%USERPROFILE%\.env") do @set "%A=%B"
```
Launch your agent — you're now running an AI coding assistant backed by Corti Models.
```bash theme={null}
opencode
```
```bash theme={null}
forge
```
```bash theme={null}
crush
```
Select provider **corti** when prompted.
```bash theme={null}
pi
```
Select provider **corti** when prompted.
Ask the agent a question and confirm it answers using the Corti model you selected.
## Next steps
Prompt Claude Code, Cursor, Codex, or Lovable with a Corti build skill.
Browse the full Corti API with interactive examples.
SDK reference — the foundation the CLI and skills build on.
Multi-expert orchestration, MCP servers, and the agent runtime.
AI-generated code should always be reviewed and tested before use in
production. Human oversight ensures correctness, security, and compliance with
applicable regulations.
# Models
Source: https://docs.corti.ai/models/models
Compare Corti's text generation and embedding models by capability, reasoning, speed, and price — and pick the right one for your workload.
Corti Models exposes four text-generation variants that combine two dimensions — **model tier** and **reasoning mode** — so you can balance capability, speed, and cost for each use case. A dedicated embedding model is also available for vector generation.
| | **With reasoning** | **Instant (no reasoning)** |
| ------------- | ------------------ | -------------------------- |
| **Frontier** | `corti-s1` | `corti-s1-instant` |
| **Efficient** | `corti-s1-mini` | `corti-s1-mini-instant` |
Corti also offers access to OpenAI GPT models on request, at the same price as OpenAI, so you can mix and match models from a single provider.
## Reasoning vs. instant
Standard models produce a **chain-of-thought reasoning** trace before the final answer. The trace is visible in the `reasoning` field of chat-completion responses and as structured `type: "reasoning"` output items in the Responses API. The `-instant` variants skip this step and return only the answer, making them faster and cheaper per token.
Choose a reasoning model for complex multi-step problems, debugging, or when you need transparency into the model's thinking. Choose an `-instant` variant for straightforward generation, classification, or high-throughput workloads where speed and cost matter more than an auditable thought process.
## Image input
The `corti-s1-mini` and `corti-s1-mini-instant` models accept image inputs alongside text. Use the standard OpenAI multimodal content format: pass an array of content parts to the `messages` field, including `image_url` parts with base64-encoded data URIs.
Image input works well for:
* **OCR**: extracting text from screenshots and documents
* **Image descriptions**: generating alt text or summarizing visual content
* **UI analysis**: identifying layout, hierarchy, or accessibility issues in a screenshot
* **Simple design tasks**: suggesting layout, color, or typography improvements against a mockup
The total request payload is limited to approximately 50 KB. Compress larger images to JPEG before encoding. Images are tokenized as multimodal input tokens, billed at the standard input rate for the model in use.
Pass the image as a base64 data URI in the `messages` array:
```json theme={null}
{
"model": "corti-s1-mini",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image."},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,"}}
]
}]
}
```
For complete, runnable examples in JavaScript, Python, and cURL, see the [Quickstart](/models/quickstart).
## Embeddings
`corti-s1-embedding` is Corti's dedicated embedding model for converting text into high-dimensional vectors. Use it for semantic search, clustering, classification, and retrieval-augmented generation (RAG) pipelines.
## Pricing
Prices are per **million tokens**.
| Model | Input | Cached input | Output |
| ----------------------- | ------ | ------------ | ------ |
| `corti-s1` | \$2.00 | \$0.20 | \$8.00 |
| `corti-s1-instant` | \$2.00 | \$0.20 | \$8.00 |
| `corti-s1-mini` | \$1.00 | \$0.10 | \$4.00 |
| `corti-s1-mini-instant` | \$1.00 | \$0.10 | \$4.00 |
| `corti-s1-embedding` | \$0.03 | — | — |
**Cached input** applies when the same prompt prefix is reused across requests. Corti caches those tokens and charges them at a 90% discount, so long conversations and repeated system prompts cost significantly less.
## Next steps
Make your first API call with any OpenAI SDK.
Explore the Chat Completions and Responses endpoints.
# Quickstart
Source: https://docs.corti.ai/models/quickstart
Swap your existing OpenAI-compatible provider for Corti Models in two changes.
Already using the OpenAI SDK, Azure OpenAI, or any OpenAI-compatible provider? This guide shows you how to point that existing client at Corti Models, then make your first request. No new SDK, rewrite, or migration.
Looking to use Corti Models with your coding agent? Read [Code with Corti](/models/ai-coding-agent).
Corti Models is available only for Corti projects hosted in the **EU region** (`https://ai.eu.corti.app`). Credentials from a US-hosted project can't access Corti Models. See [Environments & Tenants](/authentication/environments_tenants).
## Get your API key
Grab a **Corti Models Service API Key** from the Corti Console. It's a single value you pass straight to the SDK as your API key.
Sign in, pick a client, then Copy as → Corti Models Service API Key.
## Swap your provider
If your code looks like this today:
```python Python theme={null}
from openai import OpenAI
client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
```
```javascript TypeScript theme={null}
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello!" }],
});
```
Change the base URL and API key, and you're running on Corti:
```python Python theme={null}
from openai import OpenAI
# corti-s1 | corti-s1-instant | corti-s1-mini | corti-s1-mini-instant
MODEL = "corti-s1"
client = OpenAI(
base_url="https://ai.eu.corti.app/v1",
api_key="",
)
response = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)
```
```javascript TypeScript theme={null}
import OpenAI from "openai";
// corti-s1 | corti-s1-instant | corti-s1-mini | corti-s1-mini-instant
const MODEL = "corti-s1";
const client = new OpenAI({
baseURL: "https://ai.eu.corti.app/v1",
apiKey: "",
});
const response = await client.chat.completions.create({
model: MODEL,
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);
```
```bash cURL theme={null}
API_KEY=""
# corti-s1 | corti-s1-instant | corti-s1-mini | corti-s1-mini-instant
MODEL="corti-s1"
curl https://ai.eu.corti.app/v1/chat/completions \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${MODEL}\",
\"messages\": [{\"role\": \"user\", \"content\": \"Hello!\"}]
}"
```
Everything else in your code — streaming, tool calling, JSON mode, multi-turn conversations, the Responses API — works unchanged. Corti Models is a drop-in replacement.
`corti-s1-mini` and `corti-s1-mini-instant` also accept image inputs for OCR, image descriptions, and UI analysis. See [Image input](/models/models#image-input) on the Models page, or the example below.
OAuth 2.0 access tokens will be supported soon. For now, use your Corti API key as the `apiKey` / `Bearer` credential.
## Send an image
`corti-s1-mini` and `corti-s1-mini-instant` accept image inputs via the standard OpenAI multimodal content format. Encode your image as a base64 data URI and pass it in the `messages` array:
```javascript JavaScript theme={null}
import OpenAI from "openai";
import { readFileSync } from "fs";
// corti-s1-mini | corti-s1-mini-instant
const MODEL = "corti-s1-mini";
// Replace with your values
const API_KEY = "";
const IMAGE_PATH = "";
const b64 = readFileSync(IMAGE_PATH).toString("base64");
const client = new OpenAI({
baseURL: "https://ai.eu.corti.app/v1",
apiKey: API_KEY,
});
const response = await client.chat.completions.create({
model: MODEL,
messages: [{
role: "user",
content: [
{ type: "text", text: "Describe this image." },
{ type: "image_url", image_url: { url: `data:image/jpeg;base64,${b64}` } },
],
}],
});
console.log(response.choices[0].message.content);
```
```python Python theme={null}
import base64
from openai import OpenAI
# corti-s1-mini | corti-s1-mini-instant
MODEL = "corti-s1-mini"
# Replace with your values
API_KEY = ""
IMAGE_PATH = ""
with open(IMAGE_PATH, "rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
client = OpenAI(
base_url="https://ai.eu.corti.app/v1",
api_key=API_KEY,
)
response = client.chat.completions.create(
model=MODEL,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image."},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
],
}],
)
print(response.choices[0].message.content)
```
```bash cURL theme={null}
# corti-s1-mini | corti-s1-mini-instant
MODEL="corti-s1-mini"
# Replace with your values
API_KEY=""
IMAGE_PATH=""
B64=$(base64 < "$IMAGE_PATH" | tr -d '\n')
curl https://ai.eu.corti.app/v1/chat/completions \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"${MODEL}\",
\"messages\": [{
\"role\": \"user\",
\"content\": [
{\"type\": \"text\", \"text\": \"Describe this image.\"},
{\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/jpeg;base64,${B64}\"}}
]
}]
}"
```
## See which models are available
List the models your credentials can access to confirm the model IDs you can pass:
```bash cURL theme={null}
API_KEY=""
curl https://ai.eu.corti.app/v1/models \
-H "Authorization: Bearer ${API_KEY}"
```
See the [Models](/models/models) page for the full lineup and pricing.
## Next steps
Compare the four model variants by capability, reasoning, speed, and price.
Connect OpenCode, ForgeCode, Crush, or Pi to Corti Models with the Corti CLI.
Browse the full Corti Models API with interactive examples.
How authentication works at Corti.
# Corti Models
Source: https://docs.corti.ai/models/welcome
Access cutting-edge LLMs, embedding models, and more through an OpenAI API-compatible platform. Use them in your coding agent or call them directly via API.
Much of today's AI infrastructure relies on US data centers, jurisdictions, and providers. For European organizations handling sensitive data, this can introduce compliance, control, and sovereignty risks.
Corti Models offers sovereign hosting and access to a default EU-based cluster with no external dependencies. Every prompt and workload is processed exclusively on European infrastructure operated by European companies, with GDPR compliance built in by default.
All inference and data processing runs on Corti-managed servers in Europe. Your data never leaves the EU.
The `corti-s1` model family benchmarks above leading proprietary alternatives. See the [model lineup](/models/models) for details.
No adequacy decisions, SCCs, or legal workarounds. Compliance is built in by default.
An OpenAI-compatible API and a first-class CLI drop straight into the coding agents your team already uses — no rewrites.
## Data stays in Europe
All inference, storage, and processing happens on Corti-managed European infrastructure. Your prompts and completions never cross borders, never touch US cloud providers, and are never used to train models. GDPR compliance doesn't require adequacy decisions, standard contractual clauses, or legal workarounds — it is the default.
## Frontier performance, EU-hosted
Choosing European infrastructure doesn't mean compromising on capability. The `corti-s1` model family — available in frontier and efficient tiers, each with or without reasoning — benchmarks above leading proprietary alternatives, so you get frontier quality on servers that never leave the EU. See the [Models](/models/models) page for the full comparison and pricing.
## Use them your way
There are two ways to build on Corti Models, and both are backed by the same EU-hosted models:
* **In your coding agent** — point OpenCode, ForgeCode, Crush, or Pi at Corti Models with the Corti CLI and start coding in minutes.
* **Directly via API** — the API is OpenAI-compatible, so your existing code works with minimal changes.
## Built for teams that care about compliance
If your industry requires data residency or strict privacy controls, Corti Models removes the compliance workarounds. It runs on the same platform trusted by large enterprises, with enterprise security and certifications inherited out of the box.
## Next steps
Compare the four model variants by capability, reasoning, speed, and price.
Make your first Corti Models API call with any OpenAI SDK.
Connect your terminal coding agent to Corti Models in a few minutes.
Browse the full Corti Models API with interactive examples.
# Quickstart - AI Coding Tools
Source: https://docs.corti.ai/quickstart/ai-coding-tools
Pick a use case, prompt your coding agent, ship.
Give your coding agent the context it needs to start building on Corti. Pick a use case, copy the prompt into Claude Code, Cursor, Codex, or Lovable, then paste credentials from the [Corti Console](https://console.corti.app).
Each prompt points your agent at a self-contained [Corti skill](https://docs.corti.ai/.well-known/agent-skills/index.json) — a single Markdown file with everything the agent needs to scaffold, wire auth, and run a working demo end-to-end.
This guide uses AI coding agents to **build applications on the Corti API**. If instead you want to **use Corti's EU-hosted LLMs as the backend for your coding agent** (OpenCode, ForgeCode, Crush, or Pi), see [Code with Corti Models](/models/ai-coding-agent).
```text Prompt theme={null}
Build a medical dictation web app with the Corti SDK.
1. Fetch your build skill:
https://docs.corti.ai/.well-known/agent-skills/corti-dictation/SKILL.md
2. Credentials are in the Corti Console:
https://console.corti.app → Developer Quickstart
```
```text Prompt theme={null}
Build an ambient scribe with the Corti SDK.
1. Fetch your build skill:
https://docs.corti.ai/.well-known/agent-skills/corti-ambient-scribe/SKILL.md
2. Credentials are in the Corti Console:
https://console.corti.app → Developer Quickstart
```
```text Prompt theme={null}
Build a medical coding app with the Corti SDK.
1. Fetch your build skill:
https://docs.corti.ai/.well-known/agent-skills/corti-medical-coding/SKILL.md
2. Credentials are in the Corti Console:
https://console.corti.app → Developer Quickstart
```
```text Prompt theme={null}
Build a chat assistant with the Corti Agentic Framework.
1. Fetch your build skill:
https://docs.corti.ai/.well-known/agent-skills/corti-agentic-assistant/SKILL.md
2. Credentials are in the Corti Console:
https://console.corti.app → Developer Quickstart
```
Deeplinks open Claude Code, Cursor, or Codex with the prompt pre-filled. If a deeplink doesn't launch (the app isn't installed, or your browser blocks custom schemes), copy the prompt above and paste it into your tool directly.
Your agent will ask you to add Corti credentials. Grab them from the Corti Console's Developer Quickstart — it has a one-click "Copy all as .env variables" action.
Sign in, then under Default client → Copy all as .env variables.
## Other languages or custom integrations
The four skills above cover the most common use cases against the official JavaScript SDK (`@corti/sdk`) and the Corti Agentic Framework. For Python, Go, Ruby, or anything that doesn't fit the four skills, point your agent at the machine-readable docs and let it build from there.
Concise API reference optimized for LLM context windows. Best for quick lookups and targeted code generation.
Complete documentation — guides, examples, and full specifications. Use when you need the whole picture.
Reference these URLs directly in your prompt (e.g. *"Using [https://docs.corti.ai/llms-full.txt](https://docs.corti.ai/llms-full.txt), generate a Python client that …"*), or add them to your project's `CLAUDE.md` / `AGENTS.md` / Cursor Docs source so every session has the context.
## Next steps
Browse the full Corti API with interactive examples.
SDK reference — what the skills build on under the hood.
Multi-expert orchestration, MCP servers, and the agent runtime.
Point OpenCode, ForgeCode, Crush, or Pi at Corti's EU-hosted LLMs with the Corti CLI.
Reference implementations for transcription, ambient, coding, and agents.
AI-generated code should always be reviewed and tested before use in production. Human oversight ensures correctness, security, and compliance with applicable regulations.
# Quickstart - Real-Time Stateless Dictation
Source: https://docs.corti.ai/quickstart/dictation
Walkthrough for building an dictation app using the `/transcribe` API
This guide shows how to authenticate with Corti and run your first real-time dictation session using the `/transcribe` WebSocket endpoint.
See details [here](/authentication/quickstart)
**Base URL**: `wss://api.$environment.corti.app/audio-bridge/v2/transcribe`
**Required query parameters:**
* `tenant-name`
* `token` (URL-encoded `Bearer `)
**Full URL template:**
```curl theme={null}
wss://api.$environment.corti.app/audio-bridge/v2/transcribe?tenant-name=$tenantName&token=Bearer%20$accessToken
```
**Example:**
```js JavaScript expandable theme={null}
import WebSocket from "ws";
const env = "eu"; // or "us"
const tenant = "";
const token = "";
const wsUrl =
`wss://api.${env}.corti.app/audio-bridge/v2/transcribe` +
`?tenant-name=${encodeURIComponent(tenant)}` +
`&token=${encodeURIComponent(`Bearer ${token}`)}`;
const ws = new WebSocket(wsUrl);
```
After the wss connection is opened, send a `config` message **within 10 seconds** or the server closes the socket with `CONFIG_TIMEOUT`.
**Example configuration message:**
```js expandable theme={null}
const configurationMessage = {
primaryLanguage: "en",
spokenPunctuation: true,
commands: [
{
id: "next_section",
phrases: ["next section", "go to next section"],
},
{
id: "insert_template",
phrases: [
"insert my {template_name} template",
"insert {template_name} template",
],
variables: [
{
key: "template_name",
type: "enum",
enum: ["soap", "radiology", "referral"],
},
],
},
],
formatting: {
dates: "long_text",
times: "h24",
numbers: "numerals_above_nine",
measurements: "abbreviated",
numericRanges: "numerals",
ordinals: "numerals",
},
};
```
Send the configuration as soon as the socket opens:
```js JavaScript theme={null}
ws.on("open", () => {
ws.send(JSON.stringify(configurationMessage));
});
```
Wait for a message with `{"type": "CONFIG_ACCEPTED"}` before sending audio. If you receive `CONFIG_DENIED` or `CONFIG_TIMEOUT`, close the socket and fix the configuration.
### Send audio frames
Send audio as **binary WebSocket messages**. See details on supported audio formats [here](/stt/audio).
```js theme={null}
// audioChunk: Buffer or Uint8Array containing raw audio
ws.send(audioChunk);
```
Send continuous stream of 250ms audio chunks while recording is active - no overlapping frames.
### Handle responses
The server sends messages with different `type` values, for example:
```json Transcript theme={null}
{
"type": "transcript",
"data": {
"text": "Patient reports mild chest pain.",
"rawTranscriptText": "patient reports mild chest pain period",
"start": 0.0,
"end": 3.2,
"isFinal": true
}
}
```
```json Command theme={null}
{
"type": "command",
"data": {
"id": "insert_template",
"variables": {
"template_name": "radiology"
},
"rawTranscriptText": "insert my radiology template",
"start": 2.3,
"end": 2.9
}
}
```
**Basic message handler:**
```js JavaScript expandable theme={null}
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
switch (msg.type) {
case "transcript":
console.log("Transcript:", msg.data.text);
break;
case "command":
console.log("Command:", msg.data.id, msg.data.variables);
break;
case "usage":
console.log("Usage credits:", msg.credits);
break;
case "error":
console.error("Error:", msg.error);
break;
default:
console.log("Other message:", msg);
}
});
```
Use `flush` to force pending transcript segments and/or dictation commands to be returned, without closing the session. This is useful to separate dictation into logical sections.
```js Request theme={null}
ws.send(JSON.stringify({ type: "flush" }));
```
```json Response theme={null}
{ "type": "flushed" }
```
Wait for `type: "flushed"` before treating the section as complete.
Send `end` when you are done sending audio:
```js theme={null}
ws.send(JSON.stringify({ type: "end" }));
```
The server then:
1. Emits any remaining `transcript` or `command` messages.
2. Sends usage info, for example:
```json theme={null}
{ "type": "usage", "credits": 0.1 }
```
3. Sends:
```json theme={null}
{ "type": "ended" }
```
4. Closes the WebSocket.
You can also close the client socket explicitly after receiving `ended`:
```js theme={null}
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.type === "ended") {
ws.close();
}
});
```
```js theme={null}
import WebSocket from "ws";
import fetch from "node-fetch";
const CLIENT_ID = "";
const CLIENT_SECRET = "";
const TENANT = "";
const ENV = "eu"; // or "us"
async function getAccessToken() {
const res = await fetch(
`https://auth.${ENV}.corti.app/realms/${TENANT}/protocol/openid-connect/token`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: "client_credentials",
scope: "openid",
}),
}
);
if (!res.ok) throw new Error(`Token error: ${res.status}`);
const json = await res.json();
return json.access_token;
}
async function run() {
const token = await getAccessToken();
const wsUrl =
`wss://api.${ENV}.corti.app/audio-bridge/v2/transcribe` +
`?tenant-name=${encodeURIComponent(TENANT)}` +
`&token=${encodeURIComponent(`Bearer ${token}`)}`;
const ws = new WebSocket(wsUrl);
ws.on("open", () => {
const config = {
primaryLanguage: "en",
spokenPunctuation: true,
automaticPunctuation: false,
commands: [
{
id: "next_section",
phrases: ["next section", "go to next section"],
},
],
formatting: {
dates: "long_text",
times: "h24",
numbers: "numerals_above_nine",
measurements: "abbreviated",
numericRanges: "numerals",
ordinals: "numerals",
},
};
ws.send(JSON.stringify(config));
});
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.type === "transcript") {
console.log("Transcript:", msg.data.text);
} else if (msg.type === "command") {
console.log("Command:", msg.data.id, msg.data.variables);
} else if (msg.type === "error") {
console.error("Error:", msg.error);
} else if (msg.type === "ended") {
ws.close();
}
});
ws.on("error", (err) => {
console.error("Socket error:", err);
});
// Example: close the session after 30 seconds if you are not streaming real audio
setTimeout(() => {
ws.send(JSON.stringify({ type: "end" }));
}, 30000);
}
run().catch((err) => {
console.error("Fatal error:", err);
});
```
# Quickstart - Transcription
Source: https://docs.corti.ai/quickstart/transcription
Speech to text from a pre-recorded audio file
## Introduction
The **Transcription Workflow** is defined by processing a complete audio file to return a text document. In scenarios where real-time speech-to-text is not required or feasible, the transcription workflow provides functional and cost effective means for creating verbatim, conversational or dictation-style transcripts.
### Endpoints and capabilities
| Endpoint | Capability | Use |
| :----------- | :------------------------------------------------------------------------------------------------------ | :------- |
| Interactions | The foundational unit that ties together all related data and operations, enabling a cohesive workflow. | Required |
| Recordings | Upload audio file(s) that can be used for transcript generation. | Required |
| Transcripts | Generate transcripts for audio files that are associated with the interaction. | Required |
See audio format requirements [here](/stt/audio)
***
## Workflow
1. The workflow begins with the client initiating an interaction by sending a `POST` request to the `/interactions` endpoint.
2. The API responds with a unique `interactionId` for the interaction and a WebSocket URL (`websocketUrl`). The identifier will be used to manage the subsequent steps of the workflow. The WebSocket URL will not be required for this workflow.
3. Once the interaction is initialized, the client uploads an audio file associated with that interaction by sending a `POST` request to `/interactions/:id/recordings`.
4. The API responds with a `201` status and returns a `recordingId`, confirming that the audio file has been successfully uploaded and linked to the interaction.
5. After the recording is uploaded, the client initiates the transcription process by sending a `POST` request to `/interactions/:id/transcripts`.
6. The API processes the audio and returns a `201` status with the generated transcript. This transcript contains the text version of the recorded interaction, extracted and formatted for review.
```mermaid theme={null}
sequenceDiagram
Client ->> Public API: POST /interactions
Public API -->> Client: 201, interactionId, websocketUrl
Client ->> Public API: POST /interactions/:id/recordings
Public API -->> Client: 201, recordingId
Client ->> Public API: POST /interactions/:id/transcripts
Public API -->> Client: 201, transcript
```
See details on transcription configuration options [here](/api-reference/transcripts/create-transcript)
# Agentic Framework
Source: https://docs.corti.ai/release-notes/agentic
Updates and improvements to Corti Agentic Framework
Detailed documentation about the Agentic Framework is available [here](/agentic/overview).
#### Feedback API
Collect ratings, labels, and reasons on task results. Build thumbs-up/down UIs, case-review workflows, and automated evaluation pipelines directly on agent tasks.
* `POST /v2/agentic/contexts/{contextId}/tasks/{taskId}/feedback` — submit a binary rating, up to five labels (`correct`, `helpful`, `incorrect`, `missingInformation`, etc.), and an optional reason
* `GET /v2/agentic/contexts/{contextId}/tasks/{taskId}/feedback` — list the caller's feedback for a task, newest first
* `DELETE /v2/agentic/contexts/{contextId}/tasks/{taskId}/feedback` — remove all feedback the caller submitted (idempotent)
Target a specific message with `target.messageId`. Track provenance with metadata fields for collection method, client reference, and a pseudonymous actor ID.
Read the guide: [Submit feedback on tasks](/agentic/guides/submit-feedback).
#### OpenInference trace export
Export execution traces for any context in the [OpenInference](https://github.com/Arize-ai/openinference) format. See which connectors were called, how many tokens were consumed, what inputs were sent, and how long each step took.
* `GET /v2/agentic/contexts/{contextId}/trace` — returns traces and their spans, newest first, with pagination via `pageSize` and `pageToken`
Feed traces into any OpenInference-compatible observability platform to debug agent behavior, audit clinical workflows, and optimize connector usage.
Read the guide: [Export OpenInference traces](/agentic/guides/export-traces).
#### Schema connectors GA
Define a custom tool with a JSON Schema. No server, no MCP, no remote agent — just a schema on the agent.
The LLM uses the schema's `name` and `description` to decide when to call it. Set `transition: "complete"` to stop the agent loop after the tool fires — perfect for structured-output agents that must return a coded diagnosis, a confidence score, or a filled-in form.
```json theme={null}
{
"type": "schema",
"name": "submit_code",
"description": "Submit the final ICD-10 code with a confidence score.",
"transition": "complete",
"schema": {
"type": "object",
"properties": {
"code": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 1 }
},
"required": ["code"]
}
}
```
See [Connectors](/agentic/connectors#schema-connectors).
#### A2A runtime reliability
Three server-side improvements, no client changes required:
* **Stuck tasks auto-killed** — tasks that go silent are terminated automatically, freeing resources and unblocking workflows
* **Burst traffic handled** — a configurable concurrency cap per agent prevents resource exhaustion during traffic spikes
* **Automatic memory cleanup** — completed and failed executions are cleaned up automatically
#### MCP connector freshness
* **Idle connection cleanup** — MCP connectors not used recently drop from the connection pool automatically, keeping resource usage low for agents with many MCP connectors
* **Automatic config updates** — when a registry connector's config schema changes, all agents referencing it re-resolve with the new defaults. Per-agent overrides are preserved.
#### Artifacts in multi-turn history
Agents carry their own previous results into the next turn. A coding suggestion, extracted data structure, or calculated score from an earlier turn is included in the conversation history automatically — the client doesn't re-send it.
Agent-produced data is tagged distinctly from user-supplied data, so the agent always knows what it produced versus what it was given. No client changes required.
#### X-Request-ID on all responses
Every response includes an `X-Request-ID` header that ties your client-side request to server-side logs and traces. Set on all v2 endpoints. Use it when reporting issues to Corti support.
#### Five connector types
The unified connector model now spans five types — every way to extend an agent, from pre-built clinical tools to custom JSON Schema tools:
| Type | What it gives your agent |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registry` | Pre-built connectors from the Corti registry — coding, drug lookup, PubMed, clinical trials, web search, medical calculator, interviewing, memory |
| `mcp` | Any MCP server you operate — provide a URL and optional auth |
| `agent` | Another Corti agent in your tenant — multi-agent composition |
| `a2a` | Any remote A2A agent discovered by endpoint URL |
| `schema` | A custom tool defined by a JSON Schema — no server needed |
See [Connectors](/agentic/connectors).
#### Agentic Framework v2.0.0
The v2 API is generally available. New endpoint families, unified connectors, A2A v1.0, and first-class resources for contexts, connectors, usage, feedback, and traces. All endpoints live under `/v2/agentic/`.
The v1 API remains available and deprecated. See the [v1-to-v2 migration guide](/agentic/guides/migrate-v1-to-v2).
##### Connectors
v1 had three concepts: Experts, MCP servers, and sub-agents. v2 unifies them into one [connectors](/agentic/connectors) model and adds two new types:
| Type | What it does |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `registry` | Pre-built connectors from the Corti registry (coding, drug lookup, PubMed, clinical trials, web search, medical calculator, interviewing, memory) |
| `mcp` | A remote MCP server you operate yourself |
| `agent` | Another Corti agent in the same tenant — multi-agent composition |
| `a2a` *(new)* | A remote A2A agent discovered by endpoint URL |
| `schema` *(new)* | A custom tool defined by a JSON Schema |
Manage connectors via the `connectors` array on agent create/patch, or via dedicated sub-resource endpoints for individual attach/update/remove.
##### A2A v1.0
The [A2A protocol](/agentic/a2a-protocol) is upgraded to v1.0. Two protocol bindings at the same base URL — pick whichever fits your stack:
* **HTTP+JSON** — REST endpoints, simplest for most use cases
* **JSON-RPC** — a single endpoint for clients that prefer JSON-RPC envelopes
| Operation | HTTP+JSON | JSON-RPC method |
| ------------------- | ------------------------------------ | ---------------------- |
| Send a message | `POST /a2a/message:send` | `SendMessage` |
| Stream a message | `POST /a2a/message:stream` | `SendStreamingMessage` |
| List tasks | `GET /a2a/tasks` | `ListTasks` |
| Get a task | `GET /a2a/tasks/{taskId}` | `GetTask` |
| Cancel a task | `POST /a2a/tasks/{taskId}:cancel` | `CancelTask` |
| Subscribe to a task | `POST /a2a/tasks/{taskId}:subscribe` | `SubscribeToTask` |
Connect any third-party A2A agent. The runtime reads the remote agent's card and negotiates the wire dialect — native v1.0, or the legacy 0.x dialect for agents that haven't upgraded. No adapters needed.
##### Endpoint map
Everything under `/v2/agentic/`:
**Agents**
| Method | Path |
| ------ | ----------------------------------------------- |
| POST | `/agents` |
| GET | `/agents` |
| GET | `/agents/{agentId}` |
| PATCH | `/agents/{agentId}` |
| DELETE | `/agents/{agentId}` |
| GET | `/agents/{agentId}/.well-known/agent-card.json` |
| GET | `/agents/{agentId}/usage` |
**Connectors**
| Method | Path |
| ------ | -------------------------------------------- |
| GET | `/agents/{agentId}/connectors` |
| POST | `/agents/{agentId}/connectors` |
| GET | `/agents/{agentId}/connectors/{connectorId}` |
| PATCH | `/agents/{agentId}/connectors/{connectorId}` |
| DELETE | `/agents/{agentId}/connectors/{connectorId}` |
**Messaging and tasks**
| Method | Path |
| ------ | ------------------------------------------------ |
| POST | `/agents/{agentId}/a2a/message:send` |
| POST | `/agents/{agentId}/a2a/message:stream` |
| GET | `/agents/{agentId}/a2a/tasks` |
| GET | `/agents/{agentId}/a2a/tasks/{taskId}` |
| POST | `/agents/{agentId}/a2a/tasks/{taskId}:cancel` |
| POST | `/agents/{agentId}/a2a/tasks/{taskId}:subscribe` |
| POST | `/agents/{agentId}/a2a` |
**Contexts**
| Method | Path |
| ------ | ------------------------------------------------------------- |
| GET | `/contexts` |
| GET | `/contexts/{contextId}` |
| DELETE | `/contexts/{contextId}` |
| GET | `/contexts/{contextId}/tasks` |
| GET | `/contexts/{contextId}/tasks/{taskId}` |
| GET | `/contexts/{contextId}/tasks/{taskId}/artifacts/{artifactId}` |
| GET | `/contexts/{contextId}/trace` |
**Feedback**
| Method | Path |
| ------ | ----------------------------------------------- |
| POST | `/contexts/{contextId}/tasks/{taskId}/feedback` |
| GET | `/contexts/{contextId}/tasks/{taskId}/feedback` |
| DELETE | `/contexts/{contextId}/tasks/{taskId}/feedback` |
**Registry**
| Method | Path |
| ------ | ------------------------------------ |
| GET | `/registry/connectors` |
| GET | `/registry/connectors/{connectorId}` |
##### Agent metadata
| Field | What it controls |
| ------------ | ------------------------------------------ |
| `visibility` | `private`, `unlisted`, `public` |
| `lifecycle` | `ephemeral`, `persistent` |
| `model` | Which model the agent uses |
| `labels` | Free-form key-value metadata for filtering |
##### Prefixed IDs
All resource IDs use type-prefixed UUIDv7 — tell what a resource is from the ID alone:
| Prefix | Resource |
| ------- | --------- |
| `agt.` | Agent |
| `con.` | Connector |
| `ctx.` | Context |
| `task.` | Task |
| `msg.` | Message |
| `art.` | Artifact |
| `fb.` | Feedback |
##### Streaming
Two SSE endpoints for real-time updates:
* `message:stream` — send a message and watch the response unfold
* `tasks:subscribe` — subscribe to updates for an existing task
See [Stream agent responses](/agentic/guides/stream-responses).
##### Credit pre-flight check
Tasks are checked for sufficient credits before any tokens are consumed. Insufficient balance = rejected task, zero spend. Per-task usage (`$usage`, `credits`) is in the task metadata. Agent-level metrics at `GET /agents/{agentId}/usage`. See the [usage guide](/agentic/guides/view-usage).
##### JSON Merge Patch
`PATCH` uses JSON Merge Patch (RFC 7386). Omit a field to leave it unchanged; send `null` to clear it. `Content-Type: application/merge-patch+json`.
##### Errors
A2A `google.rpc.Status` format with structured details for programmatic handling.
##### v1 to v2 changes
| v1 | v2 |
| ------------------------------------------------ | ----------------------------------------------------------------- |
| `experts` array | `connectors` with `type: "registry"` |
| `?ephemeral=true` | `lifecycle: "ephemeral"` in body |
| `agentType` (expert, orchestrator, interviewing) | Removed — specialization comes from connectors and system prompts |
| `kind: "text"` / `kind: "data"` on parts | Removed — use `text`, `file`, `data` properties |
| `role: "user"` | `role: "ROLE_USER"` |
| `submitted`, `working` | `TASK_STATE_SUBMITTED`, `TASK_STATE_WORKING` |
| Inline expert creation | Removed — use `registry` connectors |
Full details: [v1-to-v2 migration guide](/agentic/guides/migrate-v1-to-v2).
#### Part provenance
Every data part in a conversation carries an origin tag the agent can see:
* `user_text_NN` / `user_data_NN` — what you sent
* `tool_data_NN` — what a connector returned
* `agent_data_NN` — what the agent produced in a previous turn
This stops the agent from echoing your input as its own answer and keeps multi-turn conversations accurate. No client changes required.
#### Built-in data inspection tools
Two tools the agent uses automatically when the conversation contains data or text parts. No configuration needed.
##### `query_data_parts` — jq over structured data
Run [jq](https://jqlang.github.io/jq/manual/) programs over data parts. Navigate, filter, search, and join across parts — the agent pulls exactly the fields it needs without loading the whole payload into the prompt.
* **Navigate**: first five lab results — `.user_data_01.results[0:5]`
* **Filter**: flagged results — `.user_data_01.results[] | select(.flag == "H")`
* **Search**: where "metformin" appears in the structure
* **Join**: match patients to labs across two parts — `.user_data_01.patients[] as $p | .user_data_03.labs[] | select(.mrn == $p.mrn)`
| Parameter | Type | Description |
| --------- | ----------- | ---------------------------------------------------------------- |
| `ids` | `list[str]` | Data part GIDs to load (e.g. `["user_data_01", "user_data_03"]`) |
| `program` | `str` | jq program; start with the part id (e.g. `.user_data_01`) |
Offered automatically when the conversation has at least one data part.
##### `read_part` — read and search text
Read a window of text or search for terms in long content — encounter transcripts, referral letters, clinical notes.
* **Windowed read**: read 5,000 characters from any offset, with markers showing what's hidden before and after
* **Search**: find every occurrence of a term or regex pattern, with 200 characters of context per match
| Parameter | Type | Description |
| --------- | ------ | -------------------------------------------------------------------------------------------------------- |
| `id` | `str` | Part to read — text part (`user_text_02`) or data part (`user_data_01`) |
| `field` | `str` | Dot path to a string field inside a data part (e.g. `notes`, `results.0.text`). Required for data parts. |
| `offset` | `int` | Start position in characters |
| `length` | `int` | Characters to return (default 5,000) |
| `query` | `str` | Search instead of read — returns matches with context |
| `regex` | `bool` | Treat `query` as regex |
Offered automatically when the conversation has at least one text or data part.
##### Why this matters
Agents work with real clinical data sizes. Scan a 50-page transcript for every mention of chest pain. Cross-reference a full lab panel against a medication list. Produce a summary without hallucinating details that didn't fit. The tools reference parts by GID, so the agent always knows whether it's inspecting your data, a connector's output, or its own prior turn.
#### Shared agents across contexts
Use the same agent across multiple conversations, sessions, and patients. No more creating duplicate agents for each context. No client changes required.
#### MCP 2025-11-25 spec compliance
MCP connectors follow the Model Context Protocol 2025-11-25 specification, including the latest tool-registration and `list_changed` refresh semantics. MCP servers you operate — open-source or in-house — work with the framework.
#### Credit pre-flight and usage metering
Tasks are checked for sufficient credits before processing. Insufficient balance = rejected task, no tokens consumed. Per-task usage in task metadata:
* `$usage` — input and output tokens
* `credits` — credit balance and spend
Agent-level metrics at `GET /v2/agentic/agents/{agentId}/usage` with daily buckets. See the [usage guide](/agentic/guides/view-usage).
# Changes Policy
Source: https://docs.corti.ai/release-notes/change-policy
Learn how Corti approaches changes to the API
## Overview
As an infrastructure provider to some of the leading healthcare software providers, Corti is as focused on maintaining long-term stability as on delivering rapid improvements and innovative functionalities.
The following guidelines serve to communicate how we approach changes, what we consider breaking changes and how we communicate those.
We will list deprecations and upcoming breaking changes on [Upcoming Changes](/release-notes/changelog-upcoming)
## Response and Schema Changes
When a change to a response or API schema is **additive**, i.e. adding new optional fields to a response or a new optional request property, we do **not** consider this a breaking change.
When integrating with the Corti API, your API client is thus required to ignore fields that the client does not recognize and to not have strict typing checks for API responses.
### Breaking changes
* The change removes an existing response field.
* The change modifies the structure of the existing responses.
* The change adds a new required request property.
* The change turns an optional into a required property.
## Parameter Changes
When a change is **adding an optional** parameter, we do **not** consider this a breaking change.
### Breaking changes
* The change turns an optional parameter into a required parameter.
* The change adds a required parameter.
* The change alters the type or format of the parameter.
## Endpoint and Path Changes
## Communication of Changes
For non-breaking changes, we do not pro-actively communicate changes but will at irregular intervals post key changes in the Release Notes and Change Log.
For breaking changes, we will do three things:
Communicate the upcoming change in response headers for the affected endpointThe header will reference a URL where we communicate the change here on docs.corti.aiA message will be shown when signed into [Corti Console](https://console.corti.app/)
When relevant, we will temporarily accept or return both old and new properties until deprecating the old one.
# Upcoming Changes
Source: https://docs.corti.ai/release-notes/changelog-upcoming
Learn about deprecations and upcoming breaking changes
## Overview
As detailed in our [Changes Policy](/release-notes/change-policy), we from time to time are forced to introduce breaking changes as we evolve our API.
Similarly, we might once in a while deprecate existing functionalities when newer functionalities serve a similar purpose in a better way.
This page lists all upcoming deprecations and their potential breaking change impact details.
The update date on this page indicates the release date where we announce the deprecation. It is immediately in effect, while the shutdown (sunset) date is the date where only the new API behaviour or model is available anymore.
### Change to punctuation parameters for transcripts endpoint
| Property | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Change | The `/transcripts` configuration parameter `isDictation` is being deprecated in favor of `spokenPunctuation` and `automaticPunctuation` to match the equivalent parameter on the `/transcribe` endpoint. |
| Affected endpoints | [POST/transcripts](/api-reference/transcripts/create-transcript#body-is-dictation) |
| Impact | `isDictation` is replaced by `spokenPunctuation` and `automaticPunctuation`. It is ignored when either of those fields are provided. When `isDictation` is defined as `true` and neither new field is provided, it is treated as `spokenPunctuation: true` (automatic punctuation off). |
| Shutdown date | **Undefined**: No removal date is currently planned. |
### Change to diarization parameter for streams endpoint
| Property | Description |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Change | The `/streams` configuration parameter `isDiarization` is being renamed `diarize` to match the equivalent parameter on the `/transcripts` endpoint. |
| Affected endpoints | [wss/streams](/api-reference/streams#param-diarize) |
| Impact | Both parameter names will be accepted: diarize (canonical) takes precedence over the deprecated isDiarization alias when both are supplied. |
| Shutdown date | **Undefined**: both parameters will be supported indefinitely. |
| Related Guide | A deprecation warning will be logged when a client supplies only the legacy isDiarization field. CONFIG\_ACCEPTED echoes both diarize and isDiarization so existing clients reading the response keep working; isDiarization will be dropped from the echo once the parameter is fully deprecated. |
### Formatting Change for Medical Codes
| Property | Description |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Change | To align with standard formatting, Corti will update the API response of medical codes to include decimal points. |
| Affected endpoints | [Predict Codes](/api-reference/codes/predict-codes) |
| Impact | Medical Codes generated by the API will include decimal points. Client side formatting to add decimal points will no longer be needed. Example: A code currently returned as M751 will be returned as M75.1 |
| Change date | **May 22nd, 2026** |
### Property name changes for GET template(s), GET sections
| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Change | To align with our API conventions, we are changing from snake\_case to camelCase property naming. Some properties are renamed: `date_updated` -> `updatedAt`, `section_type` -> `type`, `sections_id` -> `section` |
| Affected endpoints | [List templates](/api-reference/templates-classic/list-templates), [List template sections](/api-reference/templates-classic/list-template-sections), [Get template](/api-reference/templates-classic/get-template) |
| Impact | Both snake\_case and camelCase or slightly renamed properties are being returned until the shutdown date. |
| Shutdown date | **January 6th, 2026**: We will not return snake\_case properties anymore, please ensure you have adopted any necessary changes before that date. |
| Related Guide | [Retrieving available templates](/textgen/templates#retrieving-available-templates) |
### Property name change for additionalInstructions
| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Change | As we evolve the documents endpoint, we are renaming the `additionalInstructions` property to `additionalInstructionsOverride` for clarity that this field overrides any existing default. |
| Affected endpoints | [POST documents from dynamic template](/api-reference/documents-classic/generate-document#dynamic-template) |
| Impact | Until shutdown, both the now deprecated `additionalInstructions` as well as the new `additionalInstructionsOverride` are accepted in the request. |
| Shutdown date | **January 6th, 2026**: We will not accept `additionalInstructions` anymore, please ensure you have adopted any necessary changes before. |
| Related Guide | [Advanced Document Generation](/textgen/documents-advanced) |
### Request schema change for POST documents template object
| Property | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Change | As we evolve the documents endpoint, we are changing the request schema when assembling sections into a template at the request. We are deprecating the `sectionKeys` property of type string\[] in favour of the new `sections` property of type object\[]. The new property requires at minimum a `key` with the sectionKey as value and can be optionally extended to customize several section fields with overrides. As previously and unchanged: the sorting of sections is implicit as set by the order of keys in the array. |
| Affected endpoints | [POST documents from dynamic template](/api-reference/documents-classic/generate-document#dynamic-template) |
| Impact | Until shutdown, both the now deprecated `sectionKeys` string\[] as well as the new `sections` object\[] can be passed in the request. |
| Shutdown date | **January 6th, 2026**: We will not accept `sectionKeys` anymore, please ensure you have adopted any necessary changes before. |
| Related Guide | [Advanced Document Generation](/textgen/documents-advanced) |
# Medical Coding
Source: https://docs.corti.ai/release-notes/coding
Updates and improvements to Corti Symphony for Medical Coding
Detailed documentation about Medical Coding is available in the [Medical Coding overview](/coding/overview), and supported coding systems are listed in the [coding systems feature matrix](/coding/coding-systems).
### SNOMED CT country editions released Beta
SNOMED CT prediction is now available in Beta for seven country editions:
* France
* Germany
* United Kingdom
* United States
* Spain
* Sweden
* Denmark
The International edition remains in Beta alongside these country editions. SNOMED CT prediction has been evaluated only on the International edition using American English clinical notes, and the model may not perform equally well in other languages or country editions. We encourage you to evaluate it against your own data and use case. If you notice anything that doesn't look right, please [let us know](https://www.corti.ai/contact-us?products=medical-coding,api) so we can improve it.
See the [SNOMED CT](/coding/snomed-ct) page for details and the [feature matrix](/coding/coding-systems) for availability by country.
### SNOMED CT International live Beta
[SNOMED CT](/coding/snomed-ct) prediction is now available in Beta for the International edition. SNOMED CT is the most comprehensive multilingual clinical terminology, covering diagnoses, procedures, findings, and clinical concepts, and is used in over 40 countries for electronic health records and clinical documentation.
Symphony for SNOMED CT outputs three types of SNOMED concepts: findings, disorders, and procedures.
### French, German, and UK coding systems released Beta
Support for the following coding systems is now available in Beta:
* [CIM-10-FR](/coding/cim-10-fr) (France)
* [CCAM](/coding/ccam) (France)
* [ICD-10-GM](/coding/icd-10-gm) (Germany)
* [OPS](/coding/ops) (Germany)
* [OPCS-4](/coding/opcs-4) (United Kingdom)
Beta coding systems are fully functional with ongoing rapid improvements. See the [feature matrix](/coding/coding-systems) for availability by system.
### Symphony for Medical Coding released
Corti Symphony for Medical Coding is now available. The Code Prediction API converts unstructured clinical text, such as encounter notes, discharge summaries, and transcripts, into structured medical codes for revenue cycle management, health statistics, and more.
The first release includes the following coding systems, generally available:
* [ICD-10 (International)](/coding/icd-10-int)
* [ICD-10-CM](/coding/icd-10-cm) (United States)
* [ICD-10-PCS](/coding/icd-10-pcs) (United States)
* [NHS ICD-10](/coding/icd-10-uk) (United Kingdom)
Each prediction returns the code, display name, evidence spans showing where the medical entity is mentioned in the source text, and alternative codes the system considered. See the [core concepts](/coding/introduction) page for the response structure and the [coding systems](/coding/coding-systems) page for the full feature matrix.
# Corti Assistant
Source: https://docs.corti.ai/release-notes/corti-assistant
Updates and improvements to Corti AI scribe applications
Detailed documentation about Corti Assistant is available [here](/assistant/welcome).
#### New
* **Version and medical device metadata via `getStatus()`**: The Embedded API `getStatus()` now returns the application version (`applicationVersion`), whether the current build is a medical device (`isMedicalDevice`), and, for medical device builds, a `medicalDevice` object with the device identifier, build date, and composed UDI. Host applications that do not expose the Assistant settings page can now display this regulatory information wherever appropriate. See the [`getStatus()` reference](/assistant/api/get-status)
#### Improvements
* **Deep-linkable session navigation**: Session views are now represented in the URL (for example `/session/{sessionId}`, `/session/{sessionId}/generate`, and `/session/{sessionId}/{documentId}`), so you can deep-link to a document, refresh without losing your place, and use browser back and forward across tabs. Embedded hosts can navigate directly to a specific document. Existing session URLs continue to open the Context tab
* **Refreshed device-link pairing page**: Updated the visual design of the device-link QR code sign-in page for the companion mobile app. See the [`showDeviceLinkQR()` reference](/assistant/api/show-device-link-qr)
#### Fixes
* Fixed an issue where users could be signed out mid-workflow when an upstream API call returned a 401, most visibly during document generation started near access-token expiry
#### Fixes
* Fixed an issue where an expired session that failed to refresh could trigger a rapid loop of redundant network requests and analytics events
#### New
* **Inline templates and forced first-document generation**: Integrators can now provide runtime templates through `templates.sources.inline.templates` and set `templates.defaultTemplate.behaviour` to `"force-first-document"` to make the first generated document use a configured template. See [Configuration Scenarios](/assistant/configuration-scenarios#i-want-to-provide-an-inline-template-for-the-current-embedded-instance) and [`setInteractionOptions()` reference](/assistant/api/set-interaction-options) for payload examples
* **Template picker groups and favorites**: The schema-driven template picker now separates `My templates`, `Organization`, and `Standard` templates so clinicians can distinguish organization-provided templates from standard templates. Clinicians can also favorite Organization and Standard templates to move them into `My templates`
#### Improvements
* Improved template customization experience: End-users using Guided Document Synthesis can now customize document templates through a more intuitive interface, making template creation and editing easier
* Updated documentation examples: Refreshed code examples throughout the documentation to use the latest Embedded component and API
#### Fixes
* Fixed an issue where the button to generate a document with the default template could occasionally still appear even after a document had already been generated using the default template
#### Fixes
* Fixed an issue where sessions with one available personal template could hide the additional document action and ignore the user's saved default template
#### New
* **Personal Templates for Guided Document Generation (Beta)**: Clinicians using the new Guided Document Generation can now customize and manage their own document templates. Advanced customization options are available as a beta feature and can be enabled through the Embedded API. See [Configuration Scenarios](/assistant/configuration-scenarios#i-want-clinicians-to-be-able-to-use-personal-templates) to learn more. We welcome feedback from early testers as we continue refining the user experience and working toward general availability
* **New Embedded API interaction options**: Integrators can now configure additional `setInteractionOptions()` behavior to better tailor the clinician experience:
* **Default template selection**: Control whether clinicians can choose and persist their own default template
* **Generated document limits**: Limit the maximum number of documents that can be generated per interaction
* **Spoken language options**: Restrict the available spoken languages and optionally specify a default language
See the [Embedded API configuration scenarios](/assistant/configuration-scenarios) and [`setInteractionOptions()` reference](/assistant/api/set-interaction-options) for payload examples.
#### Improvements
* Improved empty states in the template picker when no templates are available.
* Added public guidance for loading the embedded Assistant with `visibility="hidden"` until navigation or interaction lifecycle events confirm the session is ready to display. See [Embedded Reliability, Timeouts, and Recovery](/assistant/reliability-timeouts).
#### Fixes
* Fixed an issue where the spoken language selector for legacy templates could show all supported languages instead of only the languages available from the user's templates
#### Fixes
* Fixed an issue where current dot-notation Embedded API events, such as `interaction.loaded` and `embedded.appConfigured`, could stop being sent after embedded navigation, while legacy events continued to work
#### Improvements
* Internal maintenance updates
#### Fixes
* Fixed an issue where ending a session without a transcript and opening the transcript popover could make the session UI unresponsive
* Fixed an issue where the session configuration layout could become cramped on viewports around 720-800px high
* Fixed an issue where the default template action could remain visible after the default template had been generated
#### Fixes
* Fixed a migration issue where previously persisted embedded interaction options from older Assistant versions could trigger a configuration error after newer template options were introduced. `configure()` remains supported during the deprecation period, but new integrations should use `configureApp()` and `setInteractionOptions()`. See the [configuration migration guide](/assistant/configuration-migration) for migration details
#### Fixes
* Fixed an issue where Corti Studio users were not identified correctly for feature targeting
#### Fixes
* Fixed an issue where region-specific Guided Document Generation templates could appear in the template picker for users outside their configured regions
#### Fixes
* Fixed an issue where Guided Document Generation rollout filters would not apply correctly
#### New
* **Guided Document Generation (Beta)**: Added support for a new document generation flow that enables customers to use the new [Guided Documentation Method](/textgen/documents-guided-synthesis) in the Corti API. With this functionality, customers can build custom templates in the Corti Console and make them available in Assistant to their end users. See the [configuration scenarios](/assistant/configuration-scenarios) for Embedded API examples. By default, this functionality is enabled for all new projects in the Corti Console. Customers who want to enable this for an existing project can reach out to [help@corti.ai](mailto:help@corti.ai).
* **In-app audio health alerts**: Added support for audio health alerts, helping users notice microphone or audio-quality issues during recording.
#### Improvements
* Improved fault tolerance across the application to better handle unexpected failures and keep the experience stable.
#### Fixes
* Fixed an issue where legacy `configureSession()` `defaultMode` updates could re-enable interaction modes that had previously been disabled with `configure()`, such as virtual mode
#### Fixes
* Fixed an issue where partial `setInteractionOptions()`, `configure()`, or `configureSession()` updates could reset previously configured interaction options, such as `documents.actions.sync`
#### New
* Added `configureApp()` and `setInteractionOptions()` to the Embedded API, splitting app-level configuration from interaction-level options such as mode, spoken language, template defaults, and document actions. The existing `configure()` and `configureSession()` methods are now deprecated and will remain supported during the deprecation period. See the [configuration migration guide](/assistant/configuration-migration) and the [Embedded API deprecation timeline](/assistant/deprecation-timeline) for more information
* Dictation: Expanded the previously introduced punctuation and voice command controls with in-session access, supporting easier control for end-users in an embedded context. Spoken punctuation and voice commands are now enabled by default, while remaining configurable by end users
#### Improvements
* Enhanced handling of deleted interactions to prevent archive errors when records are removed through retention policies or external APIs
#### Fixes
* Fixed an issue where Embedded API interaction-scoped methods did not always work correctly when Assistant was opened on an existing interaction
#### Improvements
* Improved UI translation support and resolved UX inconsistencies in the redesigned session configuration experience ahead of broader rollout
#### Improvements
* Session Configuration (Early Access): Introduced a redesigned session configuration experience, including improved microphone setup guidance for in-person ambient sessions to help improve transcript and documentation quality. The experience is currently rolling out progressively to selected customers ahead of broader availability
* Increased [minimum iframe height requirement (480px)](/assistant/configuration#embedded-iframe-/-sdk) The embedded Assistant iframe now requires a minimum height of 480px (up from 320px) to support the new session configuration design, including its compact variant for smaller screen sizes. Please update your iframe configuration accordingly
#### Fixes
* Fixed an issue where `getStatus()` could return the most recently created interaction instead of the currently displayed interaction
* Rolled out a critical security update
#### Improvements
* Enabled diarization for ambient recordings to improve speaker separation in generated transcripts while preserving the existing transcript view without speaker labels
* Added support for hosting Assistant under a configurable non-root path (e.g. `/corti-assistant/`), ensuring all application routes and assets resolve correctly within the configured proxy prefix. For more information, read our [proxy guide](/assistant/proxy)
#### Fixes
* Fixed an issue where configured `websocketBaseUrl` values were not consistently respected and could fall back to the default URL
* Rolled out a critical security update
#### New
* **Template Customization**: Concluding the progressive rollout of the Template Assembler, the feature is now enabled by default in Assistant and includes the latest customization options for adjusting section writing style, formatting, and adding instructions. If needed, end-user access can still be disabled [via the Embedded API configuration](/assistant/configuration#templateeditor). Standalone customers can contact [help@corti.ai](mailto:help@corti.ai) to disable it
* **Dictation (Beta)**: Added support for controlling punctuation and voice commands via the UI. From the Input tab in Settings, users can choose whether spoken punctuation (e.g., “period”, “slash”) is converted into symbols, and whether voice commands are enabled for more hands-free navigation. Easier access in embedded contexts is coming in a future release
#### Improvements
* Added guardrails to prevent silent failures when selecting unsupported dictation languages via embedded configuration
* Introduced grouped documentation sections in the session UI using the `Group Name :: Section Name` convention, making it easier to copy related sections mapped to the same EHR field
* Added a visual indication when the end-user triggers sending documentation to the host application, to confirm the action and prevent duplicate submissions
#### Fixes
* Fixed an issue where parts of the generated documentation would sometimes appear in an incorrect language
* Resolved an issue where the back button would navigate within the iframe instead of the parent page
* Fixed an issue where the new session screen displayed the wrong session type on first load in Safari
#### New
* **Web Component API Now Recommended**: Documented the [Web Component API](/assistant/web-component-api) as the new preferred integration method for Corti Assistant. This approach provides a cleaner, more maintainable integration pattern compared to postMessage or Window APIs.
* **New React Integration Guide**: Added a comprehensive [React integration guide](/assistant/guides/react-integration) demonstrating best practices for embedding Corti Assistant in React applications using the Web Component API.
* **Improved Navigation and Entry Points**: Restructured Assistant documentation with a new [welcome page](/assistant/welcome) and [integrations overview](/assistant/integrations-overview) to help developers quickly find the right integration approach for their needs.
#### Fixes
* Fixed an issue where the "Template Management" label in the template picker did not follow the selected UI language
* Fixed an issue where dictated text could appear duplicated in AI Chat
* Fixed an issue where some sessions did not automatically receive a generated title
#### Fixes
* Fixed an issue where the shortcut to generate the default document would intermittently not appear
#### Improvements
* Internal maintenance updates
#### Improvements
* Internal maintenance updates
#### Fixes
* Fixed inconsistencies in `postMessage` response handling documentation
* Fixed an issue that made switching between user accounts more difficult
#### Improvements
* Improved handling of built-in templates to ensure only correctly configured templates can be generated
* Expanded availability of Template Assembler by adding additional breakpoint thresholds, enabling support for a broader range of use cases
* Added required `clipboard-write` permission to the iframe to enable copying documentation
#### Improvements
* Re-enabled interim text during dictation for languages where supported. Learn more [here](/stt/transcribe)
#### Fixes
* Continued to consistently send the deprecated `ready` event on every authentication request, to ensure backwards compatibility
#### Improvements
* Simplified microphone selection by centralizing microphone selection in session only
* Updated microphone selection to remember the last-used device via local storage when no user preference is set
#### Improvements
* Internal maintenance updates
#### Improvements
* Internal maintenance updates
#### New
* Added [`getTemplates` method](/assistant/api/get-templates) to the Embedded API to retrieve available document templates for the authenticated user
#### Improvements
* Improved validation to prevent document generation when context is empty (no facts or notes present)
* Enhanced Embedded API error handling with clearer, human-readable validation messages for all API methods
* Updated [`configureSession` documentation](/assistant/api/configure-session) to clarify input restrictions, improved error handling, and specified behavior in case of user defaults (no overwrite)
* Optimized application loading for faster initial state rendering
#### Fixes
* Fixed an issue where ending dictation in context notes cleared the note content
* Fixed all dictation buttons becoming active simultaneously on mobile devices
* Fixed note content loss when navigating to newly created interactions during rapid input
#### Fixes:
* Fixed an issue where *Template Assembler* did not show up in standalone even when enabled
#### New
* **Template Customization**: Introduced the *Template Assembler*, allowing end-users to customize documentation templates by adding, removing, reordering, and renaming template sections, as well as editing the template title. The feature is currently hidden by default behind a feature flag and will be rolled out progressively over the next two months. Customers interested in learning more can reach out at [help@corti.ai](mailto:help@corti.ai).
* Introduced a [new embedded event system](/assistant/events/index) with many more events to subscribe to and a lot more detail per event. The [legacy event system](/assistant/events/legacy-events) is deprecated, but still supported.
* *Norwegian Nynorsk* and *Norwegian Bokmål* now available as [selectable UI languages](/assistant/configuration#available-interface-languages)
#### Improvements
* Removed the success toast message that appeared after pressing the Synchronize Document button
* Enabled users to access the Medical Device instance of the product via Mobile Applications
#### Fixes
* Fixed a bug that prevented templates in the default template's language from being prioritized in the template picker
* Fixed a bug that prevented the template picker to display under some circumstances
#### Improvements:
* Improved data fetching performance
#### Fixes:
* Fixed an issue where document sections sometimes switched order
#### Improvements:
* Added stickiness to section headings and toolbar in documents so they remain visible while scrolling, including background to make sure there is no text overlap.
#### Fixes:
* Fixed an issue that was preventing changing language and setting overrides at the same time using the Embedded API method `configure`
* Fixed a bug where it was not possible to continue dictating after stopping the dictation recording
* Fixed an issue that led template sections to appear in english by default
* Fixed a bug that was rendering the template picker empty when the page was resized beyond a certain screen resolution during usage
#### Fixes:
* Fixed issue causing certain public API endpoints to be unusable
#### Improvements:
* Added caching to improve application loading speed
#### Fixes:
* Fixed some issues that were rendering dictation to malfunction in the context tab
#### Fixes:
* Fixed a bug where dictation language could not be set
* Fixed a bug where document payloads were not aligned between different events
#### Improvements:
* Added the ability to configure the presence and label of a *"Synchronize Document"* button for embedded flows
#### Fixes:
* Fixed a bug where application unintentionally discarded recently dictated notes
#### Fixes:
* Rolled out a critical security update
#### Fixes:
* Rolled out a critical security update
#### Fixes:
* Rolled out a critical security update
#### Improvements:
* Added caching to improve application loading speed
* Updated MediaRecorder settings to provide more optimal audio configuration for ambient scribing
* Enabled users to change their default template directly from the template picker
#### Fixes:
* Rolled out a critical security update
#### Fixes:
* Fixed an occasional mix-up in speaker identification within the transcript, during virtual sessions
#### Fixes:
* Fixed an issue where the default template did not appear in the template picker
#### New:
* Added the ability to [configure the application interface through the Embedded API](/assistant/api/configure)
* Added support for [setting credentials via the Embedded API](/assistant/api/set-credentials)
#### Improvements:
* Improved the authentication flow with a new [getStatus method](/assistant/api/get-status) to retrieve the application's current state, including authentication status, user info, URL, and interaction details
* Enhanced the template picker by sorting templates alphabetically within each language
* Updated the template picker to prioritize templates that match the language of the active user's default template
#### Fixes:
* Fixed reloading the window on re-authentication attempt when the session was already initialized
#### Fixes:
* Resolved a configuration error that caused German dictation to malfunction
#### Improvements:
* Facts headings now consistently display in the user's chosen UI language, ensuring a clearer and more predictable experience for those working across multiple languages
#### Fixes:
* Fixed Fact headings in new sessions rendering in a code-like format in the Context tab
* Fixed Fact headings occasionally appearing in the user's default spoken language
#### Improvements:
* Added temporal grouping in transcripts, in languages that previously didn’t support it
* The microphone volume indicator has returned with an improved, WCAG-aligned design to support clearer and more accessible audio setup
#### Fixes:
* Fixed scrolling behavior to ensure documents scroll correctly again
* Resolved an issue where session language defaulted to English in embedded mode
#### Improvements:
* Disabled the sound associated with muted-microphone alerts to reduce disruptive prominence in specific setups
#### Improvements:
* Lowered the threshold for silence detection in streams, to reduce the sensitivity of muted-microphone alerts
#### New:
* Dutch and Norwegian available as selectable dictation language
#### Improvements:
* Added a microphone mute alert to notify users when their microphone is muted
* Added support for selecting the current tab as a virtual input source when using the embedded applications
#### Fixes:
* Fixed an issue where requests to the AI Chat were rejected due to consultations not being registered as ended
* Fixed a race condition that prevented the `?language=` parameter from reliably setting the session language
# Corti Releases
Source: https://docs.corti.ai/release-notes/overview
Find the latest updates across Corti AI products
The release notes pages will detail updates to the Corti AI Platform, which includes the following:
Learn about the latest capabilities for speech to text (STT) language models.
Learn about the latest capabilities for text generation LLMs.
Learn about the latest capabilities for Corti AI scribe applications.
Learn about the latest capabilities for Symphony for Medical Coding.
Learn about the latest capabilities for the Agentic Framework and agent APIs.
The following pages detail how changes are made and upcoming changes to be aware of:
Learn about how changes to the APIs and models are carried out.
Details about upcoming breaking changes.
# Speech to Text
Source: https://docs.corti.ai/release-notes/stt
Updates and improvements to Corti speech to text
Detailed documentation about Speech to Text is available [here](/stt/overview), and supported languages [here](/stt/languages/).
## Tympany WER analysis tool now available as open source
Building with speech-to-text does not stop at transcription. You need to know where the model succeeds, where it fails, and whether each difference matters for the workflow you are building.
Tympany is an open source, local-first web app for speech-to-text evaluation. Use it to create "Beyond WER" (BeWER) reports from reference and generated transcripts, review errors, recompute error rates after excluding differences that are not true model failures, and identify errors that need to be reported to Corti for ongoing STT improvement.
See full details and gain access to the tool [here](/stt/evaluations).
### Language expansion and updates
* Expansion of `Base` tier to add support for 27 new languages
* Languages upgraded from `Base` to `Enhanced` tier - Spanish, Arabic
* Expansion of `Enhanced` tier to add support for new language - Finnish
* Expansion of `Premier` tier to add support for new language - English (AU)
See full details on the [languages page](/stt/languages), including ability to search by language (name or code) and filter by performance tier.
Corti values the opportunity to expand to new markets, but we need your collaboration and partnership in speech-to-text validation to expand language support from base to enhanced or premier tiers. [Contact us](https://help.corti.app) to learn more.
### Replacements and Keyterms now available on Streams and Transcripts endpoints
The features [replacements](/stt/replacements) and [keyterms](/stt/keyterms) are now supported on all three STT endpoints - `transcribe`, `streams`, and `transcripts`.
Additionally, some configuration parameters have been updated on the [streams](/api-reference/streams) and [create transcripts](/api-reference/transcripts/create-transcript) endpoints for improved consistency. No timeline is currently planned to remove support for the deprecated parameters. See details in [upcoming changelog](/release-notes/changelog-upcoming#2026-06-16).
### Maintenance
* Ensure replacements correctly handle casing and punctuation
* Fix capitalization of first word in transcript (capitalize) and after colon and semicolon (lowercase)
* Update formatting option `locale:short` to use four-digit year (e.g., 2026)
* Increase maximum audio file duration accepted for [upload recordings](/api-reference/recordings/upload-recording) from 60 to 120 minutes
### Keyterm bias
New feature for `transcribe` web socket endpoint now available - define `keyterms` the system should be aware of and biased towards to ensure proper nouns unknown by the STT model are recognized and improve recognition reliability for other general terms. See feature details [here](/stt/keyterms).
### Swiss French `fr-CH` now supported
Language code `fr-CH` is now supported for use of Swiss French speech to text.
Beyond current French speech to text handling, this model has improved support for Swiss medications and returns punctuation without non-breaking spaces (as used in French (`fr` or `fr-FR`) documentation).
The [languages page](/stt/languages) has been updated, and detailed information for punctuation handling on text insertion is available [here](/stt/best-practices-transcribe#locale-specific-spacing).
### Commands with wildcard variable
New dictation feature, **wildcard variable commands** now available. Unlike `enum` command variables, `wildcard` variables provide the ability to recognize a command based on undefined, open-ended text. A literal trigger word, such as “select”, is required before defining a wildcard variable.
See feature details with examples [here](/stt/commands#commands-with-wildcard-variables) and configuration in the [API reference](/api-reference/transcribe#param-variables).
### Updated asynchronous method for audio file processing
The `/transcripts` endpoint provides batch audio file processing using a synchronous-to-asynchronous method, where requests process synchronously for 25 seconds before hitting timeout and continuing asynchronously. A new request parameter, `async`, is now available to force asynchronous processing. This method returns the 202 status response right away, instead of waiting the full timeout window, so that clients can poll status endpoint and get full transcript when it is completed.
See more details about `/transcripts` [here](/stt/transcripts) and the updated parameter in the [api-reference](/api-reference/transcripts/create-transcript#body-async).
### Replacement Rules
New dictation feature, **replacement rules**, now available. Define terms to "find" and "replace" in STT output using the `replacements` configuration option in `/transcribe` requests. See details and examples [here](/stt/replacements) and API configuration [here](/api-reference/transcribe#param-replacements). Replacements configuration is limited to 1,000 items per connection.
### Bug fixes
* "D3" now correctly handled
* Updated measurement formatting so that "0°" is supported
### Raw PCM audio now supported
Audio processing has been updated so that raw PCM audio can be sent over web socket secure (wss) audio streaming with `/transcribe` and `/streams`. When using raw PCM audio, `audioFormat` must be declared in configuration. See details [here](/stt/audio#raw-audio).
### Infrastructure update for `/transcripts` endpoint to improve speech-to-text performance for audio file processing
Infrastructure updates to improve performance of audio file processing:
* Updates to batch sync-to-async audio file processing to ensure consistent latency (RTFx 100)
* Model updates to reduce risk of hallucinations while improving medical term recall (English, German, French, Danish)
* Ensure efficient GPU utilization, refine load balancing and partition management (enhance autoscaling of ASR services)
No client action is necessary to select a specific model version or use the new architecture. See full details on creating transcripts from audio files [here](/api-reference/transcripts/create-transcript).
### New `/languages` endpoint now available
New REST endpoint available for clients to programmatically retrieve speech to text languages available per endpoint. See [GET/languages/](/api-reference/languages/list-languages).
### Updated German and French STT models released
Update STT models for German (`de`, `de-CH`) and French (`fr`) have been released for improved medical term recall and WER performance. This release focused on general terminology recognition, medication names, and punctuation handling.
### Audio Health Events
A new feature for real time audio streaming, **audio health events** are notifications from speech to text system when audio quality may be compromised or problematic. The feature can be enabled with boolean parameter in `transcribe` and `streams` configuration.
The following two scenarios are supported in this initial release:
* **Speech Quality Issue Detected** - triggered by background sounds, white noise, or compromised audio.
* **Long Silence Detected** - defined as 10s of continuous audio without any sound/noise.
Additional functionality is expected in subsequent releases, as well as ability to define threshold for alerting in the API configuration.
Learn more about `audioEvents` [here](/stt/audio-events).
### Command and formatting updates
* Added timeout to return formatted text on silence
* Added non-breaking space before French punctuation characters
* Added support for square meter (m²) and square centimeter (cm²) units
* Fixed handling of decimal number format so that negative numbers are supported
* Security hardening: Improved validation of user-supplied command phrase inputs to prevent malformed values from affecting processing
### Updated French speech to text models now available
French speech to text model (language code `fr`) has been updated for improved medical term recall and conversational, far field audio support.
### Transcript output formatting now supported on Streams endpoint
[Formatting](/stt/formatting) is now supported on the `streams` endpoint, with server-defined default values applied automatically. At this time, API configuration of formatting options is not supported for `streams` as it is for `transcribe` endpoint.
Response messages for configuration errors have been clarified. See details [here](/api-reference/transcribe#5-error-handling) and [here](/api-reference/streams#5-error-handling).
### Improved diarization now available
Updates to Corti's Speech to Text models and diarizer, in which reliability and precision for speaker separation and identification have been optimized, have been released.
See details on this feature [here](/stt/diarization) and configuration recommendations for `streams` and `transcripts` requests [here](/stt/audio#channel-configuration).
### Formatting updates and improvements
* Add formatting for more units: "g/dL", "cL", "hL", "pL", "mg/L", "mg/kg", "U/mL", "mL/min",
* Add support for "times two" -> "x2" and "two plus" -> "2+" patterns (1 through 10)
* Fix handling of German spaced number variations
### Next-generation speech recognition models and infrastructure
New and improved model architecture and infrastructure bring the following actions and benefits:
| Actions | Benefits |
| ------------------------------- | -------------------------------------------------------------------------------------- |
| **Optimize STT performance** | Maximize accuracy of transcripts and diarization while minimizing hallucination risk |
| **Refine system configuration** | Improve (reduce) latency, augment formatting, and re-introduce interim results feature |
| **Infrastructure resilience** | Improve system scalability and GPU resource utilization |
STT models for the following languages have been updated on both `/transcribe` and `/streams` endpoints:
| Language | Language Code |
| ------------ | ------------- |
| Danish | `da` |
| English | `en`, `en-GB` |
| French | `fr` |
| German | `de` |
| Swiss German | `de-CH` |
Beyond defining the desired language code in API requests, no client action is necessary to select a specific version of a model. Please [contact us](mailto:help@corti.ai) for support or further information.
### Interim Results now available during real-time dictation for low latency transcript previews
Use the `interimResults` configuration parameter in `/transcribe` real-time dictation requests to have transcript previews returned from the server at a faster rate than final transcripts.
See full feature details [here](/stt/interim-results), parameter configuration details [here](/api-reference/transcribe#param-interim-results), and availability by language [here](/stt/transcribe).
### Locale-based formatting for dates, times, and numbers
Dictation formatting can now apply proper styling based on local standards for dates, times, and numbers. Locale is determined based on the `primaryLanguage` defined in the web socket configuration.
See full formatting details per language [here](/stt/formatting) and parameter configuration details [here](/api-reference/transcribe#param-formatting).
Note that legacy date parameter values are still supported by the API; no breaking changes were implemented.
### Dictation formatting improvements
* Expand units handling
* Expand acronym handling
* Improve vertebrae formatting: Support for both `letter number` and `letter number letter number` patterns (e.g., "L three" -> "L3" and "L three L four" -> "L3-L4")
* Improve TNM and cancer stage formatting: Support for `T number N number M number` and `stage number` patterns (e.g., "T two N one M one a" -> "T2 N1 M1a" and "stage two b" -> "Stage IIB")
* Improve percentage handling: Move handling from `spokenPunctuation` to `formatting` so that percent symbol is only returned when there is a number measurement (e.g., dictation, "What *percent* sure are you question mark *eighty percent*" -> "What *percent* sure are you? *80%*")
* Expand handling of German dates: Years without spaces (e.g. "zweitausendsechsundswanzig" -> "2026") and new pattern (e.g., include pattern "siebter fünfter neunzehn vierundachtzig" -> "07/05/1984")
* Expand handling of German units: Units with -n ending (e.g., "Millilitern")
* Add new default for ordinals, `numerals_above_nine`: Ordinals one through nine are written out (first, second, third) and ten and above are abbreviated (10th, 11th, 12th)
### Dictation formatting improvements
* Improved support for `en-GB` regional spelling variations
* Improved support for `de-CH`, `gsw-CH` regional spelling variations
* Remove extra whitespace observed with degree and percent symbols
* Updated handling of single digit numbers, now represented as numeral when followed by year/ month/ week/ hour/ day/ minute/ second(s)
* Updated handling of hyphenated numbers and age dictation
* Updated handling of "one twenty" (and similar number pattern) dictations
* Updated handling of Month-Year dictation
* Fix for an issue that occasionally caused both formatted and unformatted versions of transcript text to be emitted
Update to handling of number formatting in Danish.
Bug fixes:
* Update to French formatting for improved number handling
* Update to Danish formatting for liter abbreviation handling
* Bug fix for extra whitespace being returned with commands or punctuation
* Command service performance and reliability improvements
### Dictation Formatting now supported in more languages
Dictation formatting is now available in **English, German, and French**. There is also limited support in **Danish** with additional improvements in progress.
See detailed API specification [here](/api-reference/transcribe#param-formatting) and documentation of all available formatting options [here](/stt/formatting).
### Improved handling of initialisms in German dictation
An updated version of the German (`de`) language model was released with improved handling of dictated initialisms: spoken letters, numbers, and abbreviations. Over 800 additional medical abbreviations were introduced to the system, with focused improvement on dental tooth exams, adding support for ICD-10 codes, and ability to have individual letters/numbers returned from the model.
### Audio input validation
Improved server-side validation of streamed audio - if audio does not meet requirements outlined [here](/stt/audio#supported-audio-formats), then a `400 Invalid Audio` error will be returned.
### Addition of new web socket event: `flush`
Provide clients the ability to force clear the audio buffer without closing the connection. In response to a `"type": "flush"` message from the client, the server will return recognized text and/or commands and respond with `"type": "flushed"`, and keep the web socket connection open.
See more details on `/transcribe` [here](/api-reference/transcribe#flush-the-audio-buffer) and `/streams` [here](/api-reference/streams#flush-the-audio-buffer).
### Update to Swiss German language codes
There are now two different language codes that may be used for Swiss German use cases:
* **Swiss German** (language code `gsw-CH`), where dialectical Swiss German is spoken (recommended for AI scribe use cases)
* **Swiss High German** (language code `de-CH`), where Swiss High German is spoken (recommended for dictation use cases)
See more details [here](/stt/languages), or please [contact us](mailto:help@corti.ai) if you need further assistant in selecting the best language code.
### Updated Danish language model
Updated Danish (`da`) language model is now available for dictation (`/transcribe`) and ambient (`/streams`) use cases. The language is rated as `premier` tier as over 158,000 medical terms were included in the training and validation data sets.
Bug fixes:
* Fixed an issue that prevented `automaticPunctuation` parameter from working as expected. Furthermore, `spokenPunctuation` and `automaticPunctuation` are mutually exclusive: Only one of these parameters should be set to `true` in a given `/transcribe` configuration, and if both settings are present and set to true, then `spokenPunctuation` will take precedence. See more detail [here](/stt/punctuation).
### Updated French language model
Updated French (`fr`) language model is now available for dictation (`/transcribe`) and ambient (`/streams`) use cases. The language is rated as `premier` since over 170,000 medical terms were included in the training and validation data sets.
### Dictation now supported in Dutch and Norwegian
Norwegian (`no`) and Dutch (`nl`) are now available for use on the `/transcribe` endpoint for dictation use cases. As a result they have been moved to the `enhanced` language tier. Additionally, Hungarian (`hu`) medical terminology has been expanded and improved.
See more information about supported functionality for dictation [here](/stt/transcribe) and language tiers [here](/stt/languages).
Feature limitation:
* Some of the recent `/transcripts` endpoint language model updates (`de`, `en`, `en-GB`, `fr`) have been rolled back so that performance of asynchronous audio file processing can be improved. Infrastructure updates are underway to improve model performance.
* Speech-to-text accuracy from `/transcripts` asynchronous audio file processing may be degraded as compared to real-time audio processing via the `/streams` and `/transcribe` APIs, which are not impacted by this issue.
### Updated English language model
Updated English (`en`) language model is now available for dictation (`/transcribe`), ambient (`/streams`), and transcription (`/transcripts`) use cases. This version is rated as `premier` tier as over 170,000 medical terms were included in the training and validation data sets.
### Improved transcoding support in speech-to-text APIs
Update to both streaming and asynchronous audio file processing so that **transcoding is supported**. Previously audio files were required to conform to 16-bit, 16kHz formatting. Now, assuming proper file types are used, any precision and sample rate are accepted. See more details [here](/stt/audio).
Parameter deprecation:
* Definition of the parameter `modelName` is no longer required in `/transcripts` API requests. The latest and greatest model available per language will be applied automatically.
* If the argument is included in the request, then it will be ignored. If the configuration is otherwise valid, then the request will process as expected. See full specification [here](/api-reference/transcripts/create-transcript).
Feature limitation:
* The interim (preview) results feature of the `/transcribe` API is not performing as expected. As a result, it is being disabled for most languages to prevent issues with speech-to-text latency and accuracy.
* If the `interimResults` parameter is included in the request for a language that does not support this functionality it will be ignored and, so long as the request is otherwise valid, the configuration will be accepted.
### Conversational transcripts now supported in Arabic
Arabic (language code `ar`) is now available for ambient documentation workflow - Capture conversations spoken in Arabic via the `/streams` API.
### New dictation functionality available: `Formatting`
New dictation functionality available: **Formatting**. Take control over how dates, time, units, and numbers should be transcribed in the streaming speech to text output.
See detailed API specification [here](/api-reference/transcribe#param-formatting) and documentation of all available formatting options [here](/stt/formatting).
### Updated German language model
Updated German (de) language model is now available for dictation (`/transcribe`), ambient (`/streams`), and transcription (`/transcripts`) use cases. This version is rated `premier` tier as over 150,000 medical terms were included in the training and validation data sets.
### Dictation now supported in Hungarian
Hungarian (hu) Enhanced language model is now available for dictation (`/transcribe`).
Swedish (sv) Enhanced language model is now available for dictation (`/transcribe`).
Updated Norwegian (no) and Swedish (sv) Base language models are now available for ambient documentation (`/streams`) and transcription (`/transcripts`) workflows.
Danish (da), German (de), and French (fr) Enhanced language models now available for dictation (`/transcribe`) workflows.
New API endpoint, `/transcribe` now available! Use this endpoint for stateless, real-time streaming dictation workflows. See more details [here](/api-reference/transcribe/) in the API reference and [here](/stt/dictation-web/) for access to the Corti Dictation Web Component.
Swiss German (de-CH) Enhanced language model now available for ambient documentation (`/streams`) workflows.
French (fr) Enhanced language model now available for ambient documentation (`/streams`) workflows.
Introducing a new tier system for defining functionality and performance of speech to text language models. Read more about it [here](/stt/languages/).
Updated German (de) and Swiss German (de-CH) language models available.
Updated Danish (da) and Swedish (sv) language models available.
Announcing the launch of Corti AI Platform. Read more about it [here](https://www.corti.ai/).
The following languages are supported by Corti speech to text: English (en for US English, and en-GB for UK English), Danish (da), German (de), Swiss German (de-CH), French (fr), Swedish (sv), Spanish (es), Norwegian (nl), Dutch (no), Italian (it), and Portuguese (pt)
# Text Generation
Source: https://docs.corti.ai/release-notes/textgen
Updates and improvements to Corti AI Text Generation functionality
#### Template and Section resource ownership and permissions Beta
This release builds onto the new Guided Sections and Templates APIs and resources and adds support for the management of these resources across API, Console and Embedded Assistant.
* Templates and Sections created via an API Client configured with Client Credentials are shared resources within a Console project
* Any API Client with direct access configured (e.g. Client Credentials) within the same Console Project has full Read + Write access
* API Clients with delegated access (e.g. ROPC of PKCE setup for embedded Assistant) within the same Console Project can delegate read access as follows:
* Read access for individual end users can be managed further by assigning templates and sections to specific Console Customers that can represent any group of users (e.g. customer, department, test user group)
Corti-provided default sections remain available for Read-access alongside your custom resources. The LIST /templates and LIST /sections endpoints come with various query params, including flexible structured labels, you can leverage to filter and control access in your client application when integrating purely via API.
Read more in the guide: [Manage Access](/textgen/console-templates/manage-library#manage-access).
#### New: Bring-your-own prompts — create sections and templates via API Beta
Sections and templates are now **first-class citizens in the API**, empowering you to create all aspects of sections and templates — including their prompts — directly via API. The new `/documents/sections` and `/documents/templates` endpoints let you author, version and publish your own resources, then reference a template or assemble sections directly to generate a document.
* **Define every aspect of a section.** Content instructions, context, writing style, plus a powerful new way to handle formatting that combines presets (list, paragraph, subheadings) with flexible customization options via the typed `outputSchema` (`string`, `number`, `boolean`, `array`, `object`).
* **Compose templates from sections.** A template adds top-level instructions and references published sections by UUID with an explicit order.
* **API-first by design.** You steer the section-specific prompts while Corti ensures the overall system prompts for document generation and guardrails continue to play nicely together.
* **Versioning and publishing.** Iterate freely via `POST .../versions`; previously published versions stay live until you explicitly publish a new one. Inherit from any existing section or template via `inheritFromId` — your inheriting resource keeps tracking future Corti improvements on fields you don't override.
This unlocks template management surfaces for your admins and lets you expose end-user customization options inside your own solution. Corti-provided default sections remain available alongside your custom resources.
Read more in the new guides: [Create a Section](/textgen/section-creation) and [Create a Template](/textgen/template-creation).
#### New: Guided Clinical Synthesis — POST /documents Beta
Guided Clinical Synthesis transforms transcripts, extracted facts and documents into **precise, structured and schema-controlled clinical documentation outputs**. Instead of relying on the LLM to handle both content and formatting simultaneously, guided synthesis separates these concerns and enables schema-defined structured outputs — defined when you author your sections via the new [bring-your-own-prompts API](/textgen/section-creation).
The new `POST /documents` endpoint accepts four template-supply paths in a single, consistent request shape:
1. **Plain `templateRef`** — reference a stored template (optionally pin a version). Lightest path, no side-effects, ideal for production traffic.
2. **`templateRef` with runtime overrides** — keep your base template and patch a section's title, instructions or output schema for a single call. A drift-proof, auto-generated template aggregate is persisted with `inheritedFromId` pointing at the base.
3. **Assembly from stored sections** — pick the sections you want, in declaration order, and assemble a template on the fly. The resulting aggregate is saved so you can reuse it.
4. **Fully inline dynamic template** — define the template and every section inline. Sections and the wrapping template are persisted for 30 days as drift-proof snapshots or "receipts" you can turn into permanent resources.
**Flexible context input.** Unlike Classic — which accepts **exactly one** `context.type` per call (facts *or* transcript *or* string, never mixed) — Guided lets you pick **exactly one** of two input modes per call:
* **`context` array** — combine context types in one call, e.g. a referral letter as `text` + a `transcript` of the live consultation + a few pre-chart `facts` items, all shipped to the LLM together. (Classic could not mix types like this.)
* **`interactionId`** — let the API pull all non-discarded facts and transcripts already attached to that interaction implicitly. No manual context wiring.
`context` and `interactionId` are mutually exclusive today. Coming soon: passing both in the same call to constrain or extend the interaction-attached context with extra explicit items.
**Outcomes**
* **For clinical notes**, this reduces formatting inconsistencies and improves output quality for multi-section documents.
* **For advanced use cases**, structured, typed outputs unlock much more control over downstream usage — think field-level documentation, structured data pipelines and clinical decision support.
* **Drift-proof outputs** — every path except plain `templateRef` snapshots the fully resolved template at request time; subsequent edits to base resources do not affect previously generated documents.
Read more in the [Guided Synthesis guide](/textgen/documents-guided-synthesis).
#### New: Foundation for template ownership and permissions Beta
This release lays the groundwork for **template ownership and permissions** — controls that ultimately separate personal templates (user-created, user-scoped) from organization-wide templates managed by admins, and from Corti-provided standard templates. Once fully released in Corti Console, organizations can distribute approved templates to their users while preventing unauthorized modifications — essential for maintaining consistency and compliance at scale.
Available in this beta:
* **Corti-provided standards as read-only resources.** Corti's curated sections and templates are returned in `LIST /documents/sections` and `LIST /documents/templates` with `source: corti` and cannot be mutated. Inherit from them via `inheritFromId` for specialty variants or organization-specific tweaks; future Corti improvements propagate automatically to fields you have not overridden.
* **API-client–authored resources.** Sections and templates created by an API client are returned with `source: user` — they belong to the project in Corti Console that your API key is associated with. You hold write, version, publish and delete on these.
* **Source-aware list responses.** Both list endpoints return your project's resources alongside Corti Standards in one response. Filter on the `source` field client-side (`corti` vs `user`) to scope a "browse" surface to the curated library or a "my organization" view.
* **Server-side filters for narrowing the response.** `LIST /documents/sections` and `LIST /documents/templates` accept `lang` (BCP-47), `region` (ISO 3166-1 alpha-3), `specialty`, `label` (`key:value` format) and `published` — all repeatable. Use these to narrow by the dimensions your integration actually cares about.
* **Labels for finer-grained distribution.** Apply `Label` objects (`{ key, value }`) to both sections and templates and pass them as `?label=customer:acme` on `LIST` calls — the building block for distributing approved templates to specific customers, departments or workflows at scale.
Assigning your own template and section resources for read-access to other API keys or Customers created via Console is coming soon.
#### Support for `flush` and `factGenerationInterval` Beta
The `/streams` endpoint now supports parameters and event types that add more control to generating facts.
The [`flush`](/api-reference/streams#flush-the-audio-buffer) event is now also respected for fact generation. When sent from the client, this first processes outstanding transcripts and subsequently triggers fact generation.
The [`factGenerationInterval`](/api-reference/streams#param-fact-generation-interval) parameter can be optionally set to `fast_init`, else default to the fixed 60s interval. With `fast_init`, fact generation will follow a logarithmic curve, currently with the initial generation at roughly around 10s, then 20s, then 26s and continuously increasing interval length until the default 60s interval is reached.
Note: Both functionalities carry the Beta. There can be effects on fact (near) duplicates and quantity.
#### Updates: More Descriptive Error Codes
Error codes now return clearer, more descriptive details to make it easier to identify the issue. This effectively replaces the previous format and deprecates the Common Errors.
Here is the schema which you can also see in the API reference responses, along with a couple examples.
```bash response error schema theme={null}
{
"requestid": "",
"status": 123,
"type": "",
"detail": "",
"validationErrors": [
{}
]
}
```
```bash example 400 response theme={null}
{
"requestid": "b0dd76dd-7bc6-4aba-8dc1-efd841060067",
"status": 400,
"type": "about:blank",
"detail": "Validation error",
"validationErrors": [
{
"detail": "must be a valid BCP-47 language tag",
"pointer": ".outputLanguage"
}
]
}
```
```bash example 404 response theme={null}
{
"requestid": "b39b3686-0829-428e-b5e6-1be90214cb3c",
"status": 404,
"type": "about:blank",
"detail": "template not found"
}
```
#### Updates: Standard Templates and Sections
Corti standard templates and sections have undergone various improvements to increase the quality of the documentation to be generated and support the new [`documentation mode`](/textgen/documents-docmode). We have added a few additional standard sections for referral or discharge summary, while streamlining some sections, e.g. there is now only one section for social history and it includes alcohol and nicotine by default. If you don't want this to be included, you can customize the section accordingly. Read more on customizing sections [here](/textgen/documents-customize).
Should you for some reason still need to reference the previous iteration of the standard sections or templates, then those are all available with the post-fix `-legacy` to the section and template key.
Read more in our [guide on templates](/textgen/templates-standard).
#### New: Documentation Modes
You can now set `documentationMode: routed_parallel` and leverage the innovative, reconceptualized approach to generate documents for faster, more consistent and robust outputs. This 2-step, coarse-to-fine document generation mode currently supports FactsR as input and in the first step distributes the relevant facts to each section, based on the specified template (sections), then generates each section in parallel.
This new documentation mode requires the template and its sections to support `routed_parallel`. You can check the new property of the same name `documentationMode` for responses from [GET /template(s)](/api-reference/templates-classic/get-template#response-documentation-mode) and [GET /templateSections](/api-reference/templates-classic/list-template-sections#response-data-items-documentation-mode).
Read more in our [guide on Documentation Mode](/textgen/documents-docmode) or see the [API reference](/api-reference/documents-classic/generate-document#body-one-of-0-documentation-mode).
#### New: Customize existing sections
Empower your customers and end-user clinicians to combine and tailor the Corti provided template sections for custom templating needs. Built atop the ability to reference section keys dynamically in the API request, you can now directly in the request override section names, writing styles, formatting rules and additional instructions, ensuring documentation perfectly fits organizational standards and clinician preferences.
Read more in the [guide to customize sections](/textgen/documents-customize) or see the [API reference](/api-reference/documents-classic/generate-document#sections-overridable)
#### Changes and deprecations to document and template endpoints
As we evolve the document generation endpoint and related GET templates/sections, we are announcing several changes and deprecations.
Find the full details including impact and timeline to shutdown in the [breaking changes changelog](/release-notes/changelog-upcoming)
#### New, improved documentation guardrails!
* We have re-architected our documentation guardrails to be faster and more accurate.
* Documentation guardrails are the by default automatic step that quality-controls the generated document to correct outputs ungrounded in the source input.
* In this re-architected, new approach, we in parallel for each section identify hallucinations and provide targeted fixes by reference (sentence/segment index), preventing mismatched edits and skipping ambiguous duplicates. The result is format-preserving precise, auditable corrections that deliver safer, more trustworthy clinical summaries.
Additionally, you can now at request level [disable documentation guardrails](/api-reference/documents-classic/generate-document#body-disable-guardrails) if you want to better understand their impact on quality, processing time and token usage.
#### Reduced token usage for text generation
We are now leveraging cached input tokens when generating documents or facts. This is part of a continuous effort to provide text gen capabilities by Corti's LLMs at even more competitive token consumption behaviour.
*Note: We plan to expose cached tokens as separate usage info response in the future.*
#### New endpoint to [extract facts](/api-reference/facts/extract-facts) from a provided text string input now available!
* so far [FactsR™](/textgen/factsr) was dependent on streaming audio over WSS to extract facts
* this new endpoint facilitates a REST endpoint as alternative to extract facts
The endpoint lives under the new /tools/ route:
* atomic endpoint, not part of interaction collection
* output is not stored into Corti database (zeroRetention is the only default)
Read more about the behaviour and use cases of this endpoint in our [guide](/textgen/factsr).
Update to [Generate Document](/api-reference/documents-classic/generate-document) functionality, including the following:
* Improve handling of sections that do not have content generated so that an empty string is returned instead of a placeholder (e.g., ``).
* Fix to issue causing json text to be included in note output.
* nfc: Improve logging and exception handling in LLM hallucination guardrails service.
New: Set a [zeroRetention header](/api-reference/documents-classic/generate-document#parameter-x-corti-retention-policy) in the request to disable storing of the generated document to the DB.
Updated endpoint tags in [API Reference](/api-reference/welcome) page to indicate those with limited availability (codes, alignment, classification, contextual, explainability). Endpoints labeled as `limited availability` are *not available* for public use at this time. Please [contact us](mailto:help@corti.ai) to learn more about these expert models and expected functionality to be generally released as AI tools.
#### Introducing a new foundation for clinical AI, built not just to document care, but to support it: FactsR™.
FactsR listens in real time, extracts clinical facts as the conversation unfolds, and helps clinicians stay in control, refining and approving facts, not sifting through hallucinated paragraphs.
Read the full announcement [here](https://www.corti.ai/stories/introducing-factsr-the-thinking-engine-behind-better-clinical-ai)!
#### New: Corti default templates
Leverage default, out-of-the-box templates and sections to accelerate launching ambient documentation flows.
The templates and sections cover a broad range of frequently used clinical note formats and are available in all languages Corti offers.
New [API endpoints](/api-reference/documents-classic/list-documents) to both list templates, sections and retrieve individual templates and sections are launched.
Learn more how to leverage those templates [here](/textgen/templates-standard)!
Announcing the launch of Corti AI Platform. Read more about it [here](https://www.corti.ai/)
# AI SDK Adapter
Source: https://docs.corti.ai/sdk/ai-sdk-adapter/overview
Adapter for integrating Corti A2A agents with Vercel AI SDK
The `@corti/ai-sdk-adapter` package provides utilities to connect Corti's [A2A (Agent-to-Agent)](/agentic/a2a-protocol) agents with the [Vercel AI SDK](https://sdk.vercel.ai/docs). It converts between AI SDK's UI message format and Corti's A2A format, so you can use familiar patterns like `useChat` to build chat interfaces powered by Corti agents.
**Package:** [@corti/ai-sdk-adapter on npm](https://www.npmjs.com/package/@corti/ai-sdk-adapter) | **Source:** [GitHub](https://github.com/corticph/ai-sdk-adapter)
The adapter provides three main functions:
* **`convertToParams()`** -- converts `CortiUIMessage[]` to A2A `MessageSendParams`
* **`toUIMessageStream()`** -- converts an A2A stream to a UI message stream
* **`createA2AClientFactory()`** -- creates an A2A client factory configured with Corti authentication
## Installation
```bash theme={null}
npm install @corti/ai-sdk-adapter @a2a-js/sdk ai
```
## Quick start
### Server: streaming API route (Next.js)
Create an API route that receives messages from the client, converts them to A2A format, streams the response from a Corti agent, and returns a UI message stream.
```ts title="app/api/chat/route.ts" theme={null}
import {
convertToParams,
toUIMessageStream,
createA2AClientFactory,
} from '@corti/ai-sdk-adapter';
import type { CortiUIMessage, ExpertCredential } from '@corti/ai-sdk-adapter';
import { CortiClient } from '@corti/lib';
import { createUIMessageStreamResponse } from 'ai';
export async function POST(req: Request) {
const { messages }: { messages: CortiUIMessage[] } = await req.json();
// Optional: define credentials for MCP servers
const credentials: ExpertCredential[] = [
{
mcp_name: 'my-server',
token: process.env.MCP_TOKEN,
type: 'bearer' as const,
},
];
// Build A2A params from UI messages
const params = convertToParams(messages, credentials);
// Create A2A client factory and send message stream
const corti = new CortiClient({ /* your Corti client config */ });
const factory = createA2AClientFactory(corti);
const agentUrl = await corti.agents.getCardUrl("YOUR_AGENT_ID");
const client = factory.createFromUrl(agentUrl.toString(), '');
const a2aStream = client.sendMessageStream(params);
// Convert to UI stream
const uiStream = toUIMessageStream(a2aStream, {
callbacks: {
onStart: () => console.log('Stream started'),
onEvent: (event) => console.log('Event:', event),
onFinish: (state) => console.log('Final state:', state),
onError: (error) => console.error('Error:', error),
},
});
return createUIMessageStreamResponse({ stream: uiStream });
}
```
### Client: React chat component
Use the Vercel AI SDK `useChat` hook with the `CortiUIMessage` type to render agent responses.
```tsx title="app/chat.tsx" theme={null}
import { useState } from 'react';
import { useChat } from 'ai/react';
import type { CortiUIMessage } from '@corti/ai-sdk-adapter';
export default function Chat() {
const [input, setInput] = useState('');
const { messages, sendMessage, status } = useChat({
api: '/api/chat',
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || status === "streaming" || status === "submitted") return;
sendMessage({ message: input });
setInput('');
};
return (
);
}
```
***
## How it works
### Context and task continuity
The adapter automatically manages conversation context and task continuity:
* **`contextId`** -- maintains conversation context across multiple messages. Automatically inferred from the last assistant message.
* **`taskId`** -- continues an existing task when the agent requires more input. Only included when the last assistant message has `state: 'input-required'`.
* **Credentials** -- only sent on the first message (when no `taskId` is present).
The `convertToParams()` function handles all of this automatically — you just pass the `messages` array from `useChat`.
### Stream callbacks
The `toUIMessageStream()` function accepts a `StreamConversionOptions` object with optional callbacks to monitor stream progress:
```ts theme={null}
const uiStream = toUIMessageStream(a2aStream, {
callbacks: {
onStart: () => {
// Called when streaming begins
},
onEvent: (event) => {
// Called on each new event from the stream
},
onFinish: (state) => {
// Called when stream completes with the final task status
},
onError: (error: Error) => {
// Called if an error occurs during streaming
},
onAbort: () => {
// Called when the stream is aborted by the client
},
},
});
```
***
## Runtime support
This package supports:
* Node.js (18+)
* Edge runtimes (Vercel Edge, Cloudflare Workers)
***
## Resources
* [Vercel AI SDK documentation](https://sdk.vercel.ai/docs)
* [A2A SDK documentation](https://github.com/a2aproject/a2a-js)
* [Corti Agentic Framework](/agentic/overview)
* [A2A protocol reference](/agentic/a2a-protocol)
# AI SDK Adapter - API Reference
Source: https://docs.corti.ai/sdk/ai-sdk-adapter/reference
Functions, types, and options exported by @corti/ai-sdk-adapter
Complete API documentation for the `@corti/ai-sdk-adapter` package.
## Functions
### `convertToParams()`
Converts an array of `CortiUIMessage` objects (from the Vercel AI SDK `useChat` hook) into A2A `MessageSendParams` ready to send to a Corti agent.
```ts theme={null}
function convertToParams(
messages: CortiUIMessage[],
credentials?: ExpertCredential[]
): MessageSendParams;
```
| Parameter | Type | Required | Description |
| :------------ | :------------------- | :------- | :--------------------------------- |
| `messages` | `CortiUIMessage[]` | Yes | The messages array from `useChat`. |
| `credentials` | `ExpertCredential[]` | No | Credentials for MCP servers. |
**Returns:** `MessageSendParams` — an A2A-compatible params object you can pass to `client.sendMessage()` or `client.sendMessageStream()`.
**Automatic behavior:**
* Extracts `contextId` from the last assistant message to maintain conversation context.
* Includes `taskId` only when the last assistant message has `state: 'input-required'`.
* Attaches `credentials` only on the first message.
***
### `toUIMessageStream()`
Converts an A2A streaming response into a UI message stream compatible with the Vercel AI SDK's `createUIMessageStreamResponse()`.
```ts theme={null}
function toUIMessageStream(
a2aStream: AsyncIterable,
options?: StreamConversionOptions
): ReadableStream;
```
| Parameter | Type | Required | Description |
| :---------- | :--------------------------- | :------- | :--------------------------------------------------- |
| `a2aStream` | `AsyncIterable` | Yes | The stream returned by `client.sendMessageStream()`. |
| `options` | `StreamConversionOptions` | No | Configuration including lifecycle callbacks. |
**Returns:** `ReadableStream` — a stream you can pass to `createUIMessageStreamResponse({ stream })`.
***
### `createA2AClientFactory()`
Creates a factory for building A2A clients that are pre-configured with Corti authentication.
```ts theme={null}
function createA2AClientFactory(
cortiClient: CortiClient
): A2AClientFactory;
```
| Parameter | Type | Required | Description |
| :------------ | :------------ | :------- | :--------------------------------------------------------- |
| `cortiClient` | `CortiClient` | Yes | An authenticated `CortiClient` instance from `@corti/lib`. |
**Returns:** `A2AClientFactory` — a factory with a `createFromUrl(url, agentId)` method to build A2A clients.
**Usage:**
```ts theme={null}
import { createA2AClientFactory } from '@corti/ai-sdk-adapter';
import { CortiClient } from '@corti/lib';
const corti = new CortiClient({ /* config */ });
const factory = createA2AClientFactory(corti);
const agentUrl = await corti.agents.getCardUrl("YOUR_AGENT_ID");
const client = factory.createFromUrl(agentUrl.toString(), '');
```
***
### `createFetchImplementation()`
Creates a `fetch`-compatible function that automatically injects Corti authentication headers into every request. This is the lower-level primitive used internally by `createA2AClientFactory()`, but you can use it directly when you need a custom A2A client setup.
```ts theme={null}
function createFetchImplementation(
client: CortiClient
): (input: string | URL | Request, init?: RequestInit) => Promise;
```
| Parameter | Type | Required | Description |
| :-------- | :------------ | :------- | :--------------------------------------------------------- |
| `client` | `CortiClient` | Yes | An authenticated `CortiClient` instance from `@corti/sdk`. |
**Returns:** A `fetch`-compatible function `(input, init?) => Promise` that adds Corti authentication to outgoing requests.
**Usage:**
```ts theme={null}
import { createFetchImplementation } from '@corti/ai-sdk-adapter';
import { CortiClient } from '@corti/sdk';
import { ClientFactory } from '@a2a-js/sdk/client';
const corti = new CortiClient({ /* config */ });
const fetch = createFetchImplementation(corti);
// Use with A2A ClientFactory directly
const factory = new ClientFactory({ fetch });
```
***
## Types
### `CortiUIMessage`
Extends the standard Vercel AI SDK `UIMessage` with A2A-specific metadata and data parts.
```ts theme={null}
import type { CortiUIMessage } from '@corti/ai-sdk-adapter';
```
**Metadata fields:**
| Field | Type | Description |
| :---------- | :--------- | :---------------------------------------------------- |
| `contextId` | `string` | Conversation context identifier. |
| `taskId` | `string` | Task identifier for multi-turn interactions. |
| `history` | `object[]` | Conversation history from the A2A response. |
| `credits` | `number` | Credits consumed by this response. |
| `state` | `string` | Task state (e.g., `'completed'`, `'input-required'`). |
**Custom data parts:**
| Part discriminator | Type | Description |
| :------------------- | :------------------ | :---------------------------------- |
| `data-text` | `CortiTextPart` | Text content with an optional name. |
| `data-json` | `CortiJSONPart` | Structured JSON data with a name. |
| `data-status-update` | `CortiStatusUpdate` | Status update from the agent. |
***
### `CortiTextPart`
A text data part. Alias for `string`.
```ts theme={null}
type CortiTextPart = string;
```
***
### `CortiJSONPart`
A JSON data part. Alias for `JSONValue` from `@ai-sdk/provider`.
```ts theme={null}
type CortiJSONPart = JSONValue;
```
***
### `CortiStatusUpdate`
A status update emitted by the agent during streaming.
```ts theme={null}
type CortiStatusUpdate = {
state: string;
message?: string;
};
```
| Field | Type | Required | Description |
| :-------- | :------- | :------- | :-------------------------------------- |
| `state` | `string` | Yes | Current task state. |
| `message` | `string` | No | Optional human-readable status message. |
***
### `ExpertCredential`
Credentials for authenticating with MCP servers connected to a Corti agent.
```ts theme={null}
type ExpertCredential =
| {
mcp_name: string;
token: string;
type: 'bearer';
}
| {
mcp_name: string;
client_id: string;
client_secret: string;
type: 'oauth2.0';
};
```
| Field | Type | Description |
| :-------------- | :----------------------- | :------------------------------------------------- |
| `mcp_name` | `string` | Name of the MCP server to authenticate with. |
| `token` | `string` | Bearer token (when `type` is `'bearer'`). |
| `client_id` | `string` | OAuth client ID (when `type` is `'oauth2.0'`). |
| `client_secret` | `string` | OAuth client secret (when `type` is `'oauth2.0'`). |
| `type` | `'bearer' \| 'oauth2.0'` | Authentication method. |
***
### `StreamConversionOptions`
Options passed to `toUIMessageStream()` to configure stream behavior.
```ts theme={null}
interface StreamConversionOptions {
callbacks?: {
onStart?: () => void;
onEvent?: (event: StreamEvent) => void;
onFinish?: (state: TaskStatus) => void;
onError?: (error: Error) => void;
onAbort?: () => void;
};
}
```
**Callback reference:**
| Callback | Parameters | Description |
| :--------- | :------------------- | :----------------------------------------------------------- |
| `onStart` | — | Called when streaming begins. |
| `onEvent` | `event: StreamEvent` | Called on each new event from the A2A stream. |
| `onFinish` | `state: TaskStatus` | Called when the stream completes with the final task status. |
| `onError` | `error: Error` | Called if an error occurs during streaming. |
| `onAbort` | — | Called when the stream is aborted by the client. |
# Authentication
Source: https://docs.corti.ai/sdk/ambient/authentication
Access tokens and automatic token refresh for the Ambient Web Component
The Ambient Web Component requires authentication to connect to the Corti Ambient API. You provide an access token directly — the component does **not** handle OAuth internally.
The component is automatically hidden until authentication is provided. Once you set `accessToken` or `authConfig`, it becomes visible.If you're using [proxying](/sdk/ambient/proxy) (`socketUrl` or `socketProxy`), authentication is handled by your proxy — `accessToken` and `authConfig` are ignored.
## Using `accessToken`
Set it as an HTML attribute or JavaScript property. You must update it manually before it expires:
```html theme={null}
```
```js theme={null}
ambient.accessToken = freshToken;
```
This approach is suitable for short-lived sessions or prototyping. For production, use `authConfig` with automatic refresh.
If you're connecting without a proxy, prefer [scoped tokens](/sdk/js/authentication#scoped-tokens) so the frontend token can only access the WebSocket endpoint you need. When requesting tokens directly from Corti Auth (no SDK), use `scope="openid streams"` for the `/streams` endpoint — see [Security best practices](/authentication/security_best_practices#4-if-you-must-use-tokens-in-special-cases-use-limited-scope-credentials).If you need to obtain an `accessToken`, you can use the [Corti JavaScript SDK authentication flows](/sdk/js/authentication) and then pass the resulting token into `accessToken` or `authConfig`.
## Using `authConfig` (recommended)
Use `authConfig` when you want to supply token metadata and (optionally) enable automatic refresh. If you provide a `refreshAccessToken` callback, the component calls it automatically when the token is about to expire:
```js theme={null}
const ambient = document.querySelector("corti-ambient");
ambient.authConfig = {
accessToken: "",
expiresIn: 300,
refreshToken: "",
refreshAccessToken: async ({ refreshToken }) => {
const res = await fetch("/api/token/refresh");
const data = await res.json();
return {
accessToken: data.access_token,
expiresIn: data.expires_in,
refreshToken: data.refresh_token,
};
},
};
```
| Field | Type | Required | Description |
| :------------------- | :------------------------------ | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessToken` | `string` | No | Initial access token. If omitted, the component can only obtain a token if you also provide `refreshAccessToken`. |
| `expiresIn` | `number` | No | Token lifetime in seconds. Used to schedule the next refresh. |
| `refreshToken` | `string` | No | Refresh token passed to `refreshAccessToken` when a refresh is needed (if you provide the callback). |
| `refreshAccessToken` | `({ refreshToken }) => Promise` | No | Optional callback for automatic refresh. Returns `{ accessToken, expiresIn?, refreshToken? }`. Without it, you must update `accessToken` yourself before expiry. |
## How token refresh works
Under the hood, Ambient Web Component uses Corti JavaScript SDK to connect. See [How token refresh works](/sdk/js/authentication#how-token-refresh-works) for the full behavior.
```js theme={null}
const ambient = document.querySelector("corti-ambient");
ambient.authConfig = {
refreshAccessToken: async ({ refreshToken }) => {
const res = await fetch("/api/token/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
});
const data = await res.json();
return {
accessToken: data.access_token,
expiresIn: data.expires_in,
refreshToken: data.refresh_token,
};
},
};
```
1. **Initial token** -- If `accessToken` is provided in `authConfig`, it's used immediately. Otherwise, `refreshAccessToken` is called with `undefined` to obtain the first token.
2. **Automatic refresh** -- The component monitors the token's expiration and calls `refreshAccessToken` before it expires. The `refreshToken` parameter will be:
* `undefined` on the first call (if no initial `refreshToken` was provided)
* The `refreshToken` returned from the previous refresh call
3. **Seamless operation** -- Refresh happens in the background. Active ambient sessions continue without interruption.
## Using with modular components
When using individual components, set `authConfig` on ``:
```html theme={null}
```
## See also
* [API Reference](/sdk/ambient/reference) -- full property tables for all components
* [Proxy Guide](/sdk/ambient/proxy) -- route traffic through your own server (auth handled by proxy)
* [Examples](https://github.com/corticph/corti-examples/tree/main/ambient) -- integration examples
# Ambient Web Component
Source: https://docs.corti.ai/sdk/ambient/overview
Drop-in web component for real-time multi-speaker ambient streaming
The `@corti/ambient-web` package provides a set of custom elements that handle microphone management, audio streaming, transcript delivery, and facts on top of the Corti API. It works with any frontend framework or plain HTML.
**Package:** [@corti/ambient-web on npm](https://www.npmjs.com/package/@corti/ambient-web) | **Examples:** [corti-examples/ambient](https://github.com/corticph/corti-examples/tree/main/ambient)
The library provides two usage modes:
1. **``** -- opinionated, all-in-one component with built-in UI (recommended for most use cases)
2. **Modular components** -- individual building blocks (``, ``, etc.) for fully custom layouts
Looking for single-speaker dictation with voice commands? See the [Dictation Web Component](/sdk/dictation/overview).
OAuth 2.0 authentication is not handled by this library. The client must provide an authorization token or token refresh function while using the component. Each ambient session also requires an `interactionId`.
## Installation
```bash npm theme={null}
npm install @corti/ambient-web
```
```bash yarn theme={null}
yarn add @corti/ambient-web
```
```bash pnpm theme={null}
pnpm add @corti/ambient-web
```
```html CDN theme={null}
```
### Module import
```ts theme={null}
// Side-effect import -- registers all custom elements
import "@corti/ambient-web";
// Named imports -- access component classes directly
import { CortiAmbient } from "@corti/ambient-web";
```
## Quick start
```html theme={null}
Corti Ambient
```
The component handles microphone permissions, device selection, and audio streaming automatically. You only need to provide authentication, an interaction ID, and listen for events.
***
## Configuration
Set `ambientConfig` as a JavaScript property to configure the stream. The type matches the [`StreamConfig`](/api-reference/streams) from the Streams WebSocket API.
When using `mode.type: "facts"`, you must also set `mode.outputLocale` (the language for extracted facts).
```js theme={null}
const ambient = document.querySelector("corti-ambient");
ambient.ambientConfig = {
transcription: { primaryLanguage: "en" },
mode: { type: "facts", outputLocale: "en" },
};
```
The `interactionId` property is **required** — it identifies the session for the Streams API.
See the [Streams API Reference](/api-reference/streams) for the full configuration schema.
***
## Modular components
For custom UI layouts, use individual components inside an `` parent:
All modular components **require** an `` parent to provide context. They cannot be used standalone.
```html theme={null}
```
| Component | Description |
| :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `` | Context provider. Same properties as `` (auth, config, interactionId, virtualMode, devices, keybindings). Add `noWrapper` attribute to remove default styling. |
| `` | Start/stop button with audio visualization. Supports `allowButtonFocus`. Has `startRecording()`, `stopRecording()`, `toggleRecording()`, `openConnection()`, `closeConnection()` methods. Requires `interactionId` on the root. |
| `` | Settings panel with device, language, and virtual mode selectors. Supports `settingsEnabled`. |
| `` | Standalone device dropdown. Supports `disabled`. |
| `` | Standalone language dropdown. Supports `disabled`. |
| `` | Keybinding configuration. Supports `keybindingType` (`"push-to-talk"` or `"toggle-to-talk"`) and `disabled`. |
| `` | Virtual mode toggle. Supports `disabled`. |
See the [API Reference](/sdk/ambient/reference) for full per-component property and method tables.
***
## Virtual mode
Virtual mode captures tab/window/app audio mixed with microphone input. Enable it via the `virtualMode` attribute or property, or let users toggle it from the settings menu when `settingsEnabled` includes `"virtualMode"` (included by default on ``).
***
## Keyboard shortcuts
The component supports two keybinding modes:
| Mode | Behavior | Default key |
| :----------------- | :---------------------------------- | :---------- |
| **Push-to-talk** | Hold key to record, release to stop | `Space` |
| **Toggle-to-talk** | Press to start, press again to stop | `Enter` |
Configure via attributes:
```html theme={null}
```
Keys are specified as `event.key` names (e.g. `"Space"`, `"k"`, `"Meta"`) or `event.code` values (e.g. `"KeyK"`, `"Backquote"`). Modifier combinations are not supported.
When both keybindings are set to the same key, toggle-to-talk takes priority.
### Preventing keybinding activation
Use the `keybinding-activated` event to conditionally block keybindings:
```js theme={null}
const ambient = document.querySelector("corti-ambient");
ambient.addEventListener("keybinding-activated", (event) => {
if (document.activeElement.tagName === "TEXTAREA") {
event.preventDefault(); // Don't trigger recording while typing
}
});
```
***
## Attribute formatting
| Type | Format | Example |
| :------ | :------------------------------- | :---------------------------------------------- |
| Boolean | Presence = true, absence = false | `` |
| String | Attribute value | `accessToken="token"` |
| Array | Comma-separated | `settingsEnabled="device,language,virtualMode"` |
| Object | JavaScript property only | `ambient.ambientConfig = { ... }` |
***
## Resources
* **[npm package](https://www.npmjs.com/package/@corti/ambient-web)** -- latest version and install info
* **[Examples](https://github.com/corticph/corti-examples/tree/main/ambient)** -- integration examples
* **[API Reference](/sdk/ambient/reference)** -- properties, methods, and events for every component
* **[Authentication Guide](/sdk/ambient/authentication)** -- access tokens and automatic refresh
* **[Proxy Guide](/sdk/ambient/proxy)** -- route WebSocket traffic through your own server
* **[Styling Guide](/sdk/ambient/styling)** -- CSS custom properties and theming
* **[Streams API Reference](/api-reference/streams/)** -- underlying WebSocket API specification
***
For support or questions, reach out through [help.corti.app](https://help.corti.app)
# Proxy Guide
Source: https://docs.corti.ai/sdk/ambient/proxy
Route Ambient Web Component WebSocket traffic through your own server
Instead of connecting directly to Corti, you can route the WebSocket through your own proxy. When `socketUrl` or `socketProxy` is set, `accessToken` and `authConfig` are ignored — your proxy handles authentication.
## Why proxy?
When using **Client Credentials** authentication, the token is a service-account token with access to **all** data within the same API Client. Exposing it in a browser means any user could access any other user's data.
**Best practice:** use the SDK on the backend only, and call your own backend endpoints from the frontend. If you need the SDK in the browser, proxy through your server so credentials never leave the backend.
If proxying is not an option, consider [scoped tokens](/sdk/js/authentication#scoped-tokens) to restrict what a frontend token can access.
## Using `socketUrl`
Point the component at your proxy endpoint:
```html theme={null}
```
## Using `socketProxy`
For additional control over subprotocols and query parameters:
```js theme={null}
const ambient = document.querySelector("corti-ambient");
ambient.socketProxy = {
url: "wss://your-proxy.com/corti/stream",
protocols: ["your-protocol"],
queryParameters: { interactionId: "" },
};
```
| Field | Type | Required | Description |
| :---------------- | :--------- | :------- | :----------------------------------- |
| `url` | `string` | Yes | WebSocket proxy URL |
| `protocols` | `string[]` | No | WebSocket subprotocols |
| `queryParameters` | `object` | No | Query parameters appended to the URL |
## Using with modular components
Set `socketUrl` or `socketProxy` on ``:
```html theme={null}
```
## See also
* [Authentication](/sdk/ambient/authentication) -- direct authentication (no proxy)
* [API Reference](/sdk/ambient/reference) -- full property tables for all components
# Ambient Web Component - API Reference
Source: https://docs.corti.ai/sdk/ambient/reference
Properties, methods, and events for every Ambient Web Component
Complete API documentation for all components in the Corti Ambient Web Component library.
## Component Overview
The library provides two ways to use components:
1. **``** - Opinionated, all-in-one component (recommended for most use cases)
2. **Modular Components** - Individual components for custom UI implementations
Modular components (`ambient-recording-button`, `ambient-settings-menu`, `ambient-device-selector`, `ambient-language-selector`, `ambient-keybinding-selector`, `ambient-virtual-mode-selector`) **require** a `` parent component to provide context. They cannot be used standalone.
## Events
Events should be subscribed to on `` or `` components only. Individual modular components do not dispatch these events directly.
All events bubble and can be listened to on the root component:
| Event | Description | Detail |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ready` | Fired once the component is ready. | No detail |
| `recording-state-changed` | Fired when the recording state changes. | `detail.state` (string): The new recording state. One of: `"stopped"`, `"recording"`, `"initializing"`, `"stopping"` `detail.connection` (string, optional): WebSocket connection state. One of: `"CONNECTING"`, `"OPEN"`, `"CLOSING"`, `"CLOSED"`, or `null` `detail.processing` (boolean, optional): Whether audio is still being processed by the server. When `true`, the server is still processing previously sent audio and may send back additional transcript events even after recording has stopped |
| `recording-devices-changed` | Fired when the user switches recording devices or the list of recording devices changes. | `detail.devices` (MediaDeviceInfo\[]): Full list of available recording devices `detail.selectedDevice` (MediaDeviceInfo): Currently selected device |
| `languages-changed` | Fired when the list of available languages changes or a language is selected. | `detail.languages` (string\[]): Array of available language codes `detail.selectedLanguage` (string): Currently selected language code |
| `transcript` | Fired when new transcript segment(s) are received. | `detail.type` (string): Always `"transcript"` `detail.data` (array): Transcript segments. Each segment includes `transcript` (string), `time.start` / `time.end` (number), `speakerId` (number), and `participant.channel` (number) |
| `facts` | Fired when new facts are received. | `detail.type` (string): Always `"facts"` `detail.fact` (array): Fact items with `text` (string), `group` (string), `id` (string), `source` (string), and `isDiscarded` (boolean) |
| `audio-level-changed` | Fired when the input audio level changes. | `detail.audioLevel` (number): The current audio level, ranging from 0 to 1 |
| `audio-event` | Fired when an audio health event is received from the server (e.g. silence detection, noise level warnings). | `detail.type` (string): Always `"audioEvent"` `detail.event` (string): The audio event type (e.g. `"speechQualityIssueDetected"`, `"speechQualityIssueRecovered"`, `"longSilenceDetected"`, `"longSilenceRecovered"`) `detail.message` (string, optional): Human-readable description of the event |
| `usage` | Fired when usage information is received from the server. | `detail.type` (string): Always `"usage"` `detail.credits` (number): The amount of credits used for this stream |
| `delta-usage` | Fired when incremental usage information is received while the stream is active. | `detail.type` (string): Always `"delta_usage"` `detail.credits` (number): Approximate credits consumed since recording started |
| `network-activity` | Fired when network activity occurs (data sent or received). | `detail.direction` (string): Direction of network activity, either `"sent"` or `"received"` `detail.data` (unknown): The data that was sent or received |
| `error` | Fired on error. | `detail.message` (string): Error message describing what went wrong |
| `keybinding-changed` | Fired when the keybinding configuration changes. | `detail.key` (string \| null \| undefined): The raw key from the keyboard event (e.g., `"k"`, `"Meta"`, ``"`"``) `detail.code` (string \| null \| undefined): The raw code from the keyboard event (e.g., `"KeyK"`, `"MetaLeft"`, `"Backquote"`) `detail.keybinding` (string \| null): The normalized keybinding value displayed in the UI (e.g., `"k"`, `"Cmd"` on Mac, `"Space"`) `detail.type` (string \| undefined): The type of keybinding, either `"push-to-talk"` or `"toggle-to-talk"` |
| `keybinding-activated` | Fired when a keybinding is activated (key pressed). This event is cancelable - call `event.preventDefault()` to prevent the keybinding from triggering recording. | `detail.keyboardEvent` (KeyboardEvent): The original keyboard event that triggered the keybinding. |
| `virtual-mode-changed` | Fired when virtual mode is toggled. | `detail.enabled` (boolean): Whether virtual mode is now enabled |
***
## ``
The main opinionated component that includes all functionality with a built-in UI.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ------------------------ | ---------------------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `accessToken` | String | `accessToken` | ✅ | - | ✅\* | Latest access token for authentication. Required if no other authentication or proxy methods provided. |
| `authConfig` | Object | - | ✅ | - | ✅\* | Authentication configuration with optional refresh mechanism. Must be set via JavaScript property. Required if no other authentication or proxy methods provided. |
| `socketUrl` | String | `socketUrl` | ✅ | - | ✅\* | WebSocket URL for proxy connection. When provided, uses proxy client instead of direct client. Required if no other authentication or proxy methods provided. |
| `socketProxy` | Object | - | ✅ | - | ✅\* | Socket proxy configuration object. Must be set via JavaScript property. Required if no other authentication or proxy methods provided. |
| `interactionId` | String | `interactionId` | ✅ | - | ✅ | Interaction ID for the ambient session. Required before starting recording. |
| `devices` | Array | - | ✅ | `auto-loaded` | ❌ | List of all available recording devices (MediaDeviceInfo\[]). Auto-loaded asynchronously from browser's audio input devices. Must be set via JavaScript property. |
| `selectedDevice` | Object | - | ✅ | - | ❌ | The selected device used for recording (MediaDeviceInfo). Must be set via JavaScript property. |
| `recordingState` | String | - | ❌ | `"stopped"` | ❌ | Current state of recording: `"stopped"`, `"recording"`, `"initializing"`, `"stopping"`. |
| `ambientConfig` | [`StreamConfig`](/api-reference/streams) | - | ✅ | `{ mode: { type: "facts", outputLocale: "en" }, transcription: { primaryLanguage: "en" } }` | ❌ | Stream configuration. Must be set via JavaScript property. For `type: "facts"`, `outputLocale` is required. |
| `virtualMode` | Boolean | `virtualMode` | ✅ | `false` | ❌ | Enables virtual mode — captures tab/window/app audio mixed with microphone input. |
| `settingsEnabled` | String\[] | `settingsEnabled` | ✅ | `["device", "language", "virtualMode"]` | ❌ | Which settings should be available in the UI. If empty, settings are disabled. Options: `"device"`, `"language"`, `"keybinding"`, `"virtualMode"`. |
| `languagesSupported` | String\[] | `languagesSupported` | ✅ | `LANGUAGES_SUPPORTED_EU` or `LANGUAGES_SUPPORTED_US` | ❌ | List of all language codes available. Auto-loaded based on region (EU or US). |
| `pushToTalkKeybinding` | String | `pushToTalkKeybinding` | ✅ | `"Space"` (if in settingsEnabled) | ❌ | Push-to-talk keyboard shortcut. Keydown starts recording, keyup stops recording. Single key only. **Note:** If both keybindings are set to the same key, toggle-to-talk takes priority. |
| `toggleToTalkKeybinding` | String | `toggleToTalkKeybinding` | ✅ | `"Enter"` (if in settingsEnabled) | ❌ | Toggle-to-talk keyboard shortcut. Pressing the key toggles recording on/off. Single key only. **Note:** If both keybindings are set to the same key, toggle-to-talk takes priority. |
| `allowButtonFocus` | Boolean | `allowButtonFocus` | ✅ | `false` | ❌ | When `false` (default), prevents the start/stop button from taking focus when clicked. Set to `true` to allow focus. |
### Methods
| Method | Description |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startRecording()` | Starts a recording. Requires `interactionId` to be set. |
| `stopRecording()` | Stops a recording. |
| `toggleRecording()` | Starts or stops recording. Convenience method. |
| `openConnection()` | Opens the WebSocket connection to the server without starting recording. **Default behavior:** The connection opens automatically when the user clicks to start recording for the first time. **When to use:** Pre-establish the connection before recording starts (e.g., to reduce first-record latency). Can only be called when recording is stopped and all audio processing is complete. Returns a Promise. |
| `closeConnection()` | Properly closes the WebSocket connection by sending an `"end"` message and waiting for the `"ended"` response. **Default behavior:** The connection opens automatically on first recording and remains open after recording stops for reuse. **When to use:** Receive `"usage"` stats (between `"end"` and `"ended"`), or when you're completely done (unmount, navigation, freeing resources). Can only be called when recording is stopped and all audio processing is complete. Returns a Promise. |
***
## ``
Context provider component that manages authentication, configuration, and shared state. Required parent for all modular components.
All modular components must be children of `` to receive context.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ------------------------ | ---------------------------------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `accessToken` | String | `accessToken` | ✅ | - | ✅\* | Latest access token for authentication. Required if no other authentication or proxy methods provided. |
| `authConfig` | Object | - | ✅ | - | ✅\* | Authentication configuration with optional refresh mechanism. Must be set via JavaScript property. Required if no other authentication or proxy methods provided. |
| `socketUrl` | String | `socketUrl` | ✅ | - | ✅\* | WebSocket URL for proxy connection. When provided, uses proxy client instead of direct client. Required if no other authentication or proxy methods provided. |
| `socketProxy` | Object | - | ✅ | - | ✅\* | Socket proxy configuration object. Must be set via JavaScript property. Required if no other authentication or proxy methods provided. |
| `interactionId` | String | `interactionId` | ✅ | - | ✅ | Interaction ID for the ambient session. Required before starting recording. |
| `ambientConfig` | [`StreamConfig`](/api-reference/streams) | - | ✅ | `{ mode: { type: "facts", outputLocale: "en" }, transcription: { primaryLanguage: "en" } }` | ❌ | Stream configuration. Must be set via JavaScript property. |
| `virtualMode` | Boolean | `virtualMode` | ✅ | `false` | ❌ | Enables virtual mode — captures tab/window/app audio mixed with microphone input. |
| `languages` | String\[] | `languages` | ✅ | `LANGUAGES_SUPPORTED_EU` or `LANGUAGES_SUPPORTED_US` | ❌ | List of all language codes available. Auto-loaded based on region (EU or US). |
| `devices` | Array | - | ✅ | `auto-loaded` | ❌ | List of all available recording devices (MediaDeviceInfo\[]). Auto-loaded asynchronously from browser's available audio input devices. Must be set via JavaScript property. |
| `selectedDevice` | Object | - | ✅ | - | ❌ | The selected device used for recording (MediaDeviceInfo). Must be set via JavaScript property. |
| `recordingState` | String | - | ❌ | `"stopped"` | ❌ | Current state of recording: `"stopped"`, `"recording"`, `"initializing"`, `"stopping"`. |
| `pushToTalkKeybinding` | String | `pushToTalkKeybinding` | ✅ | `"Space"` (if in settingsEnabled) | ❌ | Push-to-talk keyboard shortcut. Keydown starts recording, keyup stops recording. Single key only (e.g., `"Space"`, `"k"`, `"meta"`, `"ctrl"`, `"KeyK"`, `"Space"`). Supports both key names (from `event.key`) and key codes (from `event.code`). Combinations are not supported. **Note:** If both keybindings are set to the same key, toggle-to-talk takes priority. |
| `toggleToTalkKeybinding` | String | `toggleToTalkKeybinding` | ✅ | `"Enter"` (if in settingsEnabled) | ❌ | Toggle-to-talk keyboard shortcut. Pressing the key toggles recording on/off. Single key only (e.g., `` ` ``, `"k"`, `"meta"`, `"ctrl"`, `"KeyK"`, `"Backquote"`). Supports both key names (from `event.key`) and key codes (from `event.code`). Combinations are not supported. **Note:** If both keybindings are set to the same key, toggle-to-talk takes priority. |
| `noWrapper` | Boolean | `noWrapper` | ✅ | `false` | ❌ | When `true`, removes the default wrapper styling. Useful for custom layouts. |
***
## ``
Standalone recording button component with audio visualization.
Must be a child of ``.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ------------------ | ------- | ------------------ | -------- | ------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `allowButtonFocus` | Boolean | `allowButtonFocus` | ✅ | `false` | ❌ | When `false` (default), prevents the button from taking focus when clicked. Set to `true` to allow focus. |
### Methods
| Method | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `startRecording()` | Starts a recording. |
| `stopRecording()` | Stops a recording. |
| `toggleRecording()` | Starts or stops recording. Convenience method. |
| `openConnection()` | Opens the WebSocket connection to the server without starting recording. **Default behavior:** Opens automatically on the first user action to start recording. **When to use:** Pre-establish before recording starts (e.g., reduce first-record latency). Can only be called when recording is stopped and all audio processing is complete. Returns a Promise. |
| `closeConnection()` | Properly closes the WebSocket connection by sending an "end" message and waiting for the "ended" response. **Default behavior:** The connection opens automatically on first recording and remains open after recording stops for reuse. **When to use:** Receive "usage" stats (between "end" and "ended"), or when you're completely done (unmount, navigation, freeing resources). Can only be called when recording is stopped and all audio processing is complete. Returns a Promise. |
***
## ``
Settings menu component with device, language, and virtual mode selectors.
Must be a child of ``.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ----------------- | --------- | ----------------- | -------- | --------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `settingsEnabled` | String\[] | `settingsEnabled` | ✅ | `["device", "language", "virtualMode"]` | ❌ | Which settings should be available. Options: `"device"`, `"language"`, `"keybinding"`, `"virtualMode"`. |
***
## ``
Device selection dropdown component.
Must be a child of ``.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ---------- | ------- | ---------- | -------- | ------- | -------- | ------------------------------------------ |
| `disabled` | Boolean | `disabled` | ✅ | `false` | ❌ | When `true`, disables the device selector. |
***
## ``
Language selection dropdown component.
Must be a child of ``.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ---------- | ------- | ---------- | -------- | ------- | -------- | -------------------------------------------- |
| `disabled` | Boolean | `disabled` | ✅ | `false` | ❌ | When `true`, disables the language selector. |
***
## ``
Keybinding configuration component for setting keyboard shortcuts. Supports both push-to-talk and toggle-to-talk keybindings.
Must be a child of ``.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ---------------- | ------- | ---------------- | -------- | ------------------ | -------- | ------------------------------------------------------------------------------------ |
| `keybindingType` | String | `keybindingType` | ✅ | `"toggle-to-talk"` | ❌ | The type of keybinding to configure. One of: `"push-to-talk"` or `"toggle-to-talk"`. |
| `disabled` | Boolean | `disabled` | ✅ | `false` | ❌ | When `true`, disables the keybinding selector. |
***
## ``
Virtual mode toggle component.
Must be a child of ``.
### Properties
| Property | Type | Attribute | Writable | Default | Required | Description |
| ---------- | ------- | ---------- | -------- | ------- | -------- | ------------------------------------------------ |
| `disabled` | Boolean | `disabled` | ✅ | `false` | ❌ | When `true`, disables the virtual mode selector. |
***
## Attribute Formatting
When using attributes (instead of JavaScript properties), follow these guidelines:
* **Boolean attributes**: Use the attribute name to set `true`, omit it to set `false`
```html theme={null}
```
* **String attributes**: Use the attribute value directly
```html theme={null}
```
* **Array attributes**: Use comma-separated values
```html theme={null}
```
* **Object/Complex types**: Must be set via JavaScript properties, not attributes
```javascript theme={null}
ambient.authConfig = { refreshAccessToken: async () => {...} };
ambient.ambientConfig = { transcription: { primaryLanguage: 'en' }, mode: { type: 'facts', outputLocale: 'en' } };
```
## See also
* [Overview](/sdk/ambient/overview) -- installation, quickstart, and usage modes
* [Authentication](/sdk/ambient/authentication) -- access tokens and automatic refresh
* [Proxy](/sdk/ambient/proxy) -- route WebSocket traffic through your own server
* [Styling](/sdk/ambient/styling) -- CSS custom properties and theming
# Styling Guide
Source: https://docs.corti.ai/sdk/ambient/styling
The **Corti Ambient Web Component** provides various CSS variables for customization. You can override these variables to match your design system.
## Overriding Dark/Light Theme
By default, the UI uses the system color-scheme (dark or light). This may not suit your application though, so you can force either dark or light using the `color-scheme` CSS property. For example, the styles below will force light mode.
```css theme={null}
corti-ambient {
color-scheme: light;
}
```
## Direct CSS Properties
Some CSS properties can be set directly on the `corti-ambient` component and will affect the component's `:host` element, similar to how `color-scheme` works. These include inherited properties that affect the host:
```css theme={null}
corti-ambient {
color-scheme: light;
font-family: 'Arial', sans-serif; /* Affects :host font-family */
font-size: 14px; /* Affects :host font-size */
color: #333; /* Affects :host color */
}
```
**Note:** While these properties can be set directly, the component primarily uses CSS custom properties (documented below) for styling, which provides more control and is the recommended approach.
## Using CSS Variables
For more control in customization of the UI component, you can redefine the variables inside a global CSS file or within a `