# Contactivity API reference

The Contactivity API places and controls calls, manages numbers, trunks and voice apps, and reports usage. It is JSON over HTTPS, one project per path:

```
https://api.contactivity.io/v1/projects/{project_id}/...
```

The interactive reference is the [API Reference](/reference/) tab; the spec is
[/openapi/native.yaml](/openapi/native.yaml) (OpenAPI 3.1).
The SDKs are `@contactivity/sdk` (Node) and `contactivity_sdk` (Python). Moving from Twilio or Telnyx? See [compatibility.md](/guides/compatibility.md).

| Page | Resources |
|---|---|
| [calls.md](/guides/calls.md) | Calls and call actions (speak, gather, bridge, record, redirect, ...) |
| [recordings.md](/guides/recordings.md) | Recordings and transcriptions |
| [conferences-queues.md](/guides/conferences-queues.md) | Conferences, participants, queues, queue members |
| [numbers.md](/guides/numbers.md) | Number search, ordering and routing; verified numbers |
| [trunks-routes.md](/guides/trunks-routes.md) | SIP trunks, credentials, IP allow lists, routes, limits, webhook public key |
| [byoc.md](/guides/byoc.md) | Bring your own carrier: carriers, gateways, source IPs, numbers, 911 routing |
| [voice-apps.md](/guides/voice-apps.md) | Voice apps, PBX apps, client tokens, push credentials, pricing |
| [flows.md](/guides/flows.md) | Flow validation and versions |
| [billing.md](/guides/billing.md) | Usage, call detail records, balance, ledger |
| [events.md](/guides/events.md) | Events, webhooks, webhook endpoint, delivery log, error debugger, alert rules |

## Region

Send requests to your project's region: `https://api.<region>.contactivity.io` (for example
`api.us1.contactivity.io`); `https://api.contactivity.io` is `us1`. A key from another region is refused with
`401` and a body naming the right host.

## Authentication

Create an API key in the portal (Settings > API keys) and send its secret as a bearer token:

```sh
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/calls \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY"
```

A key belongs to one project. `{project_id}` must be that project, or the answer is `404 not_found`
("Project not found"). A bad or revoked key gets `401 authentication`.

### Scopes

A key carries scopes. Each endpoint needs the scopes listed on its page (and in the spec's `x-scopes`); a key
without them gets `403 insufficient_scope` and a message naming the missing scope.

| Scope | Covers |
|---|---|
| `calls:read`, `calls:write` | Calls and call actions. Adding or removing conference participants and dequeuing also need `calls:write` |
| `recordings:read`, `recordings:write` | Recordings and transcriptions |
| `conferences:read`, `conferences:write` | Conferences and participants |
| `queues:read`, `queues:write` | Queues and members |
| `numbers:read`, `numbers:write` | Available numbers, numbers |
| `verified_numbers:read`, `verified_numbers:write` | Verified numbers (fetching a verification needs `verified_numbers:write`) |
| `trunks:read`, `trunks:write` | SIP trunks, credentials, IPs |
| `routes:read`, `routes:write` | Routes |
| `byoc:read`, `byoc:write` | BYOC carriers, their numbers and 911 routing |
| `apps:read`, `apps:write` | Voice apps and flows |
| `pbx:read`, `pbx:write` | PBX apps and voicemail |
| `clients:read`, `clients:write` | Client access tokens, in-app calling settings, push credentials |
| `pricing:read` | Prices and price checks |
| `billing:read` | Usage, CDRs, balance, ledger |
| `events:read`, `events:write` | Events, webhook endpoint, delivery log, errors, alert rules, webhook key rotation |
| (any key) | `GET /limits`, `GET /webhook-public-key` |

API keys cannot create keys, top up or change payment methods; those stay in the portal.

## Requests and responses

- Bodies are JSON (`Content-Type: application/json`) with snake_case fields. Bodies are strict: an unknown field
  is `400 unknown_parameter` (with `param`), a field of the wrong type is `400 invalid_parameter`.
- `POST` that creates returns `201` and the object; `DELETE` returns `204` with no body; reads and updates
  return `200` and the object.
- `DELETE` archives; nothing is deleted (see [Archiving](#archiving)). The one exception is a phone number:
  `DELETE /numbers/{phone_number}` releases it.
- Timestamps are RFC 3339 UTC with milliseconds: `2026-10-22T14:03:11.123Z`.
- Durations are whole seconds, in fields ending in `_secs`.
- Money is an object: `{"amount": "0.0130", "currency": "USD"}`. `amount` is a decimal string and never negative
  (4 decimals for usage, 2 for balances); a ledger entry says `credit` or `debit` in `direction`.
- IDs are opaque strings: `CA…` calls, `RE…` recordings, `TR…` transcriptions, `CF…` conferences, `QU…` queues,
  `app_…` voice apps, `trk_…` trunks, `rt_…` routes, `byoc_…` BYOC carriers. Phone numbers are E.164 and are URL-encoded in paths
  (`/numbers/%2B14155551234`).
- Every response carries `X-Request-Id` (`req_…`). Quote it when you contact support.

## Errors

Every error has the same body:

```json
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_parameter",
    "message": "to must be an E.164 number, client:<name> or a SIP URI with a public host",
    "param": "to",
    "request_id": "req_us1_…",
    "doc_url": "https://docs.contactivity.io/errors/invalid_parameter"
  }
}
```

| `type` | HTTP status |
|---|---|
| `invalid_request` | 400, 402, 413, 422 |
| `authentication` | 401 |
| `permission` | 403 |
| `not_found` | 404 |
| `conflict` | 409 |
| `rate_limit` | 429 |
| `api_error` | 5xx |

Branch on `code`, not on `message`. `param` names the field, query parameter or header at fault, when there is
one. Codes every endpoint can return:

| Code | Status | Meaning |
|---|---|---|
| `authentication` | 401 | Missing, bad or revoked API key |
| `insufficient_scope` | 403 | The key lacks a scope this endpoint needs |
| `not_found` | 404 | The project (or the object) does not exist for this key |
| `unknown_parameter` | 400 | A body field the endpoint does not take |
| `invalid_parameter` | 400 | A field or query value is missing or wrong (`param` says which) |
| `invalid_json` | 400 | The body is not a JSON object |
| `rate_limit` | 429 | Too many requests; retry after `Retry-After` seconds |
| `idempotency_conflict` | 409 | The `Idempotency-Key` was used with a different request |
| `request_in_progress` | 409 | The first request with this `Idempotency-Key` is still running |
| `body_too_large` | 413 | A body over 64 KB sent with `Idempotency-Key` (uploads: the endpoint's own limit) |
| `api_error` | 500 | Our fault; safe to retry with the same `Idempotency-Key` |

Endpoint pages list the other codes they return (for example `call_state`, `insufficient_balance`,
`caller_id_not_verified`, `number_not_available`).

## Pagination

Lists return a page, newest first:

```json
{"data": [ ... ], "has_more": true, "next_cursor": "<opaque cursor>"}
```

Pass `limit` (1-200, default 50) and, for the next page, `starting_after=<next_cursor>`:

```sh
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/calls?limit=100&starting_after=$CURSOR" \
  -H "Authorization: Bearer $CONTACTIVITY_API_KEY"
```

A few small lists (conference participants, queue members, verified numbers, trunk credentials and IPs, available
numbers, catalogs such as voices) are a single page: `has_more` is always `false`. Their pages say so. The SDKs'
`iterate()` follows `next_cursor` for you.

## Archiving

Nothing you create is deleted. `DELETE` on a resource archives it and still returns `204`:

- It stops working at once: an archived API key cannot authenticate, a SIP credential or trunk IP cannot
  register or authenticate, a route no longer routes, a voice app, PBX app or flow is handled like a
  missing one, an alert rule stops firing, a caller ID cannot be used, a queue refuses new callers.
- Lists leave it out. Add `?archived=true` to a list to get only archived resources.
- `GET` by id still returns it, with `archived_at` set (an RFC 3339 timestamp; `null` while active).
- Changing it, or a second `DELETE`, is `409 archived`. Archiving is permanent: there is no restore.

Call data is never erased: calls, recordings and their audio, transcriptions, voicemail, and events. The one exception to archiving is a phone number: `DELETE /numbers/{phone_number}`
releases it. The Twilio- and Telnyx-compatible APIs archive in the same way (an archived resource answers `404`
there and leaves their lists); deleting an `IncomingPhoneNumbers` entry still releases the number.

Project retention settings (`retention` in `/v1/projects/{id}/compliance`) no longer remove anything: `PUT`
accepts them and changes nothing, and `GET` returns every retention value as `null` (data kept).

## Idempotency

Send `Idempotency-Key: <8-100 characters>` (a UUID is fine) on any `POST`. For 24 hours, the same key with the
same method, path and body returns the first response again, with `Idempotent-Replayed: true`, instead of acting
twice. So a `POST` that timed out can be retried safely with the same key.

- The same key with a different request: `409 idempotency_conflict`.
- The first request is still running: `409 request_in_progress`; retry shortly.
- With the header, the body is limited to 64 KB (`413 body_too_large`).
- Ordering a number requires the header (`400 missing_parameter` without it).

The SDKs send a key on every `POST` (yours, or a generated one reused across retries).

## Rate limits

Requests are limited per project, 100 per second for API keys. Over the limit the API answers `429 rate_limit` with `Retry-After` (seconds).
Placing calls also has calls-per-second and simultaneous-call limits (`429 rate_limit` from `POST /calls`); see
`GET /limits`.

## Webhooks and events

Contactivity calls your URLs for CoXML documents, status callbacks and call-control events. Every
request is signed with Standard Webhooks headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`,
Ed25519); the public key is at `GET /webhook-public-key`, and the SDKs verify it with
`verifyStandardWebhook` / `verify_standard_webhook`. Voice apps with `param_style: "contactivity"` receive events
in the native envelope `{id, type, created_at, project_id, data}`; see [events.md](/guides/events.md).

## Versioning

The version is in the URL (`/v1`). Additive changes (new endpoints, fields, event types, error codes) ship
without notice, so ignore fields you do not know. Breaking changes come as a new major version with at least
12 months of overlap; deprecated endpoints send `Deprecation` and `Sunset` headers.
