# 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, 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](/guides/overview.md). Flow versions are in [flows.md](/guides/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` 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 |
| `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](/guides/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 |
| `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 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`.

```sh
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"}'
```

## 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 |
| | `transcribe` | `true` transcribes each message, default `false` |
| | `transcribe_language` | Language code such as `en-US` (default) or `es-ES` |

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

```sh
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`, `download_url` (signed, valid 10 minutes, or null) and `transcription` (`id`, `status`, `text`; null
when the box does not transcribe). `DELETE …/messages/{id}` archives the message (the audio is kept;
`?archived=true` lists archived messages).

With `transcribe` on, each message is transcribed like a recording with `transcription: true` and charged per audio
minute at the transcription rate, on top of the recording. The `email_to` email then waits for the transcript and
includes it; after 2 minutes it goes out without it ("Transcript unavailable"). One email per message. Projects in restricted data mode
get the short "sign in to listen" email, never the transcript.

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

```sh
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`, `products`, then the
`sip_trunking` prices at the top level: `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`, and `recording` (`recording` per minute, `recording_storage` per minute-month, `recording_storage_free_days`,
`live_transcription` per minute, `increment`, and `custom` when your organization has its own price; see
[Recording and storage](/guides/billing.md#recording-and-storage)). 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).

`products` has one entry per [product](/guides/billing.md#products). `sip_trunking` and `programmable_voice` each have
`regions`, `international` and `custom_rates`; `programmable_voice_sip`, `byoc_sip_trunking` and
`byoc_programmable_voice` have one `rate_per_min` for every destination and direction, with `increment` and
`custom` (true when your organization has its own rate).

`GET /pricing/check?to=+442071234567&direction=outbound&duration=120` prices one call:

| Query | Rules |
|---|---|
| `product` | A [product](/guides/billing.md#products); default `sip_trunking` |
| `to` | Required, except for `programmable_voice_sip` and the BYOC products (one rate for every destination). 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`, `product`, 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` when `product`, `to`, `direction` or
`duration` is not valid.

```sh
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/pricing/check?to=%2B442071234567&duration=120&product=programmable_voice" \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY"
```
