Bring your own SIP trunk

Connect your own carrier's SIP trunk to Skysay for incoming and outgoing calls with your own numbers.

Use a SIP connection when your numbers live at a carrier Skysay has no automatic import for. You keep your carrier account and its bill. Skysay uses one generic form for every carrier: you copy your carrier's SIP settings into it, and point your carrier at Skysay.

A SIP trunk carries voice only. Numbers on a SIP connection cannot send or receive SMS.

This guide covers the API. To set up a trunk in the workspace instead, see Set up your own SIP trunk.

What you need from your carrier

  • The carrier's SIP host and port, and whether it uses UDP or TCP. TLS and SRTP are not supported yet.
  • How Skysay's calls authenticate: a SIP username and password (digest), or an IP allow-list of Skysay's signalling addresses.
  • The public IPv4 networks your carrier sends calls from.
  • Your numbers, and the format the carrier sends and expects them in. For a Turkish number +90 XXX XXX XX XX these are: with a plus (+90XXXXXXXXXX), without one (90XXXXXXXXXX), national (0XXXXXXXXXX), or with 00 (0090XXXXXXXXXX).
  • Your carrier must not require SIP registration (REGISTER). Carriers that only accept registered trunks are not supported yet.

Create the connection

curl -X POST https://api.skysay.ai/v1/sip-connections \
  -H "Authorization: Bearer $SKYSAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My carrier",
    "country": "US",
    "codecs": ["PCMU", "PCMA"],
    "outbound": {
      "host": "sip.carrier.example",
      "port": 5060,
      "transport": "udp",
      "auth": {"mode": "digest", "username": "your-sip-user", "password": "your-sip-password"},
      "dial_format": "e164",
      "caller_format": "e164"
    },
    "inbound": {
      "source_cidrs": ["203.0.113.0/28"],
      "number_formats": ["e164_plus", "e164"]
    }
  }'
  • outbound is how Skysay places calls through your carrier.
    • dial_format is the format of the number being called. national applies only to numbers in the connection's country; a call to another country is refused rather than sent in the wrong format.
    • caller_format is the format of your number as caller ID.
  • inbound is how your carrier's calls reach Skysay.
    • source_cidrs are your carrier's public IPv4 networks, at most 16, none wider than /16. Host names, private, documentation and other special-purpose ranges (and any network containing one), networks of the carriers Skysay itself uses, and malformed entries are refused. The example's 203.0.113.0/28 is a documentation range: use your carrier's own networks.
    • number_formats lists every format your carrier may send the called number in.
    • Add "auth": {"mode": "digest", ...} when your carrier can send a SIP username and password with its calls. Use it when your carrier's networks are shared with other customers.
  • Either block can be left out for a connection that only receives or only places calls.

Passwords are write-only: no response contains them. To change a connection, send PATCH /v1/sip-connections/{connection_id} with the full settings and the expected_revision the last response returned. Leave a password out to keep the stored one; changing a username needs its password again.

The response's skysay block lists what to configure at your carrier: the Skysay SIP host to send calls to, its port and transports, and the addresses Skysay's calls come from.

Point your carrier at Skysay

In your carrier's portal, route each number's incoming calls to the Skysay SIP host from the skysay block, on port 5060. If your carrier uses an IP allow-list for outgoing calls, add Skysay's signalling_addresses.

Skysay admits your carrier's networks automatically once a number is attached for incoming calls. The connection's setup.network reads admitted when every Skysay SIP host has applied them.

Until one of the connection's numbers passes its ownership check, the networks stay admitted for 24 hours after you last save the connection, attach a number or check again. After that setup.network reads not_admitted; save the connection or check again to admit them for another 24 hours. A confirmed number keeps them admitted.

Attach a number

curl -X POST https://api.skysay.ai/v1/sip-connections/$CONNECTION_ID/numbers \
  -H "Authorization: Bearer $SKYSAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"e164": "+12025550123", "agent_id": "'$AGENT_ID'"}'
  • agent_id is a published Skysay-hosted agent that answers the number's incoming calls. Agents hosted outside Skysay (external agents) cannot be used on a SIP connection's numbers yet: they are refused with sip_connection_needs_hosted_agent.
  • inbound and outbound default to what the connection carries.
  • A number can be attached to one connection in all of Skysay. If it is already connected in another Skysay workspace, the request is refused; contact support.
  • Premium-rate, shared-cost and unrecognised number types cannot be attached.
  • Skysay charges no monthly fee for the number: your carrier bills it. Calls are charged for Skysay's platform and AI components only; the carrier component is zero.

Ownership check

Before Skysay places calls with your number as caller ID, it checks that the number's incoming calls really reach this connection.

  1. When you attach the number, Skysay calls it over the public phone network through its own carrier.
  2. Your carrier routes the call to Skysay, as configured above. Skysay answers it by itself and listens for a one-time code it plays on the call. Nobody needs to pick up, and the check usually takes under a minute.
  3. When the code is heard on this connection, the number's ownership.status becomes confirmed and outbound_allowed becomes true.

Until then, outgoing calls with that caller ID are refused with sip_ownership_not_confirmed. Incoming calls work as soon as the number is attached.

If the check fails, ownership.last_check reads failed with a reason:

  • not_received: the check call did not reach this connection. Make sure your carrier sends the number's incoming calls to the Skysay SIP host from the networks on the connection.
  • code_not_heard: the call arrived but the code did not. Make sure your carrier passes keypad tones on the call, either as RFC 2833 events or as audio tones.
  • connection_paused: the call arrived while the connection was paused for unusual call volume, so it was refused like any call. Nothing needs fixing on the carrier side: check again once the connection has resumed.

Fix the route, then check again:

curl -X POST https://api.skysay.ai/v1/sip-connections/$CONNECTION_ID/numbers/$NUMBER_ID/ownership-checks \
  -H "Authorization: Bearer $SKYSAY_API_KEY"

Checks are rate limited per number and per workspace; a refusal says when to try again. Changing the connection's source networks or carrier host means every number needs a new check.

A number attached for outgoing calls only cannot be confirmed by a call, because its incoming calls go elsewhere. Contact support to confirm it.

During a check's two-minute window, a call to the number from a withheld caller ID or from Skysay's check number is treated as the check call.

Readiness checks

Three checks help you get a connection working. Each one reports what it saw and is independent of the others; none of them is a guarantee that every call will work.

Reachability

curl -X POST https://api.skysay.ai/v1/sip-connections/$CONNECTION_ID/checks/reachability \
  -H "Authorization: Bearer $SKYSAY_API_KEY"

Skysay sends one SIP OPTIONS request to your carrier host, from the same address its calls come from, over the connection's transport. The request returns 202 and the result appears on the connection as checks.reachability within about half a minute:

  • status: done with the sip_status_code and reason_phrase your carrier answered. Any answer means the host is reachable. A 401 or 407 is the carrier asking for credentials, which is normal. When a 401, 403, 404 or 407 names registration, registration_hint reads likely: your carrier may expect the trunk to register, which Skysay does not do (ask your carrier for IP- or digest-authenticated calls instead).
  • status: failed with reason: no_response: nothing answered within a few seconds. Check the host, port and transport, and that your carrier accepts traffic from Skysay.
  • status: failed with reason: probe_not_run or probe_lost: Skysay could not run the check. Try again.

A carrier that does not answer OPTIONS can still accept calls: use a test call to be sure. One check runs at a time per connection, and six an hour.

Test call

curl -X POST https://api.skysay.ai/v1/sip-connections/$CONNECTION_ID/numbers/$NUMBER_ID/test-calls \
  -H "Authorization: Bearer $SKYSAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "+12025550199", "agent_id": "'$AGENT_ID'", "dial_format": "e164_plus"}'

A test call is an ordinary outgoing call from the number to to, answered by the Skysay-hosted agent agent_id. It is billed like any call and needs the calls:write scope and a confirmed number. dial_format is optional: it tries another dialled-number format for this one call without changing the connection. One test call runs at a time per number, and three an hour.

Read it with GET /v1/sip-connections/{connection_id}/numbers/{number_id}/test-calls/{call_id}: status is calling, then answered or failed, with the call's sip_status_code when your carrier refused it. Once a test call in another format is answered, save that format for the connection:

curl -X POST https://api.skysay.ai/v1/sip-connections/$CONNECTION_ID/numbers/$NUMBER_ID/dial-format \
  -H "Authorization: Bearer $SKYSAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"test_call_id": "'$CALL_ID'"}'

Incoming test

curl -X POST https://api.skysay.ai/v1/sip-connections/$CONNECTION_ID/numbers/$NUMBER_ID/inbound-test \
  -H "Authorization: Bearer $SKYSAY_API_KEY"

This opens a three-minute window. Call the number from any phone: the next call that reaches it through this connection is noted, with only the last four digits of the caller's number (caller_last4). The call itself is handled exactly as it would be without the window.

Verified

Each attached number shows its latest tests.outbound and tests.inbound, and verified: true once every direction it carries has passed since the connection's network last changed: an answered test call for outgoing calls, a received incoming test for incoming calls. Changing the source networks or carrier host clears it.

Limits

  • Transports: UDP and TCP. No TLS or SRTP.
  • Codecs: PCMA and PCMU.
  • Source networks: public IPv4 only, at most 16, none wider than /16.
  • Ports: 5060, or 1024–65535.
  • No custom SIP headers, no P-Asserted-Identity and no username-based From.
  • channel_limit is recorded and shown; your carrier enforces it. A carrier that refuses for capacity answers with busy or unavailable.
  • The connection's number of numbers is limited to 200, and a workspace can have 20 connections.
  • A workspace can save its connections (create or change them) 30 times an hour in all.

Paused connections

A connection whose numbers receive far more calls a minute than a real phone line would is paused: its calls are refused, it places no new calls, and setup.network reads paused with the reason unusual_call_volume. setup.paused.paused_until says when it resumes by itself: 10 minutes after the calls stop, then 30 and 60 minutes if it trips again within a day. More calls while it is paused extend the pause.

To resume it sooner yourself:

curl -X POST https://api.skysay.ai/v1/sip-connections/$CONNECTION_ID/resume \
  -H "Authorization: Bearer $SKYSAY_API_KEY"
  • A connection can be resumed this way three times in any 24 hours. More are refused with sip_connection_resume_rate_limited (429, with retry_after_seconds); a connection that isn't paused is refused with sip_connection_not_paused (409), and a disabled one with sip_connection_disabled (409).
  • Resuming keeps the connection's trip history. If it trips again within a day, it pauses for the next, longer window.
  • Contact support if you need it resumed beyond that.

Refusals

Every refusal names its reason in error.

  • Creating or changing a connection (400):
    • sip_connection_source_invalid, sip_connection_source_too_broad, sip_connection_source_not_public, sip_connection_source_is_carrier, sip_connection_sources_required, sip_connection_sources_too_many: the source networks;
    • sip_connection_host_invalid, sip_connection_host_unresolved, sip_connection_host_not_public, sip_connection_port_invalid, sip_connection_transport_invalid: the carrier host;
    • sip_connection_inbound_formats_invalid, sip_connection_dial_format_invalid, sip_connection_caller_format_invalid, sip_connection_codecs_invalid, sip_connection_country_invalid, sip_connection_name_invalid, sip_connection_channel_limit_invalid, and the *_auth_invalid, *_username_invalid and *_password_invalid codes for each direction.
    • More than 30 saves in an hour across the workspace's connections are refused with sip_connection_save_rate_limited (429, with retry_after_seconds). Wait, then save again.
    • A change that would leave an attached number unreachable is refused with sip_connection_number_unreachable_inbound (400). A stale expected_revision is refused with sip_connection_revision_conflict (409).
  • Attaching a number:
    • sip_connection_number_invalid, sip_connection_number_type_unsupported or sip_connection_number_country_unsupported (400): the number itself;
    • sip_connection_number_unreachable_inbound (400): no configured called number format can reach it;
    • sip_connection_needs_hosted_agent (400): connection numbers answer with a Skysay-hosted agent only. A number-rules change to an external agent is refused the same way;
    • number_already_connected or sip_connection_alias_conflict (409): another connection or workspace already holds the number or its digits;
    • sip_connection_number_limit_reached (409).
  • The ownership check:
    • sip_ownership_check_in_progress (409);
    • sip_ownership_check_rate_limited or sip_ownership_check_busy (429, with retry_after_seconds).
  • Readiness checks:
    • sip_connection_probe_in_progress (409), sip_connection_probe_rate_limited or sip_connection_probe_busy (429): the reachability check;
    • sip_connection_disabled, sip_connection_paused or sip_connection_outbound_not_configured (409): the connection cannot be checked or tested now;
    • sip_connection_number_has_no_outbound or sip_connection_number_has_no_inbound (409): the number does not carry that direction;
    • sip_connection_test_call_in_progress (409) or sip_connection_test_call_rate_limited (429): test calls;
    • sip_connection_test_call_not_answered or sip_connection_test_call_stale (409): only a test call answered on the connection's current network can save its format;
    • a test call is refused like any call, for example with sip_ownership_not_confirmed or sip_connection_needs_hosted_agent.
  • setup.reason, when setup could not finish: an sip_connection_* reason, for example sip_connection_host_not_public when the carrier host stopped resolving to public addresses, or sip_connection_setup_busy while another setup holds the platform's lock (setup.state reads pending; saving again or "Check again" retries).
  • Calls:
    • outbound from a number that is not confirmed is refused with sip_ownership_not_confirmed;
    • any call while the connection is paused is refused with sip_connection_paused;
    • any call on a connection number with an agent that isn't Skysay-hosted is refused with sip_connection_needs_hosted_agent.

Detach or disable

POST /v1/sip-connections/{connection_id}/numbers/{number_id}/detach disconnects one number; it stays in your workspace and can be attached again. POST /v1/sip-connections/{connection_id}/disable detaches every number and removes the connection's call routing. Neither changes anything at your carrier.

The exact request and response fields are in the API reference under Numbers.