> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sempera.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions, calls and phone numbers

> Channel-neutral sessions, minting a Web voice session, phone calls, phone numbers.

## Sessions

A Session is one conversation with an Agent on any channel:

| `channel` | Where it came from |
| - | - |
| `web_voice` | A browser voice conversation opened with `POST /v1/sessions` |
| `web_chat` | A typed preview turn |
| `api_chat` | An authenticated API chat turn with the published agent |
| `simulator` | A simulated caller with a synthetic caller id |
| `phone` | A real phone call (also a Call) |
| `test` | A scripted test scenario |

`GET /v1/sessions?agent_id=...` lists them newest first; `GET /v1/sessions/{id}` returns the transcript (`turns`), the tool timeline and captured variables where the runtime recorded them, latency, outcome and the version that spoke.

## Live translation

`GET /v1/translation-sessions?agent_id=...` reads availability and recent sessions with `sessions:read`. `POST /v1/translation-sessions` takes `agent_id`, `caller_language` and `staff_language`, requires `sessions:write`, and returns two room-scoped join tokens. Both routes require workspace activation and access to the agent. No phone call is placed. Tokens must not be logged or shared outside the intended participants.

## Dialing pools

These routes require access to the agent and the workspace's dialing-pool entitlement. Reads need `calls:read`; configuration writes need `calls:write` and permission to send outbound calls.

| Route | Behavior |
| - | - |
| `GET /v1/agents/{agent_id}/dialing-pools` | Lists pools and owned-number readiness. |
| `GET /v1/agents/{agent_id}/dialing-pools/{pool_id}` | Reads one pool in this workspace and agent. |
| `POST /v1/agents/{agent_id}/dialing-pools` | Creates a pool from `name`, `phone_numbers`, `daily_cap` and `local_only`; supports an idempotency key. |
| `POST /v1/agents/{agent_id}/dialing-pools/{pool_id}/update` | Replaces the same settings; supports an idempotency key. |
| `POST /v1/agents/{agent_id}/dialing-pools/{pool_id}/preview` | Takes only `to` and previews selection without dialing or reserving capacity; needs `calls:read`. |

## Rich messaging

These routes require an active workspace and access to the agent. Reads need `calls:read`.

| Route | Behavior |
| - | - |
| `GET /v1/agents/{agent_id}/messaging` | Reports channel setup and remaining activation requirements. |
| `GET /v1/agents/{agent_id}/messaging/{channel}/conversations` | Lists stored conversations with `page` and `page_size`. |
| `GET /v1/agents/{agent_id}/messaging/{channel}/conversations/{conversation_id}` | Reads a conversation belonging to this channel, agent and workspace. |
| `POST /v1/agents/{agent_id}/messaging/{channel}/messages` | Requires `calls:write` and send admission, then refuses with `409 owner_setup_required`. No message is queued or delivered. |

## Voices

`GET /v1/voices` lists every voice a session or a draft may name, with `production_eligible` (may carry a live call) and `available` (it can speak on this deployment right now). Each voice is named by a Sempera voice id (`id`, `voice_`, then letters and digits) and described by `name`, `description`, `language`, `source` (`built_in`, `library`, or the kind of a custom voice) and, for your workspace's own custom voices, `visibility: "private"`. The id is stable: the same voice has the same id on every read and every deployment, so it is safe to store. No voice payload names the speech engine or the engine's own id for the voice. Pass the id as `voice_id` on `POST /v1/sessions` to override the agent's published voice for one conversation, or set it as the draft's `voice.id` and publish. A raw voice id from before 2026-10-28 is still accepted wherever a voice is written; reads return only Sempera voice ids.

The optional `engine` filter (also accepted as `provider`) takes a Sempera engine alias returned by a runtime reading. Treat it as opaque. Older engine values remain accepted for compatibility. An invalid alias returns `400 validation_failed` with `reason: invalid_voice_id`; catalogue refusals use Sempera's wording and a stable reason, never the engine's own error text. The MCP `list_voices` tool lists the same Sempera voice identities without an engine argument.

Spec exports and frozen environment settings name speech engines and models by the same stable Sempera aliases. Existing saved agents, published versions and retained snapshots are not rewritten.

### Voice library and studio

A workspace keeps a shortlist of cleared Sempera voices. Every workspace member sees the whole library; sharing only controls a direct link to one entry. Private clones are never added or shared. Writes need the `agents:write` scope and an Operator role or above; the studio routes need an Owner.

| Route | Scope | What it does |
| - | - | - |
| `GET /v1/voices/library` | `agents:read` | The workspace's saved voices: `{voices: [{id, voice_id, display_name, rating, shared, share_scope, share_path}]}` |
| `POST /v1/voices/library/add` | `agents:write` | Save a cleared voice by its Sempera `voice_id`. Saving the same voice again returns the same entry. `201` |
| `GET /v1/voices/library/{entry_id}` | `agents:read` | One entry, only while it is shared. Otherwise `404` |
| `POST /v1/voices/library/{entry_id}/rate` | `agents:write` | Set the workspace rating, a whole number from 1 to 5 |
| `POST /v1/voices/library/{entry_id}/share` | `agents:write` | `{"enabled": true}` or `false` turns the direct entry link on or off |
| `GET /v1/voices/library/{entry_id}/sample` | `agents:write` | Plays the voice with the workspace sample text as `audio/wav`. A sample can spend capacity, so it needs the write scope and the same Operator role as the writes and a workspace that is active |
| `GET /v1/voices/studio` | `agents:read` | Saved professional clone preparations, with a notice that training is not enabled |
| `POST /v1/voices/studio` | `agents:write` | Save preparation details: `display_name`, an active `consent_id` and `declared_audio_seconds` (600 to 7200). No recording is accepted, nothing is trained and the request stays `usable: false`. `201` |

## Models

`GET /v1/models` and `GET /v2/tts/models` (`GET /v2/tts/models/{id}` for one) list Sempera's own published, customer-eligible speech models: `scopes: models:read`. Values are Sempera's own model and voice names; no route ever names a supply account or an underlying engine. `GET /v2/models` is the same speech catalog, reshaped as the model list many client libraries expect (`{id, object: "model", created: 0, owned_by: "sempera", name, modality: "text-to-speech", capabilities: {encodings, sample_rates, containers, controls}}`, `Cache-Control: public, max-age=300`), and needs no credential at all.

## Stored prompts

`GET /v1/prompts` lists a workspace's saved prompts (`{"status": "success", "prompts": [...]}`); `GET /v1/prompts/{id}` reads one; `POST /v1/prompts` with `{"prompt": "...", "name": "..."}` (`name` optional) saves a new one, both wrapped the same way (`{"status": "success", "prompt": {"id", "prompt", "name", "last_updated"}}`). Reads need `prompts:read`; writes need `prompts:write`. A saved prompt is reused by hand, most often filling the `prompt` field on a call's own settings; nothing here runs at call time.

## Minting a Web voice session

```http theme={null}
POST /v1/sessions
{"agent_id": "northwind-barbers", "channel": "web_voice", "end_user_id": "visitor-42", "language": "en"}
```

The response is `{"id", "transport", "url", "token", "expires_at", "voice", "speech_recognition"}`, where `transport` names the realtime media protocol the `url` and `token` are for ([Web SDK](https://docs.sempera.org/web-sdk.md)). Hand `url` and `token` to the browser; the token is bound to that one room and expires in 30 minutes. Your API key never leaves your server. The request is refused with `409` when the agent has no published version, when the requested voice is not in the catalogue, or when the platform is at capacity (`503`). When the agent hears or speaks through a published Sempera model, `speech_recognition` or `voice` names it by its Sempera model id (`model_id`, repeated as `provider` and `model`), and the session runs that model; if the model cannot carry a call on this deployment, the request is refused with `409` and a sentence saying so, and no other model is used in its place. Otherwise the two legs name no engine and no model: `voice` carries the voice's Sempera id (`voice_id`) and name, and `speech_recognition` its language. A voice that cannot be used is refused in Sempera's words, and a `voice_id` that looks like a Sempera voice id but is not one is refused with `400` and `reason: invalid_voice_id`.

## What ran a session: `runtime_identity`

`GET /v1/sessions/{session_id}` carries a `runtime_identity` block for a session recorded from 2026-10-29 on: one answer to "what heard, thought and spoke, did anything fall back, and was it a customer's conversation". Older sessions return `null`.

| Field | Meaning |
| - | - |
| `legs.stt`, `legs.llm`, `legs.tts` | The hearing, thinking and speaking legs. Each has `engine`, `model` and `machine` (a category: `cloud` or `sempera`); `tts` also has `ended_on` (the engine the voice really ended on), `voice` (the Sempera voice id) and `language` when known |
| `fallbacks` | Handovers that happened: `llm` rows (`kind`, `from`, `to`) and `tts` rows (`from`, `to`, `reason` in Sempera's words). Empty lists mean nothing fell back |
| `worker` | `runtime` (`origin` when Sempera's own servers ran the call, `hosted` when a hosted call service ran it) and `transport` |
| `latency` | Timings the runtime measured: `greeting_at_s`, `first_reply_s`, `reply_turns`, and reply and voice percentiles |
| `traffic` | `class` (`customer`, `demo`, `test` or `unrecorded`) and `company_overhead`, true for anything that is not a real caller on a real line |
| `verified` | False when the stored block no longer matches its own integrity checksum. The checksum catches accidental corruption; it is not tamper evidence |

Engines and models carry Sempera's sealed names (`engine_...`), or the Sempera model id when a leg ran a published Sempera model. The block never names a supplier, a host, a device or a failure's own text, and it prices nothing.

## Calls

A phone call is a Session with `channel: phone` and also a Call: `GET /v1/calls?agent_id=...`, `GET /v1/calls/{call_id}`. A Call adds the carrier's lifecycle: caller and called numbers, direction, duration, recording state and provider correlation ids (internal).

`POST /v1/calls` (`calls:write`, owner role, requires `Idempotency-Key`) places one outbound call through the App's guarded dispatcher. A missing gateway or unreadable outbound policy refuses the request; it never reports a ledger-only call as queued.

```http theme={null}
POST /v1/calls
Idempotency-Key: appointment-reminder-unique-intent
{"agent_id": "northwind-barbers", "to": "+14165550123", "production": true,
 "message": "Your appointment reminder is ready.",
 "consent_basis": "This person asked Northwind Barbers to call them with appointment reminders.",
 "metadata": {"name": "Ada"}}
```

| Field | Rule |
| - | - |
| `agent_id`, `to` (alias `phone_number`) | Required. `to` is E.164, or national digits with `country` (an ISO code the App's Send a call page lists) |
| `production` (alias `use_production`) or `agent_version` (alias `agent_version_id`) | By default, resolve the production deployment pin. An explicit version names a published id or number. That version's exact artifact must be installed; an older version is `400` on `agent_version`, not a silent fallback. No production pin is `400 AGENT_ENV_UNPINNED`. A publish that lands while the call is being placed or the call service is loading the version refuses rather than running different bytes |
| `message` (aliases `opening`, `first_sentence`) | The opening line after the required automated-call disclosure |
| `consent_basis` | Required sentence recording why this person opted in. Never invented by the API |
| `from` (alias `from_number`) | An owned caller id |
| `metadata` | Existing opening-template context: text values substituted into `{name}` and other placeholders. Not a per-call webhook payload. Values that the batch normalizer would truncate are refused |
| `timezone`, `window` | Recipient calling hours. Defaults match the App: `America/Toronto`, `09:00-18:00`. Outside the window no call is placed |
| `precall_dtmf` | Keys pressed after connection and before the employee speaks |
| `name` | Label in the outbound record, at most 80 characters |
| `settings` | `voice_id`, `language`, `max_call_minutes`, `interruptions`, `interruptibility`, `interruption_threshold`, `acknowledgements`, `background_noise`, `noise_cancellation`, `voicemail`, `prompt`, `keywords`, `call_data`. JSON types are checked; unsupported keys and conflicting settings are `400` |
| `voice`, `language`, `task`, `max_duration`, `keywords` | Aliases for `settings.voice_id`, `.language`, `.prompt`, `.max_call_minutes`, `.keywords`. Duration is a whole number of minutes, 2 through 60, subject to the platform's stricter safety ceiling. With `mode` `prompt`, `task` is the whole conversation and is required. Otherwise the task supplements published rules. Keywords require a recognizer with per-call word-list support; otherwise `400` names `settings.keywords` |
| `mode` | `agent` (default) or `prompt`. See Prompt mode below. `prompt_only` is not a field |
| `block_interruptions` | Boolean: true blocks interruption, false allows it. Cannot be combined with interruption settings |
| `background_track` | Alias for `settings.background_noise`: `none`/`off`, `office`, `coffee_shop`, `restaurant` |
| `request_data` | Alias for `settings.call_data`: a nonempty object of named text values. Never combine both spellings. Available throughout the call, separate from opening-template metadata |

`call_data` is an object of up to 20 named text values for this conversation only. A key starts with a letter and contains only letters, digits and underscores, at most 40 characters; each value is at most 200 characters. The runtime binds these as call facts without changing the published agent. Do not send credentials in call data. The App's version picker lists the currently available published phone version and refuses an unavailable selection rather than silently replacing it. Scheduling in the App creates a deferred batch only when the scheduler is enabled; the public single-call API still places calls now.

Send only one spelling of an option, including across `settings` and top-level aliases. `voicemail` and `precall_dtmf_sequence` alias the existing voicemail and keypad settings. `webhook` selects a registered signed endpoint for this call; see [Webhooks](https://docs.sempera.org/webhooks.md). Any other option returns `400 validation_failed` naming its field, even when null or false. Recording overrides, transfers, per-call scheduling, retries, model selection and analysis overrides are refused until their runtime paths support them.

### Prompt mode

`mode: "prompt"` places one call from a prompt instead of the agent's conversation. `agent_id` is still required. It is the line the call goes out on (number, consent, do-not-contact, calling hours, caps and billing), not the conversation. A prompt is required: `task` or `settings.prompt`. `first_sentence` (alias of `message`) is the opening line after the automated-call disclosure. `voice` and `language` set the voice and language for this call. Omit `agent_version`, `production` and `environment`; sending any of them is `400`. `prompt_only` is not a field. Sending it is `400` and skips no check.

```http theme={null}
POST /v1/calls
Idempotency-Key: friday-3pm-ada
{"agent_id": "northwind-barbers", "mode": "prompt", "phone_number": "+141****0123",
 "task": "Confirm Friday at 3pm. Ask whether that time still suits.",
 "first_sentence": "Hello, this is a quick call about Friday.",
 "voice": "voice_example", "language": "en",
 "consent_basis": "This person asked Northwind Barbers to call them about Friday."}
```

The same checks as an agent call still apply, and `mode` does not turn any of them off: the consent sentence, the idempotency key, calling hours, the do-not-contact list, outbound caps, an owned caller id, workspace screening, and billing. A missing prompt places nothing. The answer is `201` with `mode: "prompt"` and no `agent_version_id`. The line's instructions, tools, connections and knowledge are not used.

### Corrected transcripts

`POST /v1/calls/{call_id}/corrected-transcript` (`calls:write` and `calls:read`, owner role) imports corrected turns for a completed phone call: `source` (`human_review` or `recording_analysis`) and 1 to 5000 `segments`, each with ordered `start` and `end` seconds, a numeric `speaker`, a `speaker_label` (`caller`, `assistant` or `unknown`), `text` and a 0 to 1 `confidence`. `GET /v1/calls/{call_id}/corrected-transcript` (`calls:read`) reads it back. Every import reads as `provenance: owner_import`, `verified: false` and `confidence_kind: importer_reported`: `source` is what the importer said, and Sempera does not verify it or re-transcribe the call. The original transcript is unchanged, and the correction shares its retention and deletion. No correction yet is `409 correction_unavailable`; a deleted or expired transcript is `410`. A call recorded under Exclude from history, a call on an agent this key cannot see and a call that does not exist all answer the same `404`. Each import is audited with ids and counts, never the words.

## Active call controls

These routes require `calls:write`, the owner supervision permission and a workspace-bound credential. An optional `agent_id` narrows the credential's agent scope; it cannot widen it. Every request requires a nonempty `reason` of at most 200 characters, recorded by the existing supervision audit trail.

* `POST /v1/calls/{call_id}/stop` ends one active phone call by its Sempera id.
* `POST /v1/calls/active/transfer` takes `call_id`, `to` (E.164) and `reason`. It uses the existing phone transfer path and preserves launch restrictions.
* `POST /v1/calls/active/stop` ends a snapshot of active phone calls within the authorized scope. Browser sessions and calls arriving afterward are not affected. The response has `data` (one outcome per call), `total` and `stopped`. Failures are reported individually, not hidden by aggregate success. A legacy room without a public call id can still be stopped in bulk; its result has `id: null` rather than exposing an internal room identifier.

One-call controls return `404` for missing, ended or out-of-scope calls. An unreadable live roster returns `503`, not an empty success. A transport failure can leave the outcome unknown: check active calls before retrying. The console's Live now section offers owner-confirmed bulk stop for the current agent, alongside its existing single-call stop and transfer controls.

## Questions about a finished call

`POST /v1/calls/{call_id}/analyze` takes `{"questions": ["What did the caller request?"]}`. It requires `calls:read`, evaluation permission and a workspace-bound credential. An optional `agent_id` query parameter narrows lookup. Each request reserves one conversation-evaluation credit unit through the existing credit ledger.

Send 1 to 10 nonempty questions, each at most 500 characters. The call must have a finished phone transcript. Missing or deleted transcripts are refused. Transcripts over 60,000 characters are refused, never silently truncated. Public outbound ids resolve to the call service's finished record of the call when that record contains the public dispatch correlation; a dispatch checkpoint alone is not a finished transcript.

The response includes `id`, `agent_id`, `object: call_analysis` and `answers`. Each answer has `question`, `answer`, `evidence` and `status`. An answered question must include a verbatim transcript quote. Missing information returns `answer: null`, empty evidence and `status: unknown`. An unavailable evaluator or invalid evidence returns a service error, never an invented answer. Answers are returned without creating a new stored analysis record. The conversation drawer offers the same questions form to members with evaluation permission and explains the credit use before submission.

## Call batches

`POST /v2/batches/create` requires `calls:write`, owner outbound permission and `Idempotency-Key`. Send `global` with the call's agent, production/version, consent basis and supported call options, and `call_objects` containing `phone_number`, optional `metadata` template context, `timezone` and `precall_dtmf_sequence`. Alternatively supply `csv`, using the existing recipient CSV columns. Invalid rows reject the request rather than disappearing. Unknown per-recipient options are refused. Optional top-level fields are `description`, `concurrency`, `max_attempts`, `retry_after_s`, `start_at`, and `status_webhook` (an enabled signed endpoint registered in the workspace).

Creation persists and starts the batch, then advances one due pass through the existing consent, do-not-contact, quota and call-cap gates. Future windows and retries remain queued. The response includes the scheduler's actual state: automatic continuation requires the existing deployment switch `SEMPERA_OUTBOUND_SCHEDULER=1`; no API changes that switch or raises caps. `POST /v2/batches/{batch_id}/run`, with an idempotency key, also advances due recipients using that same checkpointed path.

`GET /v2/batches/list`, `GET /v2/batches/{batch_id}` and `GET /v2/batches/{batch_id}/logs` return real persisted records and attempts. Lists and logs support `take` and `skip`; logs also accept `direction=asc|desc`. `POST /v2/batches/{batch_id}/stop` stops future recipients and retries, not an already connected call. Repeating stop is safe. Every endpoint subscribed to `batch.updated` receives signed events with `batch_id`, `state` and `summary` after passes and lifecycle changes. A `status_webhook` must name an endpoint subscribed to `batch.updated` (otherwise `400`), and receives each event once. A failed notification never turns a placed batch into an error. All resources are workspace and agent scoped.

The workspace Outbound list offers recorded attempts and owner-only Start, Resume, Pause and Stop actions using the same guarded lifecycle as the agent's Outbound page. Each action asks for confirmation; Stop requires a reason. Starting or resuming makes the batch eligible for the scheduler, not proof that a recipient was contacted. The list's `single_call` flag identifies one-call checkpoints, which do not offer batch lifecycle actions. Attempts show status and time without recipient numbers or provider messages.

The answer is `201` with a `call` object: `id` (`call-...`), `agent_id`, `channel` (`phone`), the dispatcher's real `status` (`answered` on connection), `direction` (`outbound`), `caller` and `to`, `agent_version_id`, `agent_version_number`, `batch_id`, `attempt` and `settings`. The request is checkpointed as a one-call batch before dialing. `GET /v1/calls/{id}` reads that dispatch record in the same workspace; this checkpoint is not a completed-call transcript or duration. Internal provider correlation ids never appear in the creation response. Before dialing, the number is checked against the workspace's do-not-contact list and outbound limits, and nobody is called when either stops it:

* A number on the do-not-contact list is refused with `501 capability_unavailable`, `reason` `do_not_contact` in the error and a message that says the number is on the list. The one-call batch is still recorded, with that person marked as blocked, and the refusal is audited.
* A reached limit answers `429 rate_limited` with `retry_after_s` in the error, and a limit that cannot be read answers `503 service_unavailable`. Nothing is recorded but the audit row ([Owner features in the App](https://docs.sempera.org/app-features.md)).

The same request and key replay the successful response for 24 hours; a changed request is `409 idempotency_key_reused`. Concurrent requests and uncertain dispatches are `409 request_in_progress`. A timeout or process loss after dispatch begins does not release the reservation or reclaim it after two minutes. Reconcile the saved outbound attempt before making a new intent; do not use a new key to blindly retry an uncertain call.

## Phone numbers

`GET /v1/phone-numbers?agent_id=...` lists the numbers bound to an agent, with the carrier readback that proves where the number really points. `POST /v1/phone-numbers` with `{"agent_id", "number"}` binds a number to an agent (owner role, `phone_numbers:write`). Binding is what makes a real call storable at all: an unbound number's calls are never stored.

A developer does not need to know which telephony or voice platform Sempera runs to bind a number. Those are internal and may change.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.