doc_version: "1.0.0" title: "Peak Voice — documentation index (LLM / agent)" description: "Entry point for machine-readable API documentation and OpenAPI."
Peak Voice API documentation (LLM / agent index)
This site exposes machine-readable API documentation for coding agents and automations.
Canonical artifacts
| Artifact | URL |
|---|---|
| Full integration guide (on this page) | Scroll below the index on /documentation |
| Same guide (raw Markdown) | /documentation/peak-voice-api.md |
| Same guide (legacy alias) | /documentation/agents.md |
| This index only (raw Markdown) | /documentation/index.md |
| OpenAPI JSON | /openapi.json |
| Swagger UI | /docs |
| ReDoc | /redoc |
Base URL
Use the deployed app origin as the API base. REST routes live under /api/v1.
Interactive schema: Swagger at /docs (public; no login required). Calling /api/v1/* still requires Authorization: Bearer or X-API-Key.
Reading order
- Full guide: Neutral Peak Voice onboarding for agents—it ships inline on
/documentationand as standalone Markdown/documentation/peak-voice-api.mdor/documentation/agents.md. - Field-level detail: Use
/openapi.json(or Swagger/docs) for exact request/response models. - Recipes: Follow the numbered sections in the full guide (messaging → agents → phone numbers → call brief → calls).
doc_version: "1.0.0" title: "Peak Voice HTTP API — LLM integration guide" description: "Recipes and conventions for agents; OpenAPI is authoritative for field-level detail."
Peak Voice HTTP API — LLM / agent integration guide
Audience: Coding agents, scripts, and automations.
Authority: Field-level request/response shapes live in /openapi.json and Swagger /docs. This document describes workflows, conventions, and behavior—not every property.
Base URL: Same origin as the deployed app (replace https://peak-voice.peak6labs.com when copying examples).
API prefix: /api/v1
1. Authentication
Most application routes accept either:
Authorization: Bearer <jwt_access_token>X-API-Key: <pv_...>
JWT-only (API keys are rejected):
POST /api/v1/auth/api-keysGET /api/v1/auth/api-keysDELETE /api/v1/auth/api-keys/{key_id}
Create API keys with JWT; use keys for server-to-server calls.
1.1 API key scope
An API key inherits the full scope of the user that created it — calls, agents, phone numbers, SMS, etc. There are no per-key permission flags. To revoke a key, delete it via DELETE /api/v1/auth/api-keys/{key_id}.
The only routes that reject API keys are the key-management routes listed above; that boundary exists so a leaked key cannot mint or list other keys.
2. Response conventions
- Successful list endpoints often return
{ "data": [...], "meta": { "page", "per_page", "total" } }. - Single-resource responses often use
{ "data": { ... } }. - Errors:
{ "error": { "code": "<string>", "message": "<string>", "details"?: { ... } } }with appropriate HTTP status.
3. Recipe — Messaging (SMS)
Prerequisites: A sender number exists in Peak Voice, it is active and SMS-capable, and your caller credential (X-API-Key or JWT) allows sends.
3.1 Send outbound SMS
POST /api/v1/sms/send
- MUST include
idempotency_keyfor safe retries. - MUST include either
from_numberorphone_number_id(not both required by schema—check OpenAPI; one sender identifier is required in practice).
3.2 Read delivery state
GET /api/v1/sms/{message_id}
3.3 Read inbound messages
GET /api/v1/sms/inbound(list; supports filters—see OpenAPI)GET /api/v1/sms/inbound/{message_id}
3.4 Optional: conversation-oriented reads
GET /api/v1/conversationsGET /api/v1/conversations/{conversation_id}GET /api/v1/conversations/{conversation_id}/messages
3.5 Number helpers
POST /api/v1/numbers/parsePOST /api/v1/numbers/validatePOST /api/v1/numbers/format
4. Recipe — Voice agents
4.1 List agents
GET /api/v1/agents — paginated.
4.2 Create an agent
POST /api/v1/agents
Body requires at least name. Optional tuning fields (voice_config, tools, etc.) mirror /openapi.json. Most callers should omit legacy/internal compatibility fields unless an operator mandates them.
Provisioning when the backing voice runtime is unreachable returns 503 with diagnostic codes enumerated in /openapi.json (during migration responses may reuse legacy identifiers—treat codes as telemetry, not end-user taxonomy).
4.3 Read / update / delete
GET /api/v1/agents/{agent_id}PATCH /api/v1/agents/{agent_id}DELETE /api/v1/agents/{agent_id}—204on success
4.4 Versions
GET /api/v1/agents/{agent_id}/versionsGET /api/v1/agents/{agent_id}/versions/{version_number}
4.5 Service reconciliation
POST /api/v1/agents/{agent_id}/reconcile
Use when mirrored sync fields imply drift; inspect provider_sync_status / provider_sync_error (legacy/OpenAPI naming retained for compatibility).
5. Recipe — Phone numbers and agent binding (two steps)
There is no single endpoint that creates an agent and assigns a number. Use two API calls in order.
Step A — Create the agent
POST /api/v1/agents → obtain agent_id.
Step B — Attach a number to that agent
Either:
POST /api/v1/phone-numberswithvoice_agent_idset toagent_id, orPATCH /api/v1/phone-numbers/{phone_id}withvoice_agent_idset toagent_id
Compatibility: line + agent pairing must satisfy Peak Voice routing rules surfaced in /openapi.json (legacy fields such as voice_provider describe internal compatibility—not product choices). Errors include 409 with phone_provider_mismatch until the pairing is corrected.
Other phone-number routes
GET /api/v1/phone-numbers— list (paginated)POST /api/v1/phone-numbers/sync— refresh inventory / service syncDELETE /api/v1/phone-numbers/{phone_id}
Outbound voice uses the caller ID derived from Peak Voice routing for voice_agent_id—see §7.
6. Recipe — Call brief (expected variables)
6.1 Read the contract / schema for an agent
GET /api/v1/agents/{agent_id}/call-brief-schema
Returns product-neutral projections of call_brief_contract, including which dispatch inputs callers should supply (call_brief_text / call_brief_variables — §7).
6.2 Preview normalization (optional)
POST /api/v1/agents/{agent_id}/call-brief/normalize
- Does not enforce required variables (
enforce_requiredis false here);missing_required_fieldsis advisory for UIs and preflight checks. - Returns normalized variables, field sources, warnings, plus internal compatibility previews when Peak Voice attaches them.
6.3 Actual dispatch
When placing a call, POST /api/v1/calls accepts call_brief_text and call_brief_variables (string map). Types and validation are strict—see OpenAPI (StrictStr).
Missing required inputs can return 400 with missing_required_call_brief_variables.
7. Recipe — Outbound calls
7.1 Create an outbound call
POST /api/v1/calls
Recommended body (minimal integration path):
agent_id(UUID)phone_number_to- Optional
phone_number_from - Optional
call_brief_text/call_brief_variables - Optional
webhook_url(consumers supplying callbacks)
/openapi.json remains authoritative for exhaustive fields.
Legacy/internal overrides (voice_provider, greeting, voice, voice_id, …) remain available strictly for compat during schema migration — omit them unless orchestration tooling supplies explicit overrides.
Operational notes:
- Omitting
phone_number_frommakes Peak Voice inherit the assigned outbound line (400no_phone_numberwhen unresolved). - When optional compatibility fields mismatch reality, callers may observe
missing_provider_resource_id,agent_not_synced,incompatible_phone, or related validation errors—repair via §4.5 / §5 before retrying.
7.2 List calls
GET /api/v1/calls — supports agent_id, direction, status filters (OpenAPI).
7.3 Get call detail and completion payload
GET /api/v1/calls/{call_id}
Returns data.call (status, numbers, timestamps, integration metadata mirroring Peak Voice storage) plus data.result when call artifacts are available:
transcriptrecording_urlstructured_datasummaryinsights(normalized provider call insights)objectives(normalized objective / completion results)- timestamps / identifiers per OpenAPI
7.4 Real-time status stream
GET /api/v1/calls/{call_id}/stream — SSE (server-sent events) for status updates.
8. Public schema surfaces (no API key)
These paths are read-only documentation and do not grant access to tenant data:
/docs— Swagger UI/redoc— ReDoc/openapi.json— OpenAPI schema
Several models still advertise legacy/internal compatibility identifiers (*_provider, voice_provider_*, etc.). Prefer openapi.json descriptions + this guide rather than implying those enums are externally branded products.
All /api/v1/* routes remain authenticated as described in §1.
9. Appendix — Representative error meanings
Operational codes evolve—use /openapi.json plus live responses first. Typical concepts map to retries as follows:
| Concept | Typical HTTP | Operator action |
|---|---|---|
| Missing / invalid credential | 401 | Rotate key / JWT |
| Resource missing / tenancy mismatch | 404 | Re-fetch ids |
Line ↔ agent incompatibility (phone_provider_mismatch) | 409 | Align assignments / rerun service sync |
| Missing caller line assignment | 400 (no_phone_number) | Assign compatible line (§5) |
| Required brief vars absent | 400 (missing_required_call_brief_variables) | Hydrate structured brief |
Stale mirrored agent state (agent_not_synced, …) | 409/400 mix | POST …/reconcile then retry once |
| Outbound throttling | 429 | Back off / batch |
Downstream messaging failure (telephony_provider_error) | 502 | Escalate; retry after delay |
Voice runtime/integration fault (voice_provider_error, …) | 502/503 | Escalate; pause automated retries |
Legacy response-code note: Stable machine strings remain in payloads during migration—they signal Peak Voice availability, not standalone products callers integrate with.
Inbound voice
Peak Voice supports Telnyx-backed inbound voice via three primitives:
- Phone number → agent binding.
phone_numbers.voice_agent_idselects the agent that answers calls to a number. - Inbound session (per-event). An
inbound_sessionties a phone number, an agent, optional caller-match constraints (allowed_caller_number), and a callback window together. It is created per interaction (e.g. when a POS system needs a callback to a customer) and expires automatically. - Callback targets and expected callers. Within a session,
callback_targetsandexpected_callersdescribe who Peak Voice expects to call in. Their identity fields (contact_name,aliases,match_hints,known_phone_numbers) feed both the inbound webhook's initial caller match and thelookup_inbound_caller_contexttool used mid-call.
Inbound dynamic variables returned to the Telnyx assistant include caller_match_status, which is one of matched, session_only, unmatched, caller_not_allowed, or empty (outbound calls). The assistant's prompt is expected to branch on this value.
For specialist routing within an inbound call, see Handoff configuration.
Inbound sessions
POST /api/v1/inbound-sessions
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
phone_number_id | uuid | yes | Must be a phone number the requesting team owns. |
agent_id | uuid | yes | The agent that will answer calls on this session. |
expires_at | ISO-8601 | no | Defaults to now + callback_window_seconds. |
callback_window_seconds | int | no | Default 3600. |
allowed_caller_number | E.164 string | no | If set, calls from other numbers are rejected at the TeXML inbound gate. Leave null to allow any caller and identify mid-call. |
context_variables | object | no | Session-level variables returned on every dynamic-variables webhook. Do not put caller-specific facts here — they leak into the unmatched flow. Use callback-target context_variables for caller-specific values. |
Example:
{
"phone_number_id": "f1e2…",
"agent_id": "0a5e6088-46ec-4d4a-b2e3-6728237e41db",
"callback_window_seconds": 7200,
"context_variables": { "campaign": "rsvp-2026" }
}
Returns the full session object including embedded callback_targets and expected_callers arrays (both initially empty).
GET /api/v1/inbound-sessions/{id}
Returns the session with embedded targets.
PATCH /api/v1/inbound-sessions/{id}
Patchable fields: expires_at, callback_window_seconds, context_variables, status.
DELETE /api/v1/inbound-sessions/{id}
Marks the session deleted. Idempotent.
GET /api/v1/inbound-sessions
Query params: agent_id, status (single value), external_session_id, phone_number_id, page, per_page.
Example: GET /api/v1/inbound-sessions?agent_id=0a5e6088…&status=ready returns ready sessions for that agent.
Callback targets
POST /api/v1/inbound-sessions/{id}/callback-targets
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
label | string | no | Free-form display label. |
organization_name | string | no | Caller's organization. |
contact_name | string | no | Caller's name. Matched casefold against caller speech. |
known_phone_numbers | string[] | no | E.164 strings. Exact match (normalized) against caller's number. |
aliases | string[] | no | Casefolded substring match against caller speech. |
match_hints | object | no | Free-form jsonb for ad-hoc matching cues. |
context_variables | object | no | Returned in the dynamic-variables webhook only when this caller is matched. Put caller-specific facts here, not on the session. |
priority | int | no | Higher = tried first when multiple targets match. |
suggested_handoff_target_agent_id | uuid | no | Routing hint surfaced to the front-desk assistant. |
PATCH /api/v1/inbound-sessions/{id}/callback-targets/{target_id}
Patches any of the fields above.
DELETE /api/v1/inbound-sessions/{id}/callback-targets/{target_id}
Removes the target.
Lookup tool matching semantics
The lookup_inbound_caller_context tool is auto-injected on a Telnyx assistant when voice_config.telnyx.handoff.enabled = true. The assistant calls it mid-call with caller_name and/or caller_phone_number. Matching priority:
known_phone_numbers— exact match after E.164 normalization.contact_name— casefold equality or token-level overlap with caller-provided name.aliases— casefold substring match.match_hints— free-form jsonb; current matcher checks string equality on common keys (seebackend/app/services/inbound_session_service.pyfor details).
If any field matches, the tool returns the matching callback target's context_variables and identity fields. The assistant then continues with caller-aware context.
Context variables: scoping rules
- Session-level
context_variablesare returned on every dynamic-variables webhook regardless of caller match. Put session-wide facts here only (campaign id, organization-wide context). Caller-specific text will leak into the unmatched flow. - Callback-target
context_variablesare returned only when that target matches. Put caller-specific facts here (caller name, verification phrase, session goal).
This split is enforced by convention, not by the schema. Violating it will cause the assistant to greet unknown callers as if it knows them.
Webhook events
Inbound sessions support a per-session callback URL. Set it on session create:
{
"phone_number_id": "...",
"agent_id": "...",
"webhook_url": "https://your-app.example.com/api/webhooks/peak-voice/inbound-events"
}
When events occur, Peak Voice POSTs to webhook_url with the standard envelope:
- Headers
Content-Type: application/jsonX-Peak-Voice-Signature: sha256=<hex hmac of raw body>(HMAC-SHA256 over the body usingpeak_voice_webhook_signing_secret)X-Peak-Voice-Event-Type: <event_type>X-Peak-Voice-Delivery-Id: <fresh uuid per attempt>
- Body
{ "event": "<event_type>", "event_id": "<uuid>", "occurred_at": "<ISO-8601 with Z>", "data": { ... } }
Reliability: 3 attempts at t=0, t≈5s, t≈35s. Consumers dedupe on event_id.
Event types in V1:
| Event | When | Key fields in data |
|---|---|---|
callback.received | An inbound call routed to this session and dynamic-variables resolution started. | inbound_session_id, call_id, match_status |
callback.matched | A callback target matched the caller. | inbound_session_id, call_id, callback_target_id, match_status: "matched" |
callback.unmatched | Call routed to session but no target matched (or caller not allowed). | inbound_session_id, call_id, match_status |
callback.late | Inbound traffic arrived after the pool entry transitioned to draining or cooldown. | inbound_session_id, caller_number, pool_status |
session.released | Operator explicitly released the session. | inbound_session_id, status |
Deferred to a follow-up (schema reserved): callback.ambiguous, handoff.suggested, handoff.completed.
The terminal call-lifecycle events for the inbound call itself (call.completed, call.failed, etc.) continue to flow through the existing per-call webhook_url on the Call row — see the Calls section.
Handoff configuration
Specialist routing within an inbound call is configured on the front-desk agent's voice_config.telnyx.handoff:
{
"voice_config": {
"provider": "telnyx",
"telnyx": {
"handoff": {
"enabled": true,
"voice_mode": "unified",
"targets": [
{
"name": "General Specialist",
"agent_id": "4f9d8178-6e1c-40ee-b00f-50076c9d299f",
"trigger": "caller needs help unrelated to an RSVP confirmation",
"description": "General Purpose Voice Agent — fallback specialist for general inquiries."
}
]
}
}
}
}
Validation (enforced by PATCH /api/v1/agents/{id}):
- Each target's
agent_idmust reference an agent in the same team. - Target agent's
default_voice_providermust equal"telnyx". - Target agent's
channelsmust include"voice_inbound". - Target agent must be
provider_sync_status = "synced"with a non-emptyprovider_agent_id. - No duplicate
agent_idvalues withintargets. - An agent cannot hand off to itself.
voice_modeis currently"unified"only.
Error codes:
| HTTP | Code | Cause |
|---|---|---|
| 400 | invalid_handoff_policy | Empty targets when enabled, duplicate agent ids, or self-handoff. |
| 404 | invalid_handoff_target | Target agent not found in this team. |
| 409 | invalid_handoff_target | Target lacks voice_inbound or wrong provider. |
| 409 | handoff_target_not_synced | Target not yet synced to Telnyx. |
When handoff.enabled = true, Peak Voice also auto-injects the lookup_inbound_caller_context webhook tool. Identifying callers mid-call and handoff are coupled today.
Worked example: POS callback (Clover-style)
A POS application creates a per-event session when a customer asks for a callback at checkout.
- POS posts a new inbound session bound to the front-desk agent and a leased Peak Voice number:
curl -X POST https://api.example.com/api/v1/inbound-sessions \
-H "Authorization: Bearer $POS_TOKEN" \
-d '{
"phone_number_id": "f1e2…",
"agent_id": "0a5e6088-46ec-4d4a-b2e3-6728237e41db",
"callback_window_seconds": 3600,
"allowed_caller_number": null,
"context_variables": { "campaign": "pos-callback" }
}'
- POS posts a callback target carrying the customer's identity:
curl -X POST https://api.example.com/api/v1/inbound-sessions/$SID/callback-targets \
-H "Authorization: Bearer $POS_TOKEN" \
-d '{
"label": "Order #4421",
"contact_name": "Jane Doe",
"known_phone_numbers": ["+18001234567"],
"context_variables": {
"order_id": "4421",
"items_count": 3
}
}'
-
The customer dials the leased number. Peak Voice's dynamic-variables webhook returns
caller_match_status: "matched"with the order context, and the front-desk assistant greets the customer with their order context. -
If the customer needs to escalate (e.g. billing dispute), the front-desk assistant invokes the configured handoff target. Telnyx's unified handoff switches the live AI Assistant in place.
To reproduce the unmatched and handoff variants locally, see docs/runbooks/inbound-callback-local-e2e-test.md.