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

ArtifactURL
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

  1. Full guide: Neutral Peak Voice onboarding for agents—it ships inline on /documentation and as standalone Markdown /documentation/peak-voice-api.md or /documentation/agents.md.
  2. Field-level detail: Use /openapi.json (or Swagger /docs) for exact request/response models.
  3. 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-keys
  • GET /api/v1/auth/api-keys
  • DELETE /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_key for safe retries.
  • MUST include either from_number or phone_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/conversations
  • GET /api/v1/conversations/{conversation_id}
  • GET /api/v1/conversations/{conversation_id}/messages

3.5 Number helpers

  • POST /api/v1/numbers/parse
  • POST /api/v1/numbers/validate
  • POST /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}204 on success

4.4 Versions

  • GET /api/v1/agents/{agent_id}/versions
  • GET /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-numbers with voice_agent_id set to agent_id, or
  • PATCH /api/v1/phone-numbers/{phone_id} with voice_agent_id set to agent_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 sync
  • DELETE /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_required is false here); missing_required_fields is 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_from makes Peak Voice inherit the assigned outbound line (400 no_phone_number when 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:

  • transcript
  • recording_url
  • structured_data
  • summary
  • insights (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:

ConceptTypical HTTPOperator action
Missing / invalid credential401Rotate key / JWT
Resource missing / tenancy mismatch404Re-fetch ids
Line ↔ agent incompatibility (phone_provider_mismatch)409Align assignments / rerun service sync
Missing caller line assignment400 (no_phone_number)Assign compatible line (§5)
Required brief vars absent400 (missing_required_call_brief_variables)Hydrate structured brief
Stale mirrored agent state (agent_not_synced, …)409/400 mixPOST …/reconcile then retry once
Outbound throttling429Back off / batch
Downstream messaging failure (telephony_provider_error)502Escalate; retry after delay
Voice runtime/integration fault (voice_provider_error, …)502/503Escalate; 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:

  1. Phone number → agent binding. phone_numbers.voice_agent_id selects the agent that answers calls to a number.
  2. Inbound session (per-event). An inbound_session ties 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.
  3. Callback targets and expected callers. Within a session, callback_targets and expected_callers describe 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 the lookup_inbound_caller_context tool 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:

FieldTypeRequiredNotes
phone_number_iduuidyesMust be a phone number the requesting team owns.
agent_iduuidyesThe agent that will answer calls on this session.
expires_atISO-8601noDefaults to now + callback_window_seconds.
callback_window_secondsintnoDefault 3600.
allowed_caller_numberE.164 stringnoIf set, calls from other numbers are rejected at the TeXML inbound gate. Leave null to allow any caller and identify mid-call.
context_variablesobjectnoSession-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:

FieldTypeRequiredNotes
labelstringnoFree-form display label.
organization_namestringnoCaller's organization.
contact_namestringnoCaller's name. Matched casefold against caller speech.
known_phone_numbersstring[]noE.164 strings. Exact match (normalized) against caller's number.
aliasesstring[]noCasefolded substring match against caller speech.
match_hintsobjectnoFree-form jsonb for ad-hoc matching cues.
context_variablesobjectnoReturned in the dynamic-variables webhook only when this caller is matched. Put caller-specific facts here, not on the session.
priorityintnoHigher = tried first when multiple targets match.
suggested_handoff_target_agent_iduuidnoRouting 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:

  1. known_phone_numbers — exact match after E.164 normalization.
  2. contact_name — casefold equality or token-level overlap with caller-provided name.
  3. aliases — casefold substring match.
  4. match_hints — free-form jsonb; current matcher checks string equality on common keys (see backend/app/services/inbound_session_service.py for 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_variables are 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_variables are 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/json
    • X-Peak-Voice-Signature: sha256=<hex hmac of raw body> (HMAC-SHA256 over the body using peak_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:

EventWhenKey fields in data
callback.receivedAn inbound call routed to this session and dynamic-variables resolution started.inbound_session_id, call_id, match_status
callback.matchedA callback target matched the caller.inbound_session_id, call_id, callback_target_id, match_status: "matched"
callback.unmatchedCall routed to session but no target matched (or caller not allowed).inbound_session_id, call_id, match_status
callback.lateInbound traffic arrived after the pool entry transitioned to draining or cooldown.inbound_session_id, caller_number, pool_status
session.releasedOperator 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_id must reference an agent in the same team.
  • Target agent's default_voice_provider must equal "telnyx".
  • Target agent's channels must include "voice_inbound".
  • Target agent must be provider_sync_status = "synced" with a non-empty provider_agent_id.
  • No duplicate agent_id values within targets.
  • An agent cannot hand off to itself.
  • voice_mode is currently "unified" only.

Error codes:

HTTPCodeCause
400invalid_handoff_policyEmpty targets when enabled, duplicate agent ids, or self-handoff.
404invalid_handoff_targetTarget agent not found in this team.
409invalid_handoff_targetTarget lacks voice_inbound or wrong provider.
409handoff_target_not_syncedTarget 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.

  1. 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" }
  }'
  1. 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
    }
  }'
  1. 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.

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