SIP trunks, routes and project settings
SIP trunks connect your phones, PBX or SIP provider to Contactivity. Routes decide where a number's inbound calls go. This page also covers the project's limits and the webhook public key.
All paths are relative to https://api.contactivity.io/v1/projects/{project_id}. Authentication, errors, paging
and idempotency work as described in the overview.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| GET | /trunks |
trunks:read |
List trunks (paged) |
| POST | /trunks |
trunks:write |
Create a trunk |
| GET | /trunks/{trunk_id} |
trunks:read |
Fetch a trunk |
| PATCH | /trunks/{trunk_id} |
trunks:write |
Update a trunk |
| DELETE | /trunks/{trunk_id} |
trunks:write |
Archive a trunk |
| GET | /trunks/{trunk_id}/credentials |
trunks:read |
List SIP credentials (one page) |
| POST | /trunks/{trunk_id}/credentials |
trunks:write |
Create a SIP credential |
| PUT | /trunks/{trunk_id}/credentials/order |
trunks:write |
Set the ring order of SIP credentials |
| DELETE | /trunks/{trunk_id}/credentials/{username} |
trunks:write |
Archive a SIP credential |
| GET | /trunks/{trunk_id}/ips |
trunks:read |
List allowed IPs (one page) |
| POST | /trunks/{trunk_id}/ips |
trunks:write |
Allow an IP or range |
| DELETE | /trunks/{trunk_id}/ips/{ip_id} |
trunks:write |
Archive an allowed IP |
| GET | /routes |
routes:read |
List routes (paged; filter type) |
| POST | /routes |
routes:write |
Create a route |
| GET | /routes/{route_id} |
routes:read |
Fetch a route |
| PATCH | /routes/{route_id} |
routes:write |
Update a route |
| DELETE | /routes/{route_id} |
routes:write |
Archive a route |
| GET | /limits |
any key | The project's limits and today's spend |
| GET | /webhook-public-key |
any key | The Ed25519 key that signs webhooks |
| POST | /webhook-public-key/rotate |
events:write |
Rotate the project's signing key |
PATCH requests change only the fields you send; the result is checked like a create. Every write is recorded in the project's audit log with the API key's id.
Trunks
The trunk object
| Field | Type | Description |
|---|---|---|
id |
string | trk_… |
name |
string | |
auth_type |
string | register (SIP credentials), ip (IP allowlist) or host (calls go to and come from fixed hosts) |
hosts |
string[] | host[:port] targets of a host trunk, in failover order. Empty for other trunks |
default_caller_id |
string or null | Caller ID used when a call from the trunk has none of the project's |
recording |
boolean | Record calls of this trunk |
ring_strategy |
string | How an inbound call rings a register trunk's devices (see Ring strategy) |
ring_secs |
integer | Seconds each device rings before the next one (not for ring_all) |
created_at |
timestamp |
Create or update a trunk
| Field | Type | Rules |
|---|---|---|
name |
string | Required on create. 1-100 characters, trimmed |
auth_type |
string | Required on create: register, ip or host. Cannot be changed |
hosts |
string[] | Required for, and only allowed on, host trunks. Each host[:port] must resolve to public addresses. An IP host that another trunk or a carrier already uses is refused |
default_caller_id |
string or null | A number of this project or a verified number. "" or null clears it |
recording |
boolean | Default false |
ring_strategy |
string | ring_all (default), sequential, round_robin, longest_idle or random. Other trunks than register take only ring_all |
ring_secs |
integer | 5-120, default 20 |
cps_limit, session_limit |
Refused. Calls per second and concurrent calls are account limits set by Contactivity (see /limits) |
Errors: 400 invalid_parameter (param name, auth_type, hosts, default_caller_id, ring_strategy, ring_secs or cps_limit),
400 invalid_request (a host that is invalid, not public or already in use), 400 unknown_parameter,
400 invalid_json, 404 not_found (PATCH), 409 already_exists.
Archiving a trunk (DELETE) also archives its SIP credentials and IPs: they stop authenticating and routing at
once. A trunk that an active route or PBX app uses is refused with 409 in_use. An archived SIP credential's
username and an archived IP can be used again; ?archived=true on the credentials and IPs lists shows archived
ones. An archived route stops routing: numbers pointing at it are unrouted in effect.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/trunks \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"name": "Office PBX", "auth_type": "host", "hosts": ["203.0.113.10:5060"], "recording": true}'SIP credentials (register trunks)
Phones and PBXs register with a username and password. Only a digest hash of the password is stored.
| Field | Type | Rules |
|---|---|---|
username |
string | Required. 3-64 letters, digits, _, . or -. Unique on the platform |
password |
string | Required. 12-128 characters. Never returned |
position |
integer | 0-10000. Ring order; default after the trunk's last credential |
The response is {"username", "domain", "trunk_id", "position"}; domain is where the phone registers (your
organization's SIP domain). The list returns {"username", "domain", "position"} items in ring order, in one page
(has_more is always false).
Errors: 400 invalid_parameter (param username, password, position), 400 invalid_request (the trunk is not a
register trunk), 404 not_found, 409 already_exists (username taken). Deleting a phone that a PBX app rings
returns 409 in_use.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/trunks/$TRUNK_ID/credentials \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"username": "frontdesk-101", "password": "correct-horse-battery"}'Ring strategy
Each SIP credential of a register trunk is one device (all phones registered under it ring together). An inbound
call, or a PBX app dialing the trunk, rings them by the trunk's ring_strategy:
| Strategy | Rings |
|---|---|
ring_all |
Every device at once; the first to answer takes the call |
sequential |
One device at a time, for ring_secs each, in position order |
round_robin |
As sequential, starting with the device after the one that started the previous call |
longest_idle |
As sequential, the device whose last call was answered or ended longest ago first |
random |
As sequential, in a new random order each call |
Devices with no registration are skipped. The route's ring timeout still ends the call. A device that is busy or does
not answer passes the call to the next one; a decline (603) ends it. When none is left the caller gets 486
(last one busy) or 480.
PUT /trunks/{trunk_id}/credentials/order with {"usernames": [...]} sets the order: every active credential of the
trunk once, first to ring first. It returns the credentials in their new order. Errors: 400 invalid_parameter
(param usernames), 400 invalid_request (not a register trunk), 404 not_found.
curl -X PUT https://api.contactivity.io/v1/projects/$PROJECT_ID/trunks/$TRUNK_ID/credentials/order \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" -H "Content-Type: application/json" \
-d '{"usernames": ["frontdesk-101", "frontdesk-102"]}'Allowed IPs (ip trunks)
POST {"cidr": "203.0.113.0/29"} allows a public IP (203.0.113.7) or a network of at most /29 (IPv4) or /125
(IPv6); host bits are masked. A range that overlaps any trunk or carrier range is refused with
409 already_exists. Items are {"id", "ip_addr", "mask"} (id is a numeric string); the list is one page and
also shows the IP hosts of a host trunk.
Errors: 400 invalid_parameter (param cidr), 400 invalid_request (the trunk is not an ip trunk),
404 not_found, 409 already_exists.
Routes
A route sends inbound calls somewhere. Point a number at it with PATCH /numbers/{e164} (route_id).
The route object
| Field | Type | Description |
|---|---|---|
id |
string | rt_… |
type |
string | trunk, forward, sip_uri, app, coxml, call_control, ai_agent or flow |
target |
string | Trunk id, E.164 number, SIP URI, PBX app id, or voice app id (by type) |
mode |
string | priority or percentage |
fallback_route_id |
string or null | Route tried when this one fails with one of failover_codes |
recording |
boolean | Record calls on this route |
failover_codes |
string | Comma-separated SIP codes |
ring_timeout |
integer | Seconds |
created_at |
timestamp |
Create or update a route
| Field | Type | Rules |
|---|---|---|
type |
string | Required on create. See above |
target |
string | Required on create. Must match type: a trunk, PBX app (app) or voice app of that kind of this project; an E.164 number that is not blocked (forward); a SIP URI to a public host (sip_uri) |
mode |
string | priority (default) or percentage (forward routes only) |
fallback_route_id |
string or null | Another route of this project. "" or null clears it |
recording |
boolean | Default false |
failover_codes |
string | Default 408,500,502,503,504. SIP codes 100-699, comma-separated |
ring_timeout |
integer | 5-600 seconds, default 30 |
scope |
Refused (carrier routes only) |
Errors: 400 invalid_parameter (param type, target, mode, failover_codes, ring_timeout,
fallback_route_id), 400 invalid_request (percentage mode on a non-forward route, scope given, blocked
destination), 400 unknown_parameter, 404 not_found (PATCH).
Deleting a route unroutes the numbers that pointed at it and clears other routes' fallback to it.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/routes \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
-d '{"type": "trunk", "target": "'$TRUNK_ID'", "ring_timeout": 25}'
curl "https://api.contactivity.io/v1/projects/$PROJECT_ID/routes?type=trunk&limit=20" \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY"Limits
GET /limits (any key of the project) returns:
| Field | Type | Description |
|---|---|---|
project_id |
string | |
status |
string | Project status |
kyc_approved |
boolean | |
cps_limit |
integer | New calls per second |
session_limit |
integer | Concurrent calls |
recording_default |
boolean | |
intl_enabled |
boolean | International calling allowed |
intl_prefixes |
string[] | Allowed international prefixes (empty: all) |
max_rate_per_min |
money or null | Calls to destinations above this price are refused |
daily_spend_limit |
money or null | Spend cap per UTC day |
spend_today |
money | Spend so far today (UTC) |
Limits are set by Contactivity; contact support to change them.
Webhook public key
GET /webhook-public-key (any key of the project) returns:
{"algorithm": "ed25519", "public_key": "<base64>", "previous_public_key": "<base64>" | null,
"previous_expires_at": "2026-10-23T09:00:00.000Z" | null, "rotated_at": "2026-10-22T09:00:00.000Z" | null}public_key verifies the webhook-signature header on every webhook, CoXML fetch and AI tool call. Until the
project rotates its key this is the platform key and rotated_at is null. The SDKs'
verifyStandardWebhook / verify_standard_webhook take this key.
POST /webhook-public-key/rotate (scope events:write; in the portal, an organization owner/admin or project
admin) makes a new key and returns the same body. The replaced key keeps signing for 24 h
(previous_public_key until previous_expires_at): during that time webhook-signature carries two
space-separated v1a, entries, the new key's first, so a receiver that trusts either key keeps working. Other
servers pick up a rotation within a minute; rotating again within 24 h drops the oldest key at once.
curl https://api.contactivity.io/v1/projects/$PROJECT_ID/webhook-public-key \
-H "Authorization: Bearer $CONTACTIVITY_API_KEY"
