Signals Docs
SIP Trunks

Authentication and security

Before Signals carries a call from your PBX, it must be sure the call really comes from you. Otherwise anyone who found your trunk could place calls on your account. This page explains the two ways a trunk recognises your PBX, how to manage credentials safely, and how to protect signalling and audio in transit.

Every change on this page is live once the API accepts it and takes effect within a minute. There is nothing to publish. Each edit needs the trunk's current ETag in If-Match and returns a new one. See Saving changes.

Choose an authentication mode

A trunk uses one mode, set by auth_mode when you create it and changeable with PATCH /v1/trunks/{trunk_id}:

IP allow list (ip_allow_list)Digest (digest)
How Signals recognises youThe public IP address the call comes from.A username and password your PBX sends when Signals challenges it.
Best forA PBX or session border controller with a fixed public IP address.A PBX behind a changing address, or several sites sharing one trunk.
What you manageA list of addresses and ranges.Generated credentials, rotated on a schedule.
Secret to protectNone.The password, shown once.

When you can, prefer an IP allow list with TLS. There is no password to leak, and a call from any other address is refused.

IP allow lists

An IP allow list is the set of public addresses Signals accepts calls from. Each entry is an IP credential: a CIDR range, such as 203.0.113.10/32 for one address or 203.0.113.0/28 for sixteen.

Set the list when you create the trunk

Pass the addresses in sources with "auth_mode": "ip_allow_list". The list must not be empty. Each entry must be a public IPv4 address or range. Private ranges such as 10.0.0.0/8 and 192.168.0.0/16, loopback, link-local, multicast and broadcast addresses are refused with 422 invalid_trunk.

JSONCode
{ "auth_mode": "ip_allow_list", "sources": ["203.0.113.10/32", "203.0.113.11/32"] }

Add, change and remove addresses

Manage the list afterwards with the IP credential endpoints:

ActionRequest
ListGET /v1/trunks/{trunk_id}/ip-credentials
AddPOST /v1/trunks/{trunk_id}/ip-credentials with If-Match and Idempotency-Key
ChangePATCH /v1/trunks/{trunk_id}/ip-credentials/{credential_id} with If-Match
RemoveDELETE /v1/trunks/{trunk_id}/ip-credentials/{credential_id} with If-Match

An add or change takes a body like this:

JSONCode
{ "cidr": "203.0.113.16/29", "selector_precedence": 100, "enabled": true }
  • cidr must include the prefix length, for example /32.
  • selector_precedence decides which entry wins when ranges overlap. It defaults to 100.
  • enabled lets you switch an entry off without deleting it. It defaults to true.

Two enabled entries on the same trunk cannot overlap at the same selector_precedence, and the same range cannot appear twice. Either returns 409 trunk_component_conflict.

When you replace a PBX address, add the new address first and remove the old one a minute later. Calls from an address that is not on the list are refused.

Digest credentials

Digest authentication is a challenge and response. Signals challenges a call from your PBX, and the PBX answers with a username and a value derived from the password. The password itself never crosses the network. Signals stores only that derived value (HA1), so it cannot show you the password again later.

Create a digest trunk

Create the trunk with "auth_mode": "digest", an empty sources list and a digest_realm, the name of the authentication domain your PBX answers for. Sending addresses with digest returns 422 invalid_trunk, and so does leaving out digest_realm.

JSONCode
{ "auth_mode": "digest", "sources": [], "digest_realm": "sip.trunk.pressone.co" }

Signals creates the trunk's first credential in the same request. The 201 Created body is { "trunk": { ... }, "digest_password": "..." }. The username is in the trunk's authentication.digest. Copy the password now: this is the only response that contains it, and a replay of the same request returns "digest_password": null.

Create another credential

To add a credential to an existing trunk, for example for a second PBX, create one. Signals generates the username and password. You choose only the realm, the name of the authentication domain your PBX answers for. The realm must be 1 to 255 characters with no spaces or colons. The console uses the platform realm sip.trunk.pressone.co from tls.realm in GET /v1/platform/endpoints.

TerminalCode
curl -X POST "$SIGNALS/trunks/$TRUNK_ID/digest-credentials" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H 'If-Match: "1"' \ -H "Idempotency-Key: 0d6b9e41-8c2f-4a57-b3e0-9f1a7c5d2e84" \ -d '{ "realm": "sip.trunk.pressone.co" }'
JSONCode
{ "id": "01992f3a-91b0-7c55-8e21-4a6d0f3b9c18", "username": "sig_01992f3a91b07c558e214a6d0f3b9c18", "realm": "sip.trunk.pressone.co", "enabled": true, "valid_until": null, "rotated_from_id": null, "new_applied_at": null, "previous_retires_at": null, "created_at": "2026-10-01T10:02:11Z", "revoked_at": null, "password": "01992f3a91b27e04a5c8d1f60b3e7a2901992f3a91b27e04b9d2c4a81e6f0d35", "secret_unavailable": false }

Copy the password into your PBX or a secrets manager now. This is the only response that contains it.

The password is shown once

  • GET /v1/trunks/{trunk_id}/digest-credentials returns usernames and lifecycle dates, never the password or HA1.
  • If your request times out and you retry it with the same Idempotency-Key, you get the same credential back with "password": null and "secret_unavailable": true. The password is never disclosed twice.
  • If you lose a password, rotate the credential or revoke it and create a new one.

A digest trunk must keep at least one enabled credential. A change that would leave it with none returns 422 with a detail that starts with the trunk would not apply:.

Rotate with overlap

Rotation replaces a credential without dropping calls. Signals creates a new username and password and keeps the old credential valid for 24 hours after the network has applied the new one. That overlap gives you a day to update every PBX.

  1. Rotate the credential. The response contains the new password, once:

    TerminalCode
    curl -X POST "$SIGNALS/trunks/$TRUNK_ID/digest-credentials/01992f3a-91b0-7c55-8e21-4a6d0f3b9c18/rotate" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H 'If-Match: "4"' \ -H "Idempotency-Key: 7e2c0a95-1d4b-4f86-a9c3-5b8e2d6f0a17"
    JSONCode
    { "id": "01992f9c-2e70-7a13-9b54-8c1d3f6a0e29", "username": "sig_01992f9c2e707a139b548c1d3f6a0e29", "realm": "sip.trunk.pressone.co", "rotated_from_id": "01992f3a-91b0-7c55-8e21-4a6d0f3b9c18", "new_applied_at": null, "previous_retires_at": null, "password": "01992f9c2e72705d8a41c6e93f0b7d2401992f9c2e727e1fb6a0d39c4e85f172", "secret_unavailable": false }
  2. Within a minute the network accepts the new credential. Its new_applied_at is then set, and previous_retires_at shows when the old credential stops working, 24 hours later.

  3. Update each PBX with the new username and password before previous_retires_at.

Revoke a credential

DELETE /v1/trunks/{trunk_id}/digest-credentials/{credential_id} with If-Match revokes the credential and returns 204 No Content. The network stops accepting it within a minute.

TLS for signalling

SIP messages carry phone numbers, caller IDs and, with digest, authentication exchanges. TLS encrypts them between your PBX and Signals. Signals accepts SIP over TLS on port 5061 at the signalling address in GET /v1/platform/endpoints, and over UDP or TCP on port 5060.

Set require_tls to true to keep a trunk on TLS. With require_tls on, every termination target must use "transport": "tls", so Signals reaches your PBX over TLS too. A target with udp or tcp returns 422 invalid_trunk. A target written as a sips: URI must always use tls.

JSONCode
{ "require_tls": true, "targets": [ { "sip_uri": "sip:203.0.113.10:5061", "priority": 10, "weight": 100, "transport": "tls" } ] }

Point your PBX at port 5061 with TLS, and verify the Signals certificate. The tls object in the platform endpoints response gives the TLS name (realm) and any certificate chain information (trust_chain).

Source validation

source_validation takes relaxed or strict. strict also checks the SIP Via header against your allowed sources, so a request has to come from, and claim to come from, an address you listed. Use strict with an IP allow list. Keep relaxed with digest, where your PBX address may change.

Media security

Audio travels as RTP, separately from SIP. Two settings control it.

media_security decides whether audio is encrypted:

ValueWhat happens
rtpAudio is plain RTP.
sdes_srtp_requiredAudio must be SRTP, encrypted with keys exchanged in the SDP (SDES). A call that does not negotiate SRTP is refused rather than downgraded to plain RTP.

SDES keys travel inside the SIP messages, so SRTP only protects audio when signalling is also encrypted. Pair sdes_srtp_required with require_tls: true and a TLS connection from your PBX.

codecs decides which audio codecs the trunk offers. G.711 (PCMA and PCMU) is always on; add G729, G722 or OPUS to turn those on too. A repeated codec is refused. Signals offers them to your PBX in a fixed order, OPUS, G722, G729, PCMA, PCMU, whatever order you send.

Audio flows through the Signals media address in media_cidrs. Your PBX never exchanges audio directly with the far end.

Limits as a safety control

Limits stop a compromised PBX or a dialling loop from running up cost:

  • On the trunk: concurrency, cps and max_call_duration_seconds.
  • On the outbound policy: the same three, plus max_destination_rate_minor and daily_spend_limit_minor. See Outbound policies.

Signals applies every limit that covers a call, and the tightest wins.

Checklist

  • Use an IP allow list with strict source validation when your PBX has a fixed public address.
  • Turn on require_tls and connect on port 5061.
  • Use sdes_srtp_required when audio must be encrypted.
  • Store digest passwords in a secrets manager, never in tickets or chat.
  • Rotate digest credentials on a schedule.
  • Set a daily spend cap on every outbound policy.
Last modified on