Contactivity
Numbers and SIP.md

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.

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

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

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

sh
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:

json
{"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.

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