> For the complete documentation index, see [llms.txt](https://docs.concurrence.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.concurrence.com/developer-guide/platform-api/conversations/calls.md).

# Calls

Start outbound voice calls with use-case service routing, startup instructions, retry correlation, and best-effort patient lineage.

The outbound call endpoint lets you place a voice call from a workspace phone number to a patient. The request validates a patient entity and includes a reason; after the voice service accepts the call, the Platform API makes a best-effort attempt to emit lineage metadata on that entity.

The request requires a workspace API key, `phone_to`, `use_case_id`, `reason`, exactly one patient identifier, and a valid outbound voice setup. The service is resolved from the use case's workspace binding. No trigger or campaign resource is required for an on-demand call.

{% hint style="info" %}
**On-demand and scheduled calls.** This endpoint is for on-demand calls - a CRM event fires, a care coordinator clicks a button, or your backend decides a patient needs a callback right away. Use this endpoint for every outbound call, including calls your own scheduler places, and apply your own consent, retry, pacing, and business-hours policy before calling it.
{% endhint %}

## Quick Start

```bash
curl -X POST https://platform.api-us.concurrence.com/v1/{workspace_id}/calls/outbound \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_to": "+18005551234",
    "use_case_id": "cccccccc-0000-0000-0000-000000000001",
    "patient_entity_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "reason": "lab_results"
  }'
```

The response includes a `call_sid` for tracking. Treat it as an opaque platform call handle: the voice path returns a UUID-shaped reference, and other call records can carry carrier-style identifiers. Store and return the exact string; do not require a `CA` prefix or UUID syntax, or use it directly with a carrier API. It is distinct from the Runs API's `run_id`. When the best-effort lineage emit succeeds, `event_id` points to that world event; otherwise it is `null`:

```json
{
  "call_sid": "2c725f98-a9c6-4be2-8a48-6eb42d1f81cb",
  "status": "queued",
  "phone_from": "+12125559876",
  "setup_id": null,
  "event_id": "f9e8d7c6-b5a4-3210-fedc-ba0987654321"
}
```

***

## Endpoint

{% openapi src="<https://platform.api-us.concurrence.com/v1/openapi.json>" path="/v1/{workspace\_id}/calls/outbound" method="post" %}
<https://platform.api-us.concurrence.com/v1/openapi.json>
{% endopenapi %}

{% hint style="warning" %}
**`patient_entity_id` and `patient_canonical_id` are mutually exclusive.** Provide exactly one. Use `patient_entity_id` when you have the workspace entity UUID, or `patient_canonical_id` to reference the patient by world model canonical ID (`source:resource_type:id`).
{% endhint %}

`service_id` is not a field on this request. Configure and inspect the use case's [service binding](/developer-guide/platform-api/workspaces/use-cases.md); the Platform API validates start-time context against that bound service, and the voice runtime resolves its service through the same binding.

***

## How It Works

When you create an outbound call, the platform coordinates the initial request and voice runtime while exposing later states separately:

1. **Validation** - The Platform API verifies that the patient resolves to a projected `person` in the workspace.
2. **Voice preparation and dial** - The voice service validates the service and use case, selects an assigned caller ID, prepares the first agent audio, and then asks the carrier to create the call. Preparation or routing failure can reject the request before a usable call is returned.
3. **Connection** - On the platform-managed runtime, the agent says nothing while the phone rings. Once the callee answers, the call opens with the [AI and recording notice](/developer-guide/platform-api/conversations/voice-agent.md#call-opening) and the greeting. A voicemail that picks up counts as an answer, so the notice and greeting play to it; keep the greeting fit to leave on a voicemail. A dial that is busy, goes unanswered, or fails ends the call before the agent speaks. On either runtime, a call whose setup fails before the callee answers ends its dial before anyone answers, and a callee who has already answered hears the [safe exit](/developer-guide/platform-api/conversations/voice-agent.md#graceful-degradation). How soon the agent speaks after the callee answers depends on telephony and provider latency.
4. **Conversation** - The service's runtime determines available context, state-machine behavior, and tools. Outbound and inbound calls share infrastructure, but they are not contractually identical and do not universally expose every tool or clinical field.
5. **Completion** - Status and post-call artifacts become available as their operational stores and asynchronous analyses update.

The response's `status` is `queued`. Use the canonical [Runs](/developer-guide/platform-api/conversations/runs.md) inventory (`kind=conversation&channel=voice`) to browse call runs. The legacy `GET /calls` and `GET /calls/{call_id}` compatibility routes return `Deprecation: true` and `Sunset: 2026-07-01`; `POST /calls/outbound` remains current.

### Caller Turns Cut Off at Hangup <a href="#incomplete-caller-turns" id="incomplete-caller-turns"></a>

When a platform-managed call ends while the caller is still speaking and the agent still owes a reply, the call's turns keep what the caller was saying as a caller turn with `incomplete: true`. The turn shows what the caller said and that the agent never answered it. If the caller speaks again before any reply plays, each utterance is kept as its own turn. The flag appears on the turns of [call detail](/developer-guide/platform-api/conversations/voice-agent.md#compatibility-call-detail); a turn the agent answered has `incomplete: false`.

***

## Startup Instructions and Patient Identity

When you provide a `goal`, the Platform API turns it into a startup instruction. If `system_prompt` is also present, the goal is appended to that instruction before it is sent to the voice service. On the `amigo` runtime, this instruction takes precedence over task-derived greeting context; it does not replace the agent definition or Context Graph.

The patient identifier validates that a projected `person` exists in the workspace and anchors the best-effort `outbound.initiated` lineage event. The direct call request does not pass that entity ID to the voice service as a dedicated patient-context field. Any caller lookup or ambient context therefore depends on the configured service and the runtime's own identity-resolution path.

| You provide                                   | The agent gets                                                                             |
| --------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `patient_entity_id` or `patient_canonical_id` | Patient validation and best-effort lineage anchor; not a dedicated runtime context payload |
| `goal`                                        | Startup instruction formatted as a call objective                                          |
| `system_prompt`                               | Explicit startup instruction for the call                                                  |
| `system_prompt` + `goal`                      | Goal appended to the explicit startup instruction                                          |
| Neither `system_prompt` nor `goal`            | The service's normal outbound greeting and context behavior                                |

{% hint style="warning" %}
**A `gpt_realtime` service takes no startup instruction or outbound task.** A `gpt_realtime` call takes its instructions from the service, not from the request. When the use case's service runs on `gpt_realtime`, a request that carries `system_prompt`, `goal`, or `outbound_task_entity_id` returns `422` and nothing is dialed. For this refusal, `detail` is a message string rather than a list of validation errors, and it names the refused fields. Place the call without them, or set the service's `voice_config.session_provider` to `amigo` (see [Session Provider](/developer-guide/platform-api/workspaces/services.md#session-provider)). The `idempotency_key` is released, so you can resend the corrected request with the same key.
{% endhint %}

## Start-Time Tool Context

Use the request's `context` map for string and number values declared in the bound service's `accepted_context`. State-to-tool `context_bindings` select which parameters receive those values. Follow [Pass Context to Tool Parameters](/developer-guide/guides/conversation-context.md) for schema setup, received-input verification, and current runtime limits. On the live call, a tool bound to a declared key the request did not supply refuses to run until the key is set. To add a verified value after the call starts, see [Set Context on a Live Voice Call](/developer-guide/guides/live-call-context.md).

The map is separate from startup instructions (`goal` and `system_prompt`), correlation `metadata`, and patient-identity resolution. Supplying a patient identifier in the map does not establish that person's identity or preload their world-model data. Automatic `channel-id` also depends on the runtime path described in the guide.

***

## Phone Number Selection

Send `phone_to` as a valid international phone number in E.164 form. Validation checks the number, not just a leading `+` and digit count, so a placeholder with an invalid country or area code can be rejected. `phone_to` is the number the platform dials, so send the direct number: one with an extension, such as `+16095550199x123`, returns `422` and no call is placed. Accepted formatting does not prove reachability, ownership, or permission to call. Replace example numbers with an authorized test destination before dialing.

The caller ID is always resolved from the use case. The current selector chooses one phone number at random from the numbers assigned to that outbound-voice use case. The resolved number is returned as `phone_from`; the compatibility `setup_id` field is currently `null`.

```json
{
  "phone_to": "+18005551234",
  "use_case_id": "cccccccc-0000-0000-0000-000000000001",
  "patient_entity_id": "...",
  "reason": "appointment_reminder"
}
```

Response:

```json
{
  "call_sid": "5b96fb3f-d802-4c54-b90e-f794b099b120",
  "status": "queued",
  "phone_from": "+13105550199",
  "setup_id": null,
  "event_id": "..."
}
```

***

## Callback Chains

When a call is a follow-up to a previous conversation, pass `derived_from_call_sid` with the original call's identifier. When lineage emission succeeds, that identifier is stored in the new `outbound.initiated` event so your own queries can correlate the two calls. It does not create a world-model graph edge or prove that either call completed.

```json
{
  "phone_to": "+18005551234",
  "use_case_id": "cccccccc-0000-0000-0000-000000000001",
  "patient_entity_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "reason": "follow_up",
  "derived_from_call_sid": "2c725f98-a9c6-4be2-8a48-6eb42d1f81cb"
}
```

When the relevant initiation events are emitted and projected successfully, the patient's timeline can expose the call identifiers for correlation. Treat that timeline as asynchronous evidence of initiation, not as a guaranteed relationship view or confirmation that either call completed.

***

## External System Correlation

Use `tags` and `metadata` to connect calls to records in your other systems. When lineage emission succeeds, these values are stored in the `outbound.initiated` event. They remain queryable event data but are not guaranteed to appear in every built-in analytics view.

```json
{
  "phone_to": "+18005551234",
  "use_case_id": "cccccccc-0000-0000-0000-000000000001",
  "patient_entity_id": "...",
  "reason": "care_plan_review",
  "tags": ["chronic_care", "diabetes_management"],
  "metadata": {
    "ehr_encounter_id": "ENC-2026-5678",
    "crm_opportunity_id": "OPP-1234",
    "campaign": "annual_wellness_q2"
  }
}
```

Tags and metadata can support custom event filtering and joins back to source systems when the initiation event is available.

***

## Idempotency

Include an `idempotency_key` on requests that may need an ambiguous retry:

```bash
curl -X POST .../calls/outbound \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "phone_to": "+18005551234",
    "use_case_id": "cccccccc-0000-0000-0000-000000000001",
    "patient_entity_id": "...",
    "reason": "appointment_reminder",
    "idempotency_key": "reminder-apt-2026-1234"
  }'
```

A key stands for one call in your workspace for one hour. The key is claimed before anything is dialed:

* A retry after the call was placed returns the original response without dialing again.
* A retry while the first request is still placing the call returns `409`. Wait briefly and retry with the same key.
* The same key sent with a different `use_case_id`, `phone_to`, task entity, or `context` returns `409`. Use a new key for a different call.
* If the attempt fails before the callee's dial goes out, or the dial is refused, the attempt is cleaned up and the key is released, so a retry with the same key can place the call.
* Once the callee's dial goes out and isn't refused, the request returns the call with `status` `queued`, and a retry with the same key returns that call instead of dialing again. The request can also end in a `502` or `504` whose same-key retry returns the call.
* If the channel provider refuses the call, for example because the number can't be called, the request returns `422` and the key is released. Most refusals are permanent, so check the `detail` and fix the request before you retry it.
* If the platform cannot check the key, it returns `503` and dials nothing. Retry with the same key.

A `201` means the dial went out, or may have; it doesn't mean the phone rang or that anyone answered. After a `502`, `503`, or `504`, retry with the same key and the same request. A `504` means the call wasn't confirmed in time and may still be placed. The retry gets `409` while the first attempt is still being placed, the call's response once it is placed, and a new attempt when the first one placed nothing. Deduplication is best-effort, so a retry can occasionally place a second call.

A retry that replays the original response does not emit a second `outbound.initiated` event. If an outcome is still ambiguous, reconcile by `call_sid` or the Runs inventory before retrying. If you omit the key, the service generates one internally, so separate client retries do not share a deduplication key.

***

## World Model Lineage

After the voice service accepts the outbound call, the Platform API attempts to emit an `outbound.initiated` event on the patient entity. Emission is best-effort: failure is logged, does not cancel the accepted call, and leaves `event_id` as `null`. A successfully emitted event captures:

* **Who** requested the call - the API key identifier (not the secret)
* **Why** - the `reason` and `goal` you provided
* **What** external systems are involved - your `tags` and `metadata`
* **Where** it fits in a sequence - `derived_from_call_sid` if this is a callback

The `event_id` in the response points to this event. You can use it to:

* Correlate initiation with available downstream call records by `call_sid`
* Query the patient's timeline for available initiation events
* Build custom event analysis for API-initiated calls

Once the asynchronous world projection ingests the event, it can appear in the patient's timeline; read-after-write visibility is not immediate.

***

## Error Responses

| Status | When                                                                                                                                                                                                                                                                                                 | What to do                                                                                                                                                                                                                                                                                                                                   |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404`  | Patient entity does not exist in this workspace (or is not a person entity), the use case has no service binding, or no phone number is available for the `use_case_id`                                                                                                                              | Verify the `patient_entity_id` exists via `GET /v1/{workspace_id}/world/entities/{entity_id}`. For use case 404s, check the service binding and phone number provisioning.                                                                                                                                                                   |
| `409`  | A call with this `idempotency_key` is still being placed, or the key was already used for a different call                                                                                                                                                                                           | Retry shortly with the same key if the first request is still in flight. Use a new key for a different call.                                                                                                                                                                                                                                 |
| `422`  | Invalid E.164 phone number, invalid UUID, missing required field, invalid patient-identifier combination, or context rejected by the bound service; `system_prompt`, `goal`, or `outbound_task_entity_id` when the use case's service runs on `gpt_realtime`; or a call the channel provider refused | Supply `phone_to`, `use_case_id`, `reason`, exactly one patient identifier, and context matching the bound service schema. For a `gpt_realtime` service, leave out `system_prompt`, `goal`, and `outbound_task_entity_id`, or run the service on `amigo`. A `422` is final: check the `detail` and fix the request before you send it again. |
| `429`  | Rate limit exceeded                                                                                                                                                                                                                                                                                  | Back off and retry. The limit applies per API key and per route (scoped to `/calls/outbound`).                                                                                                                                                                                                                                               |
| `502`  | Placing the call failed, or the platform couldn't confirm whether it was placed                                                                                                                                                                                                                      | Retry with the same `idempotency_key`: the retry returns the call if it was placed and tries again if it wasn't. If the same `502` keeps returning, contact support.                                                                                                                                                                         |
| `503`  | The call could not be placed, or outbound calling is not configured for this workspace                                                                                                                                                                                                               | Retry with the same `idempotency_key`. If it persists, contact support to confirm outbound calling is enabled.                                                                                                                                                                                                                               |
| `504`  | The voice service did not respond in time; the call may still be placed                                                                                                                                                                                                                              | Retry with the same `idempotency_key`. A `409` means the first request is still placing the call; a later retry returns the call once it is placed, or tries again if the first attempt placed nothing.                                                                                                                                      |

***

## Rate Limits

The outbound call endpoint allows 1000 requests per minute, enforced per API key and per route. This limit is scoped to `/calls/outbound` only; other Platform API routes apply their own limits.

If you exceed the limit, you receive a `429` response. Retry with the same `idempotency_key`; the key is claimed before dialing, so a retry that overlaps the first request returns `409` instead of placing a second call.

Carrier-level constraints on individual phone numbers are enforced separately by the downstream voice carrier and are independent of this API rate limit.

***

## Full Example

A CRM webhook fires when a patient's lab results are ready. Your backend calls the outbound API to notify them:

```bash
curl -X POST https://platform.api-us.concurrence.com/v1/ea03a2a2-1234-5678-abcd-ef1234567890/calls/outbound \
  -H "Authorization: Bearer amigo_pk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "phone_to": "+14155550123",
    "use_case_id": "cccccccc-0000-0000-0000-000000000001",
    "patient_entity_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "reason": "lab_results",
    "goal": "Inform the patient that their A1C results are ready and discuss next steps for their diabetes management plan",
    "tags": ["lab_notification", "diabetes"],
    "metadata": {
      "ehr_order_id": "ORD-2026-7890",
      "lab_type": "A1C"
    },
    "idempotency_key": "lab-notify-ORD-2026-7890"
  }'
```

On the `amigo` runtime, this goal becomes the call's startup instruction. A `gpt_realtime` service refuses a request with a `goal`; see [Startup Instructions and Patient Identity](#startup-instructions-and-patient-identity). Patient-context availability depends on the service's runtime and tools, so validate the intended context path before relying on clinical fields during the call.

***

## Troubleshooting

| Symptom                                            | Cause                                                                                                                                                                                                                   | Fix                                                                                                                                                                                                                                                                                      |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Call connects but patient hears silence/hold music | The prepared agent audio or conference join did not become available as expected.                                                                                                                                       | Verify the use case's service binding, the service release, and outbound voice setup; inspect the resulting run and call status.                                                                                                                                                         |
| `503 Service Unavailable`                          | The call could not be placed, or call state could not be checked.                                                                                                                                                       | Retry with the same `idempotency_key`. If it persists, contact support to confirm outbound voice setup.                                                                                                                                                                                  |
| `404 Not Found` on `patient_entity_id`             | The entity doesn't exist, isn't in this workspace, or isn't a `person` entity type.                                                                                                                                     | Check the entity via `GET /v1/{workspace_id}/world/entities/{entity_id}`.                                                                                                                                                                                                                |
| Call goes to voicemail                             | A voicemail that picks up counts as an answer, so the notice and greeting play to it.                                                                                                                                   | Keep the greeting fit to leave on a voicemail. Find the call in the [Runs](/developer-guide/platform-api/conversations/runs.md) inventory. A later call attempt should use a deliberate retry policy and a new idempotency key; `derived_from_call_sid` can record correlation metadata. |
| Agent speaks but has no patient context            | The direct endpoint validates the patient for lineage but does not pass the entity ID as a dedicated voice-runtime context field. Caller lookup, projection freshness, or service configuration may also be incomplete. | Verify the service's identity-resolution and world-tool path instead of assuming the request's patient identifier preloads context.                                                                                                                                                      |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.concurrence.com/developer-guide/platform-api/conversations/calls.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
