AI agents
An AI agent is a voice app of kind ai_agent. Its agent object (instructions, voice, models,
tools, transfers, ...) is the draft; create and edit it with POST /voice-apps and PATCH /voice-apps/{id}.
The endpoints on this page publish versions, split traffic between them, test agents, and read their
conversations, stats and analytics. A second group, AI providers, covers provider keys, voices and voice
clones, models and prices, tool tests and integrations.
Auth, errors, paging and idempotency work as described in the overview. Every endpoint here needs
scope apps:read (GET) or apps:write (everything else).
Base URL: https://api.contactivity.io/v1/projects/{project_id}
| Method | Path | Purpose |
|---|---|---|
| GET | /ai/agents/{app_id}/versions |
List published versions, traffic split, draft state |
| POST | /ai/agents/{app_id}/versions |
Publish the draft (runs required tests first) |
| GET | /ai/agents/{app_id}/versions/diff?from=&to= |
Compare two versions or the draft |
| GET | /ai/agents/{app_id}/versions/{version} |
Fetch a version with its config |
| POST | /ai/agents/{app_id}/versions/{version}/rollback |
Publish an older version again |
| PUT | /ai/agents/{app_id}/traffic |
A/B split between published versions |
| GET, POST | /ai/agents/{app_id}/tests |
List / create test scenarios |
| PATCH, DELETE | /ai/agents/{app_id}/tests/{id} |
Update / archive a scenario |
| POST, GET | /ai/agents/{app_id}/tests/runs |
Run scenarios / list runs |
| GET | /ai/agents/{app_id}/tests/runs/{run_id} |
Fetch a run with its transcript |
| GET | /ai/agents/{app_id}/calls |
An agent's conversations |
| GET | /ai/calls/{call_id} |
One conversation with its transcript |
| GET | /ai/agents/{app_id}/stats?days= |
One agent's totals |
| GET | /ai/agents/stats?days= |
Totals of every agent |
| GET | /ai/analytics?from=&to=&voice_app_id= |
Analytics dashboard data |
| GET | /ai/keys |
Provider keys (state only) |
| PUT, DELETE | /ai/keys/{provider} |
Set / archive a provider key |
| GET | /ai/options |
Providers, models, languages and a default agent config |
| GET | /ai/rates |
AI prices |
| GET | /ai/usage |
Billed agent usage |
| GET | /ai/voices?provider= |
Voices of a TTS provider |
| GET | /ai/voices/{id}/preview |
Audio sample of a voice |
| POST | /ai/voices/clones/consent |
Consent phrase for a voice clone |
| GET, POST | /ai/voices/clones |
List / create cloned voices |
| DELETE | /ai/voices/clones/{id} |
Archive a cloned voice |
| POST | /ai/tools/test |
Send a signed test call to a tool webhook |
| GET, POST | /ai/integrations |
List / create integrations |
| PATCH, DELETE | /ai/integrations/{id} |
Update / archive an integration |
| GET | /ai/integrations/{id}/tools |
Tools an integration gives an agent |
Money fields (cost, llm_cost, speech_cost, clone price) are money objects
{"amount": "0.0130", "currency": "USD"}, except in /ai/options, which lists plain USD numbers (/ai/rates uses money objects with 6 decimals).
The agent config
The agent object of the voice app. Defaults are filled on save, so a stored config is complete. A sub-object
(llm, tts, stt, turn, idle) given in part keeps the defaults of the fields left out. Unknown fields
are refused. GET /ai/options returns a complete default config under defaults.
| Field | Type | Notes |
|---|---|---|
instructions |
string, required | The system prompt, up to 20000 characters |
greeting |
string | Up to 1000; required when first_message_mode is assistant |
first_message_mode |
string | assistant (speak the greeting), assistant_generated, user (wait for the caller) |
personality |
string | none, professional, friendly, concise, empathetic, energetic |
voice |
string | The TTS provider's voice id (see /ai/voices); empty: the default |
language |
string | en or en-US form |
auto_language |
boolean | Reply in the caller's language (multilingual transcriber and voice needed) |
model |
string | LLM model id of llm.provider (an enabled model in /ai/options) |
llm |
object | provider (openai, anthropic, google, groq; default openai), temperature 0-2 (0.7), max_tokens 32-4096 (250), fallbacks (up to 3 {provider, model}) |
tts |
object | provider (cartesia, elevenlabs, openai, deepgram; default deepgram), model (default aura-2-thalia-en), speed, volume 0.5-2, instructions (OpenAI style prompt, up to 500) |
stt |
object | provider (deepgram, openai, elevenlabs, parakeet; default deepgram), model (nova-3), keyterms (up to 50), smart_format, filler_words, endpointing_ms 10-1000 |
turn |
object | detection (model, vad, stt), min_endpointing_ms 0-5000, max_endpointing_ms 500-20000, allow_interruptions, min_interruption_ms 100-5000, min_interruption_words 0-10, vad_threshold 0.1-0.9, vad_min_silence_ms 100-3000, preemptive_generation |
idle |
object | timeout_secs 5-120 (15), message, max_prompts 0-5 (2), end_message (messages up to 300) |
background_sound |
string | none, office, city, crowded_room, keyboard |
end_call_phrases |
string[] | Up to 10; a reply containing one ends the call |
tools |
array | Up to 20; see below |
transfers |
array | Up to 10 `{name, description, type: number |
transfer_mode |
string | blind or warm (the person hears a summary first) |
max_duration_secs |
integer | 30-3600, default 600 |
recording, noise_suppression, summary |
boolean | Record; denoise the caller; AI summary after the call |
extraction |
object | JSON Schema (type object, up to 8 KB) of data extracted after the call |
success_criteria |
string | Up to 2000; each call is evaluated against it |
voicemail |
object | `{action: hangup |
knowledge_base_ids |
string[] | Up to 5 knowledge bases |
pii_redaction |
boolean | Redact personal data in transcripts, summaries and data (default on) |
recording_consent |
object | {enabled, message}: a consent message spoken first |
mcp_servers |
array | Up to 5 {integration_id, allowed_tools} of mcp integrations |
crm_sync |
object | {integration_id} of a HubSpot or GoHighLevel integration; gets a note after each call |
A tool (tools[]): name (unique snake_case, not end_call, transfer_call or search_knowledge),
description (up to 1000), kind (webhook default, automation, integration), url (public http(s), a
signed POST like webhooks), method (POST), headers, parameters (JSON Schema, type object, up to 8 KB),
timeout_secs 1-30, speak_before (up to 200, said while the tool runs). automation adds provider
(zapier, make, n8n); integration uses integration_id and action (from /ai/integrations/{id}/tools)
and no url or headers.
Versions and traffic
Publishing snapshots the draft as the next version (1, 2, 3, ...) and sends all calls to it. An agent never
published answers with its draft. Version object: version, note, author (user id or api-key),
author_email, created_at; fetch adds config.
# Publish (runs the agent's text test scenarios first; a failing required one refuses the publish)
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/agents/app_us1_abc/versions \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"note": "Shorter greeting"}'Body (optional): note (up to 500 characters), skip_tests (publish even when a required test fails). Returns
201 with the version, plus test_runs when tests ran. Errors: 400 invalid_parameter (note),
400 unknown_parameter, 400 invalid_json, 404 not_found, 409 conflict (a required test did not pass; the
error carries test_runs).
GET /ai/agents/{app_id}/versions is paged, newest first, and also carries traffic ([{version, weight}]) and
draft_changed. GET .../versions/{version} returns one version with its config.
POST .../versions/{version}/rollback publishes that version's config as a new version (no tests; default note
Rollback to version N). Returns 201.
GET .../versions/diff?from=3&to=draft returns {from, to, changes: [{path, from, to}]} with dotted paths such
as tts.speed; lists are compared whole. from and to are version numbers or draft.
Split traffic between one or two published versions (weights 1-100, adding up to 100); each call picks one from a hash of its id:
curl -X PUT https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/agents/app_us1_abc/traffic \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" -H "Content-Type: application/json" \
-d '{"split": [{"version": 3, "weight": 90}, {"version": 4, "weight": 10}]}'Returns {split} sorted by version. Errors: 400 invalid_parameter (bad split, or a version not published),
404 not_found.
Tests
A test scenario is a simulated caller (persona, goal) and criteria an LLM judges. text runs let an LLM
play the caller against the agent's own prompt, model and tools (mocked with tool_mocks, or the real
webhooks with live_tools). voice runs place a real call to the agent. LLM tokens of runs are charged like
agent calls.
Scenario fields:
| Field | Type | Required | Limits |
|---|---|---|---|
name |
string | yes | 1-100 |
persona |
string | yes | 1-4000 |
goal |
string | no | up to 2000 |
criteria |
string[] | yes | 1-10 items, each 1-500 |
max_turns |
integer | no | 1-30, default 10 |
required |
boolean | no | A failing run blocks publish |
live_tools |
boolean | no | Call the real tool webhooks |
tool_mocks |
object | no | Up to 20 tool_name: result (result up to 4000) |
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/agents/app_us1_abc/tests \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "Books a cleaning", "persona": "Busy parent, wants Tuesday morning",
"criteria": ["The agent offers a Tuesday morning time", "The agent confirms the booking"], "required": true}'POST returns 201 (at most 20 scenarios per agent: 409 conflict). PATCH .../tests/{id} updates the fields
given (it accepts and ignores the read-only id, created_at, updated_at, last_run). DELETE archives the scenario
(its runs are kept) and returns 204. The list is paged, newest first, each scenario with last_run.
Run them:
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/agents/app_us1_abc/tests/runs \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"kind": "text"}'Body (optional): scenario_ids (default all), kind (text default, or voice with from, a number of this
project, and to, the number routed to the agent, both E.164). Returns 202 with the queued runs as one page.
Errors: 400 invalid_parameter (kind, from, to, scenario_ids; the agent has no scenarios), 404 not_found.
Run object: id (trun_…), scenario_id, scenario_name, required, kind, trigger (manual, publish),
status (queued, running, passed, failed, error), score (0-100), results
([{criterion, pass, reason}]), summary, call_sid, params, cost, error, created_at, finished_at.
GET .../tests/runs lists them (paged); GET .../tests/runs/{run_id} adds transcript and llm_usage.
Conversations, stats, analytics
GET /ai/agents/{app_id}/calls lists an agent's conversations, newest first (paged). Each row: call_sid,
session_sid, voice_app_id, agent_version (null: the draft), from, to, seconds, billed_secs,
model, byok, cost, llm_cost, speech_cost, ended_reason, summary, data (extracted), the STT/TTS
providers and models, latency, eval_success, eval_sentiment, eval_reason, created_at.
GET /ai/calls/{call_id} returns one with its transcript.
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/agents/app_us1_abc/calls?limit=20" \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY"GET /ai/agents/{app_id}/stats?days=30 (1-365, default 30): calls, minutes, cost, avg_seconds,
ended_reasons (reason: count) and by_version. An unknown agent gives zeros.
GET /ai/agents/stats?days=30: one row per agent with calls (voice_app_id, calls, minutes, cost), one page.
GET /ai/analytics?from=&to=&voice_app_id= (RFC 3339; default the last 30 days, at most 366): totals (calls,
minutes, cost, avg_seconds, evaluated, success_rate, transfer_rate, sentiment), latency (p50/p95 ms by
component), end_reasons, daily, by_agent, by_version (with voice_app_id). Days are UTC.
Errors: 400 invalid_parameter (from, to).
AI providers
Provider keys
Bring your own key for openai, anthropic, google, groq, deepgram, cartesia or elevenlabs; the
project's agents then use it instead of the platform's. The API never returns a key.
curl -X PUT https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/keys/deepgram \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" -H "Content-Type: application/json" \
-d '{"key": "dg_xxxxxxxxxxxxxxxxxxxxxxxx"}'key: 20-300 characters, no whitespace (OpenAI: sk-…). The key is checked with one request to the provider.
Returns {provider, configured: true, last4}. GET /ai/keys lists every provider (provider, configured,
last4, created_at) as one page. DELETE /ai/keys/{provider} archives the key (no longer
used; a new one can be set) and returns 204; GET /ai/keys?archived=true lists archived keys.
Errors: 400 invalid_parameter (key), 404 not_found (unknown provider).
Options, rates, usage
GET /ai/options: providers (with your key state), enabled LLM/STT/TTS models with prices, languages, personalities, background sounds, first-message modes, anddefaults(a complete agent config).GET /ai/rates:currency,voice_engine(per-minute parts andbundle_per_min),llm_markup_pct,models(per-million-token prices andest_per_min),rates_per_min. Prices are money objects with 6 decimals;llm_markup_pctis a number.GET /ai/usage: billed agent conversations, newest first (paged; same row fields as agent calls withoutfrom,to,summary,data).
Voices
GET /ai/voices?provider=cartesia|elevenlabs|openai|deepgram (default cartesia) returns the provider's voices
{id, name, description, language, gender}, your cloned voices first (cloned: true), as one page.
ElevenLabs lists the voices of your own key when you set one. Use id as agent.voice.
GET /ai/voices/{id}/preview?provider=&text=&language= returns audio (audio/wav; audio/mpeg for ElevenLabs)
of the voice saying text (up to 300 characters; language two letters, default en). At most 20 new previews
per minute per project.
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/voices/aura-2-thalia-en/preview?provider=deepgram" \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" -o preview.wavErrors: 400 invalid_parameter (provider, id, text, language), 429 rate_limit,
502 upstream_error, 503 service_unavailable (no key for the provider).
Voice clones
Cloning needs the speaker's recorded consent.
POST /ai/voices/clones/consentwith{"speaker_name": "Jane Doe"}returns{consent_id, speaker_name, phrase, code, expires_at}. The phrase includes a 6-digit code and is valid 30 minutes (20 phrases per hour).- The speaker reads the phrase aloud; record it as a WAV of 2-60 seconds (
consent_clip). POST /ai/voices/clonesasmultipart/form-data:
| Field | Required | Notes |
|---|---|---|
provider |
no | cartesia (default) or elevenlabs |
name |
yes | Up to 100 characters |
description |
no | Up to 500 |
language |
no | Two letters, default en |
attest |
yes | true: you confirm the speaker consented |
consent_id |
yes | From step 1 (5 attempts each) |
sample |
yes | WAV, MP3 or M4A, 10 seconds to 5 minutes, up to 20 MB |
consent_clip |
yes | WAV, 2-60 seconds, up to 5 MB |
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/voices/clones \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-F name="Jane" -F attest=true -F consent_id=vcc_us1_abc \
-F sample=@jane.wav -F consent_clip=@consent.wavThe clone price is
charged to the balance. Returns 201 with {id, provider, voice_id, name, description, language, speaker_name, consent_text, attested_by, byok, price, created_at}; use voice_id as agent.voice. At most 50 clones per project.
Errors: 400 invalid_parameter (a field above; 50-clone limit), 402 insufficient_balance (with required,
balance), 422 invalid_parameter (the consent clip does not match the phrase: the error carries transcript
and phrase; or the provider rejected the sample), 429 rate_limit (too many attempts with one phrase),
502 upstream_error, 503 service_unavailable.
GET /ai/voices/clones lists them (one page). DELETE /ai/voices/clones/{id} archives the voice (no longer offered;
the provider copy is removed) and returns 204; 409 conflict (with agents) while an agent uses it.
Tool test
POST /ai/tools/test sends the signed request an agent sends for a tool call and reports the answer:
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/tools/test \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"url": "https://example.com/tools/lookup", "tool": "lookup_order", "arguments": {"order_id": "A-17"}}'Body: url (required, public http(s)), tool (snake_case, default test_tool), headers, arguments (object).
Returns {status, duration_ms, body} (status 0 when the request failed; body is the first 4 KB).
Errors: 400 invalid_parameter (url, tool, arguments, headers).
Integrations
Credentials an agent uses through tools (tools[].kind: integration, mcp_servers) and crm_sync. Calls to them
run in the API, so the secrets never reach the agent. At most 50 per project.
kind |
config |
secrets |
|---|---|---|
mcp |
url (required: the server's Streamable HTTP endpoint) |
headers |
calcom |
event_type_id (required) |
api_key (required) |
google_calendar |
calendar_id (required) |
service_account_json (required) |
hubspot |
api_key (private app token, required) |
|
gohighlevel |
location_id (required), calendar_id |
api_key (private integration token, required) |
Every kind also takes config.time_zone (IANA, default America/New_York) and config.duration_mins (5-480,
default 30). name is required (up to 80).
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/ai/integrations \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"kind": "calcom", "name": "Front desk", "config": {"event_type_id": 12345}, "secrets": {"api_key": "cal_live_xxxxxxxx"}}'Returns 201 with {id, kind, name, config, secrets, created_at, updated_at}; secrets holds hints only
(api_key: "…1234", header hints, service_account_email). PATCH /ai/integrations/{id} replaces the fields
given (kind cannot change; a header set to "" is removed). DELETE archives it (agents stop using it) and returns 204,
or 409 conflict while an agent uses it. GET /ai/integrations is paged. GET /ai/integrations/{id}/tools lists the tools it gives an agent
(name, description, parameters; an MCP server is asked live), one page.
Errors: 400 invalid_parameter (kind, name, config.*, secrets.*), 400 unknown_parameter,
400 invalid_json, 404 not_found, 409 conflict, 502 upstream_error (MCP server failed).

