Voice apps, PBX apps, clients and pricing
A voice app decides what happens on a call: fetch CoXML, send events to your call-control webhook, run a hosted AI agent, or run a flow. PBX apps are ready-made IVRs, ring groups and voicemail boxes. Client tokens let the browser and mobile voice SDKs make and take calls. Pricing shows what the project pays.
All paths are relative to https://api.contactivity.io/v1/projects/{project_id}. Authentication, errors, paging
and idempotency work as described in the overview. AI agent versions, tests and analytics are in
ai-agents.md; flow versions in flows.md.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /voice-apps |
apps:read |
List voice apps (paged; filter kind, comma-separated) |
| POST | /voice-apps |
apps:write |
Create a voice app |
| GET | /voice-apps/{app_id} |
apps:read |
Fetch a voice app |
| PATCH | /voice-apps/{app_id} |
apps:write |
Update a voice app |
| DELETE | /voice-apps/{app_id} |
apps:write |
Archive a voice app |
| GET | /pbx-apps |
pbx:read |
List PBX apps (paged) |
| POST | /pbx-apps |
pbx:write |
Create a PBX app |
| GET | /pbx-apps/{app_id} |
pbx:read |
Fetch a PBX app |
| PATCH | /pbx-apps/{app_id} |
pbx:write |
Update a PBX app |
| DELETE | /pbx-apps/{app_id} |
pbx:write |
Archive a PBX app |
| GET | /pbx-apps/{app_id}/messages |
pbx:read |
List voicemail messages (paged) |
| DELETE | /pbx-apps/{app_id}/messages/{message_id} |
pbx:write |
Archive a voicemail message |
| POST | /clients/tokens |
clients:write |
Create a client access token |
| DELETE | /clients/{identity}/tokens |
clients:write |
Revoke an identity's tokens |
| GET | /clients/settings |
clients:read |
Fetch client settings |
| PATCH | /clients/settings |
clients:write |
Update client settings |
| GET | /push-credentials |
clients:read |
List push credentials (paged) |
| POST | /push-credentials |
clients:write |
Create a push credential |
| DELETE | /push-credentials/{credential_id} |
clients:write |
Archive a push credential |
| GET | /pricing |
pricing:read |
The project's price list |
| GET | /pricing/check |
pricing:read |
Price one call |
PATCH requests change only the fields you send; the result is checked like a create. Request bodies refuse
unknown fields (400 unknown_parameter).
Voice apps
The voice app object
| Field | Type | Description |
|---|---|---|
id |
string | app_… (AP… for apps made through the compatibility API) |
kind |
string | coxml, call_control, ai_agent or flow |
name |
string | |
markup |
string | coxml, plivo or auto (detect) |
param_style |
string | contactivity or twilio: the callback dialect (see below) |
voice_url, voice_method |
string | Where CoXML is fetched for each call (GET or POST) |
fallback_url |
string or null | Fetched when voice_url fails |
status_callback |
string or null | Receives call status changes |
hangup_url |
string or null | Called when the call ends |
webhook_url |
string or null | Call-control events |
webhook_failover_url |
string or null | Tried when webhook_url fails |
agent |
object or null | ai_agent apps: the agent config, with every default filled in |
flow |
object or null | flow apps: the draft flow graph |
flow_version |
integer or null | flow apps: the published version that runs |
enabled |
boolean | A disabled app never runs: calls routed to it are rejected (logged in /errors, category app) and POST /calls with it returns 409 app_disabled |
created_at |
timestamp |
Create or update a voice app
| Field | Type | Rules |
|---|---|---|
kind |
string | Required on create. Cannot be changed |
name |
string | Up to 100 characters |
enabled |
boolean | true (default) or false to disable the app |
markup |
string | coxml (default), plivo or auto |
param_style |
string | contactivity (default for new apps) or twilio. contactivity: call-control apps get the native event envelope (events.md) and CoXML requests carry snake_case params; twilio: Twilio/Telnyx-compatible names. An update keeps the stored value |
voice_url |
string | Required for coxml apps |
voice_method |
string | GET or POST (default) |
webhook_url |
string | Required for call_control apps |
fallback_url, status_callback, hangup_url, webhook_failover_url |
string | Optional; "" clears |
agent |
object | Required for, and only allowed on, ai_agent apps. instructions is required; see the AgentConfig schema in the OpenAPI spec for every field |
flow |
object | Required for, and only allowed on, flow apps. Validated on save (see POST /flows/validate) |
Every URL must be a public http(s) URL.
Errors: 400 invalid_parameter (param names the field, including agent and flow fields), 400 unknown_parameter,
400 invalid_json, 404 not_found. Deleting an app that a route or a PBX app uses returns 409 conflict.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/voice-apps \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"kind": "coxml", "name": "Main menu", "voice_url": "https://example.com/voice"}'
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/voice-apps \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"kind": "ai_agent", "name": "Receptionist", "webhook_url": "https://example.com/agent-events",
"agent": {"instructions": "You are the receptionist for Acme Dental.", "greeting": "Hi, how can I help?"}}'PBX apps
The PBX app object
| Field | Type | Description |
|---|---|---|
id |
string | pa_… |
name |
string | |
kind |
string | ivr, ring_group or voicemail |
config |
object | The config of the kind, with defaults filled in |
created_at, updated_at |
timestamp |
Create or update a PBX app
name (1-100 characters) and kind are required on create; kind cannot be changed. A config you send
replaces the stored one whole. Unknown config fields are refused.
An action is {"type", "target"}. type is extension (an extension or SIP username of this project),
trunk, number (E.164, not blocked), sip_uri, pbx_app, voice_app, voicemail (a voicemail PBX app) or
hangup (empty target). Ring group members take only extension, trunk, number or sip_uri.
A greeting is {"say"?, "voice"?, "play_url"?}: text up to 1000 characters, a voice id (1-64 letters, digits,
_, ., -) and/or a public https audio URL.
| Kind | Config field | Rules |
|---|---|---|
ivr |
greeting |
Greeting |
timeout_s |
1-30, default 5 | |
max_digits |
1-6, default 1 | |
retries |
0-5, default 2 | |
invalid_message |
Up to 1000 characters | |
options |
Up to 20 {"digits", "action"}; digits are 1-6 of 0-9*#, unique, at most max_digits long |
|
default_action |
Action | |
ring_group |
strategy |
Required: simultaneous or sequential |
ring_timeout_s |
5-120, default 25 | |
members |
Required: 1-20 member actions | |
no_answer_action |
Action | |
voicemail |
greeting |
Greeting |
max_length_s |
5-300, default 120 | |
email_to |
Up to 10 email addresses |
Errors: 400 invalid_parameter (param names the field, e.g. name, kind, strategy, options[0].digits,
members[1].target), 400 unknown_parameter, 400 invalid_json, 404 not_found. Deleting an app that a route or
another PBX app points at returns 409 conflict.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/pbx-apps \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "Main IVR", "kind": "ivr", "config": {
"greeting": {"say": "Press 1 for sales, 2 for support."},
"options": [{"digits": "1", "action": {"type": "extension", "target": "101"}},
{"digits": "2", "action": {"type": "number", "target": "+15551234567"}}]}}'Voicemail messages
GET /pbx-apps/{app_id}/messages lists messages newest first: id (a recording id), from, to, duration_secs,
created_at and download_url (signed, valid 10 minutes, or null). DELETE …/messages/{id} archives
the message (the audio is kept; ?archived=true lists archived messages).
Clients (in-app calling)
Access tokens
Mint a token on your server and hand it to the browser or mobile SDK. It signs in as client:<identity>, so calls
to client:<identity> ring it.
| Field | Type | Rules |
|---|---|---|
identity |
string | Required. 1-64 letters, digits, _, . or - |
ttl |
integer | 60-3600 seconds, default 3600 |
voice_app_id |
string | Voice app for calls the client places (default: client settings) |
push_credential_id |
string | Push credential the mobile SDK registers its push token with |
Response 201: {"token": "ctk1.…", "identity", "expires_at"}. Errors: 400 invalid_parameter (param
identity, ttl, voice_app_id, push_credential_id).
DELETE /clients/{identity}/tokens ends every unexpired token of the identity and returns {"revoked": <count>}.
Devices already registered stay until their registration is refreshed.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/clients/tokens \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"identity": "alice", "ttl": 600}'Client settings
GET/PATCH /clients/settings: ring_time (10-180 seconds a call to a client rings) and voice_app_id
(default app for calls clients place; "" clears it). Errors: 400 invalid_parameter (param ring_time,
voice_app_id).
Push credentials
Your APNs key or Firebase service account, used to wake the mobile SDK for incoming calls. Secrets are stored encrypted and never returned.
| Field | Type | Rules |
|---|---|---|
kind |
string | Required: apns or fcm |
name |
string | Required. 1-100 characters |
team_id, key_id |
string | apns: 10 characters, A-Z and 0-9 |
bundle_id |
string | apns: your app's bundle identifier |
private_key |
string | apns: contents of the .p8 key (PKCS#8 P-256) |
sandbox |
boolean | apns: development builds |
service_account |
object | fcm: the Firebase service-account JSON file |
The object returned is {"id": "pcr_…", "kind", "name", "team_id", "key_id", "bundle_id", "project_id", "sandbox", "created_at"}. Errors: 400 invalid_parameter (param name, kind, private_key,
service_account, or no param for a bad APNs id), 404 not_found (delete). DELETE archives the credential: it is no longer used to wake devices.
Pricing
GET /pricing returns the project's price list: currency, billing_increment, regions (voice rates for +1
numbers by region and direction), international (prefix rates; the longest matching prefix applies),
custom_rates (your organization's rates, which replace standard prices for matching prefixes), number_fees,
number_rates and number_rates_source (standard or custom), blocked_prefixes, ai_rates_per_min and
ai. Amounts are money objects ({"amount": "0.009000", "currency": "USD"}, 6 decimals); each rate has its own increment (e.g. 60/60: per 60
seconds, rounded up).
GET /pricing/check?to=+442071234567&direction=outbound&duration=120 prices one call:
| Query | Rules |
|---|---|
to |
Required. E.164 (a missing + is added) |
direction |
outbound (default) or inbound (a carrier calling your number to) |
duration |
1-86400 seconds, default 60 |
The response has to, direction, duration, then either blocked: true (outbound to a blocked destination),
or custom, custom_prefix, billing_increment, billed_seconds and available. When available is true it
also has rate_per_min and cost as money objects. Errors: 400 invalid_parameter (param to, direction,
duration).
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/pricing/check?to=%2B442071234567&duration=120" \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY"
