Contactivity
AI.md

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.

sh
# 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:

sh
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)
sh
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:

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

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

sh
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, and defaults (a complete agent config).
  • GET /ai/rates: currency, voice_engine (per-minute parts and bundle_per_min), llm_markup_pct, models (per-million-token prices and est_per_min), rates_per_min. Prices are money objects with 6 decimals; llm_markup_pct is a number.
  • GET /ai/usage: billed agent conversations, newest first (paged; same row fields as agent calls without from, 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.

sh
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.wav

Errors: 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.

  1. POST /ai/voices/clones/consent with {"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).
  2. The speaker reads the phrase aloud; record it as a WAV of 2-60 seconds (consent_clip).
  3. POST /ai/voices/clones as multipart/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
sh
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.wav

The 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:

sh
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).

sh
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).