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 tab; the spec is
/openapi/native.yaml (OpenAPI 3.1).
The SDKs are @contactivity/sdk (Node) and contactivity_sdk (Python). Moving from Twilio or Telnyx? See Compatibility API.
| Page | Resources |
|---|---|
| Calls | Calls and call actions (speak, gather, bridge, record, redirect, ...) |
| Recordings and transcriptions | Recordings and transcriptions |
| Conferences and queues | Conferences, participants, queues, queue members |
| Numbers | Number search, ordering and routing; verified numbers |
| SIP trunks and routes | SIP trunks, credentials, IP allow lists, routes, limits, webhook public key |
| Bring your own carrier | Bring your own carrier: carriers, gateways, source IPs, numbers, 911 routing |
| Voice apps | Voice apps, PBX apps, client tokens, push credentials, pricing |
| Flows | Flow validation and versions |
| Billing | Usage, call detail records, balance, ledger |
| Events and webhooks | 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:
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 is400 unknown_parameter(withparam), a field of the wrong type is400 invalid_parameter. POSTthat creates returns201and the object;DELETEreturns204with no body; reads and updates return200and the object.DELETEarchives; nothing is deleted (see 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"}.amountis a decimal string and never negative (4 decimals for usage, 2 for balances); a ledger entry sayscreditordebitindirection. - 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:
{
"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:
{"data": [ ... ], "has_more": true, "next_cursor": "<opaque cursor>"}Pass limit (1-200, default 50) and, for the next page, starting_after=<next_cursor>:
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=trueto a list to get only archived resources. GETby id still returns it, witharchived_atset (an RFC 3339 timestamp;nullwhile active).- Changing it, or a second
DELETE, is409 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_parameterwithout 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 and webhooks.
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.

