Documentation
SIP integration
Ask AI about this page
Point an IP-authenticated SIP trunk at the ingress host on your account, port 5060 over UDP or TCP, no registration, no credentials, G.711 in the INVITE. Open Voice Shield authorizes by source IP, anchors the media to record it, and relays the call to the destination you configure.
The shape of the connection#
Open Voice Shield is a SIP proxy in the path of calls you choose to send through it. Your switch sends an INVITE to our ingress; we check the source address, anchor the media, and forward the same call to a destination you have configured. There is no B2BUA, no media injection and no re-origination. The far end sees your call, not ours.
- Signalling is a normal SIP hop that carries your dialled number, caller ID and headers.
- Media is relayed through us in both directions, which is what makes the recording possible.
- The final response your switch sees is the far end's own response, unless we refused the call ourselves, see what your switch gets back.
One trunk, or one dial pattern
You do not have to move all your traffic. Most operators start with a single route, campaign, queue or dial pattern, and keep the existing direct route as a fallback.
Where to send the INVITE#
The ingress address is per account. Read it from the Network page in the portal, or from the API:
GET /customer/network-info
curl -s https://ovs.telecomsxchange.com/api/customer/network-info -H "Authorization: Bearer $TOKEN"
# {"ingress_host":"192.0.2.10","ingress_port":5060,"transports":["udp","tcp"]}
| Parameter | Value |
|---|---|
| Host | the ingress_host on your Network page |
| Port | 5060 |
| Transports | udp, tcp |
| TLS towards us | not offered on the ingress. TLS outward, from us to your destination, is supported. |
| Request-URI user | the dialled number |
| From user | the caller ID (ANI). Automation rules act on this value. |
| Registration | none: see below |
The same host is the source address of everything we send onward, so your destination has to trust it. TCP is the safer choice when your INVITEs carry large SDP.
Authentication is by source IP#
There is no SIP account, no password and no registrar. A REGISTER is answered 403 Registration Not Supported. An INVITE is accepted only when its source address falls inside a CIDR entry on your whitelist, so add the public address your switch sends from, the address after NAT, not the private one on its interface.
| Rule | Detail |
|---|---|
| Format | CIDR. A single host is /32 (IPv4) or /128 (IPv6). |
| Widest accepted | /16 for IPv4, /48 for IPv6. Anything broader is refused 422 CIDR too broad. |
| Exclusivity | A range belongs to one account platform-wide. Overlapping a range held by another account returns 409. |
| Several matches | The longest prefix wins; the oldest entry breaks a tie. |
| How many | Up to 50 entries per account. |
| Per-entry destination | An entry can name its own destination, so one site's traffic can leave to a different place. |
| Turning one off | enabled: false keeps the entry and stops accepting calls from it. |
Check the address you actually send from
A switch behind NAT, a multi-homed SBC or a cloud instance with a separate egress address will send from something other than the address written in its configuration. The source address we see is the only one that counts.
Several sub-customers behind one IP#
If one switch carries traffic for several of your own customers, give each of them a dial prefix on the same IP. Your switch puts the prefix in front of the number it dials, we label the call with that sub-customer, remove the prefix and send the call on as usual. The Calls list then shows, and filters by, whose call it was.
Two sub-customers on 174.136.44.213
curl -s -X POST https://ovs.telecomsxchange.com/api/customer/ips \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"cidr":"174.136.44.213/32","dial_prefix":"1234#","label":"Sub customer A"}'
curl -s -X POST https://ovs.telecomsxchange.com/api/customer/ips \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"cidr":"174.136.44.213/32","dial_prefix":"2222#","label":"Sub customer B"}'
# INVITE sip:1234#15551234567@... is Sub customer A's call, relayed as sip:15551234567@...
| Rule | Detail |
|---|---|
| Format | 1 to 16 digits, * and #. A # sent escaped as %23 matches too. |
| Which entry | The longest prefix the dialed number starts with, among your entries for that source IP. |
| No prefix matches | An entry for the IP without a prefix takes the call unchanged. If the IP has prefixed entries only, the call is refused 403 Prefix not whitelisted. |
| After the match | The number without the prefix is what your routing rules, the destination's tech prefix, the call record and the bill see. |
| Per sub-customer route | Each prefixed entry can name its own destination. |
| On the call | sub_customer and dial_prefix in the API, the webhook and the export. Filter the list with GET /calls?sub_customer=. |
Trunk settings to enter on your switch#
Every switch has its own name for these (peer, trunk, carrier, gateway, endpoint), but the settings are the same. Create an IP-authenticated entry with no registration and route the traffic you want analyzed through it.
Step-by-step versions of this page exist for your Asterisk, FreeSWITCH, Kamailio, OpenSIPS or any other SIP-capable switch.
| Setting | Value | Why |
|---|---|---|
| Peer host / proxy / SIP server | the ingress_host on your Network page | Where the INVITE goes. |
| Port | 5060 | The only SIP port on the ingress. |
| Transport | UDP or TCP | Pick one. Both are accepted on the same port. |
| Authentication / credentials | none | Authorization is by source IP. |
| Registration | off | A REGISTER is answered 403. |
| Codecs offered | PCMU, PCMA first; G.722, G.729, G.723.1 also decoded | Anything else cannot be transcribed. See codecs. |
| DTMF | telephone-event (RFC 2833) may be offered | It is relayed like any other payload type. |
| SDP | early offer, in the initial INVITE | An INVITE with no SDP is answered 488. |
| SRTP / media encryption | off | The ingress relays plain RTP. Outward, a voice agent that requires SRTP (OpenAI) gets it from the platform; your side stays plain. |
| ICE | off | ICE attributes are removed from the SDP. |
| Session timers | optional | Re-INVITEs and UPDATEs are relayed; they do not extend the call limit. |
| Keepalive | SIP OPTIONS | Answered 200 OK from any address. |
| Max-Forwards | leave at the default | A request that arrives with Max-Forwards already at zero is answered 483. |
| Fallback route | your existing direct route | So a 503 takes the old path instead of failing the call. |
Firewall, both directions#
Signalling and media use different ports and the media leg is the one people forget. Media is plain RTP on UDP 30000–40000 at the ingress address.
| Direction | Protocol and ports | Carries |
|---|---|---|
| Out, to the ingress host | UDP or TCP 5060 | Your INVITEs and your OPTIONS keepalive. |
| Out, to the ingress host | UDP 30000–40000 | Your RTP towards us. |
| In, from the ingress host | UDP 30000–40000 | The far end's RTP, relayed back to you. |
| In, from the ingress host | the SIP port of your destination | The call, relayed onward from our address. |
| In, from the ingress host | your destination's RTP range | The media, relayed onward from our address. |
If the destination is your own SBC or PBX, add the ingress host to its trusted peers or ACL before the first test call. Rejection by your own equipment is the most common first-call failure.
Codecs and media#
The ingress does not transcode. Whatever your switch and the far end agree on is what gets relayed, and what gets recorded.
| Codec | Relayed | Analyzed |
|---|---|---|
| G.711 µ-law (PCMU, payload type 0) | yes | yes |
| G.711 A-law (PCMA, payload type 8) | yes | yes |
| G.722 (payload type 9) | yes | yes, wideband |
| G.729, G.729B (payload type 18) | yes | yes, decoded after hang-up; some transcription accuracy is lost to the 8 kbit/s codec |
| G.723.1, 6.3 and 5.3 kbit/s (payload type 4) | yes | yes, decoded after hang-up; same caveat |
| telephone-event (RFC 2833) | yes | not speech: ignored by the transcript |
| Comfort noise (payload type 13) | yes | ignored inside a G.711 stream |
| Anything else | yes | no, the call is relayed and billed, but produces no transcript and no analysis |
Offer a codec we decode or you get no verdict
The recording is decoded from the relayed stream after hang-up, at 8 kHz, one channel per direction; nothing is transcoded while the call is up. A call that negotiates a codec outside the table still completes normally but has nothing to transcribe. G.711 first gives the best transcription. G.729 and G.723.1 work, with a little less accuracy.
The media is anchored: the SDP your switch receives points at the ingress media address, in both directions, for the whole call. ICE is stripped, the origin and connection lines are rewritten, and the audio is captured while it is relayed. Nothing is injected into the audio and neither party hears anything added.
Where the call goes next#
A destination is a host, port and transport we relay the accepted call to: your SBC, PBX or carrier. Up to 20 per account, with udp, tcp or tls outward, independent of the transport you use towards us.
The destination for a given call is chosen in this order, first match wins:
- an active divert control on the caller's ANI, placed by one of your automation rules;
- a routing rule whose prefix is the longest leading match of the dialled number;
- the destination attached to the whitelist entry that matched the source IP;
- your default destination;
- failing all of those, back to the source address on 5060/udp.
Prefix routing#
A routing rule maps a leading digit string on the dialled number to one of your destinations. Use it to send a test range, a country, or a voice-agent number somewhere other than the default while everything else follows the normal path.
POST /customer/routing-rules
curl -s -X POST https://ovs.telecomsxchange.com/api/customer/routing-rules \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"prefix":"1800","destination_id":"<destination id>","priority":100,"label":"toll-free to SBC B"}'
| Field | Rule |
|---|---|
| prefix | 1 to 15 digits. Matched against the leading digits of the dialled number. |
| Which rule wins | The longest matching prefix. On equal length, the highest priority, then the most recently updated rule. |
| priority | 0 to 100000, default 100. It is a tie-breaker only and never beats a longer prefix. |
| enabled | false takes the rule out of matching without deleting it. |
| Duplicates | A second rule for the same prefix returns 409. |
| No match | Selection falls through to the whitelist entry's destination, then the default. |
Tech prefix#
Carriers that identify a trunk by a leading digit string want that string on the number they receive, not on the number you dial. Set tech_prefix on the destination and we prepend it to the Request-URI user before relaying: dialling 15551234567 through a destination with tech prefix 009 leaves us as 00915551234567.
- Characters 0-9, * and # only, up to 32 of them.
- It is a property of the destination, so every call routed there gets it.
- The call record keeps both the dialled number and the prefix that was applied.
- Clear it by sending an empty value.
Digest authentication towards the destination#
Some carriers and PBXs whitelist our IP and also challenge the INVITE with 401 or 407. Store the credential they gave you on the destination (auth_username, auth_password, optional auth_realm, or Network → the destination → Advanced → SIP authentication) and the proxy answers the challenge and re-sends the INVITE. Your switch never sees the challenge.
- Optional. Without a credential, a challenge from the destination reaches your switch as before.
- Write-only: stored sealed, never returned. The destination shows has_credential.
- Leave the realm blank to answer whatever realm the destination challenges with.
- A wrong credential ends the call with the destination's own reply, reported once.
- Only the INVITE is answered. A destination that also challenges BYE or re-INVITE is not supported.
Routing to a voice agent#
A destination of kind voice_ai is an AI voice agent reached as a SIP endpoint, so a screened call, or a caller diverted by an automation rule, goes to the agent instead of a human queue. Two providers are supported. Each has its own endpoint, identifier and connection rules, and GET /customer/voice-providers returns them exactly as the platform applies them.
| Provider | Endpoint | agent_id | Credential |
|---|---|---|---|
| xai | sip.voice.x.ai | the agent's Direct SIP number in E.164; a bare number gets the leading + | the agent may challenge the INVITE: store its digest credential on the destination (auth_username, auth_password, optional auth_realm) |
| openai | sip.api.openai.com | your OpenAI project id (proj_…) | no digest. OpenAI asks a webhook to accept each call and the platform hosts it for you: register the destination's webhook_url in your project and store the signing secret and a project API key on the destination (setup) |
POST /customer/destinations
curl -s -X POST https://ovs.telecomsxchange.com/api/customer/destinations \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"kind":"voice_ai","provider":"openai","host":"sip.api.openai.com","port":5061,"transport":"tls",
"agent_id":"proj_abc123","label":"after-hours agent"}'
- The credential is write-only: it is stored sealed, never returned, and has_credential / needs_credential on the destination tell you where you stand. An xAI agent without one is flagged on the Network page.
- The agent leg is metered per second and shown on the call record as agent_seconds with the rate applied; the recording, transcript, verdict and per-leg quality are produced as for any other destination.
- A voice-agent destination is monitored with SIP OPTIONS like any other, and the destination-down alert applies.
OpenAI: the hosted webhook
OpenAI does not answer a SIP call on its own. Every call to a project raises an incoming-call event on the project's webhook, and the call connects only when that webhook accepts it with a session: the model, the voice, the instructions. The platform hosts that webhook for you, one URL per destination, so nothing of yours has to be online to take the call.
- Create the destination (the request above, or Network → Add destination → Voice AI → OpenAI). The record comes back with its webhook_url, https://ovs.telecomsxchange.com/api/hooks/openai/<token>; the Network page shows it with a copy button.
- In the OpenAI dashboard, Settings → Project → Webhooks: add that URL and subscribe it to the incoming-call event of the model family you want: live.transport.incoming for GPT-Live (the default, GPT-Live-1), or realtime.call.incoming for the Realtime models. Copy the signing secret OpenAI shows.
- Create an API key in the same project (Settings → API keys).
- Store both on the destination: webhook_secret and provider_api_key, in the portal or with a PATCH. They are write-only and stored sealed; has_webhook_secret and has_provider_key confirm they are there, and the Needs webhook setup flag clears.
- Optionally set the agent: config takes model (default GPT-Live-1 for the GPT-Live event, a Realtime model for the Realtime event), voice and instructions; the portal has fields for the three.
- Place a call. The destination records the last event (webhook_last_status accepted, or failed with the reason, for example an API key the project refuses), and the call record carries agent_call_ref, OpenAI's id for the call.
PATCH /customer/destinations/{id}
curl -s -X PATCH https://ovs.telecomsxchange.com/api/customer/destinations/$DEST \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"webhook_secret":"whsec_…","provider_api_key":"sk-…",
"config":{"model":"gpt-live-1","voice":"marin","instructions":"You are the after-hours agent for Example Corp."}}'
- Every event is checked against the signing secret (the Standard Webhooks signature, five-minute tolerance); an unsigned, stale or mis-signed event is refused and the URL is a 32-byte secret of its own. rotate_webhook_token: true on a PATCH mints a new URL and retires the old one; clear_webhook_secret and clear_provider_key drop the secrets.
- GPT-Live requires encrypted media. The platform offers SRTP on the agent leg and keeps your side on plain RTP, as for any other destination, so there is nothing to configure on your switch.
- The API key is used for one thing: accepting or rejecting the call the event announces. It is never returned and never logged.
Putting an agent behind an existing switch, SBC or PBX that has no TLS or SRTP? The connect any SBC to a voice agent guide is the four-step version, and the voice agent gateway page explains what the platform does in between.
Destination monitoring#
Every destination is checked with a SIP OPTIONS request the moment it is saved, and again whenever its host, port or transport changes. A destination that answers is monitored from then on: one OPTIONS a minute from the ingress host, with the verdict, the last check time and the round-trip time shown next to it on the Network page and returned on the destination record. A destination that never answers is left alone (status unknown, "no reply") and tried again once a day, or whenever you press Check now.
| Field | Meaning |
|---|---|
| monitor_status | up once it answers; down after two consecutive checks without an answer, or a 503; unknown before the first answer or while monitoring is off. |
| monitor_enabled | Switched on automatically the first time the address answers. Set it to false to stop, and that sticks; true to keep checking an address that has not answered yet. |
| monitor_supported | null until the first check, true once it has ever answered, false when it never has. |
| monitor_last_check_at | When the last OPTIONS was sent. |
| monitor_last_change_at | When the status last flipped. The hook for heartbeat alerts. |
| monitor_last_code | The SIP response code of the last check. Any code counts as an answer (405 and 403 included) except 503 Service Unavailable. |
| monitor_rtt_ms | Round trip of the last answered check, in milliseconds. |
| monitor_last_error | Why the last check got no answer: timeout, unreachable, dns, tls or blocked (the name now resolves to an address we refuse to send to). |
POST /customer/destinations/{id}/check
curl -s -X POST https://ovs.telecomsxchange.com/api/customer/destinations/<destination id>/check \
-H "Authorization: Bearer $TOKEN"
# {"id":"...","host":"sbc.example.net","port":5060,"transport":"udp", ...
# "monitor_enabled":true,"monitor_supported":true,"monitor_status":"up",
# "monitor_last_check_at":"2026-09-06T14:02:11Z","monitor_rtt_ms":38,"monitor_last_code":200,"monitor_last_error":null}
- The check comes from the ingress host, so a destination that only trusts that address still answers. If yours answers INVITE but not OPTIONS, allow OPTIONS from the ingress host and press Check now.
- Over TLS the certificate is not verified for the check; self-signed trunk certificates would otherwise read as down.
- Heartbeat alerts: an alert rule with the trigger Destination down emails its recipients, or posts to its Slack channel, when a monitored destination goes down, and again when it comes back. The rule's throttle applies.
- Agents get the same fields from list_destinations and can run a check with check_destination, see the agents guide.
Carrying your own reference on the call#
Two SIP headers on the initial INVITE are captured and stored on the call record, so a verdict can be joined back to the campaign, agent or ticket that produced it: X-Customer-ID and X-Customer-Name. Values are trimmed to 256 characters and appear as external_id and external_name on the call. The captured set is fixed platform-wide and is not customer-configurable. Write to support@telecomsxchange.com if you need different headers.
What your switch gets back#
If we refuse the call, the response comes from us and comes fast, before any INVITE is sent onward. If we accept it, everything your switch sees after that, 180, 183, 200, a busy, a rejection, is the far end's own response, relayed unchanged.
| Response | Reason phrase | Means |
|---|---|---|
| 200 | OK | Answer to an OPTIONS keepalive. Sent from any source address, it proves the path, not the whitelist. |
| 402 | Payment Required | Prepaid balance will not cover a minute at your rate. |
| 403 | IP not whitelisted | The source address is not in any enabled CIDR entry on your account. |
| 403 | Prefix not whitelisted | The source IP is whitelisted with dial prefixes only, and the dialed number starts with none of them. |
| 403 | Account suspended | The account is not active. |
| 403 | Blocked by rule | An automation rule of yours has this caller ID under an active block. The attempt is written to your call records. |
| 403 | Registration Not Supported | A REGISTER arrived. We are a proxy, not a registrar. |
| 403 | Destination Not Allowed | The resolved destination points at loopback, link-local or the ingress itself. |
| 405 | Method Not Allowed | An out-of-dialog method other than INVITE, ACK, CANCEL, OPTIONS or REGISTER. |
| 481 | Call/Transaction Does Not Exist | A CANCEL or in-dialog request that matches no live transaction or dialog. |
| 483 | Too Many Hops | Max-Forwards reached zero, or an in-dialog request would route back to the ingress itself. |
| 486 | Busy Here | You are already at your plan's concurrent call limit. |
| 487 | , | Your own CANCEL was honoured. |
| 488 | Not Acceptable Here | The INVITE carried no SDP. Send an early offer. |
| 503 | Service Unavailable | The call could not be authorized or relayed. Take your fallback route. |
| 503 | Media Relay Unavailable | The media relay could not take the call, so it was refused rather than carried without recording. |
| 500 | Server Internal Error | An admission check failed for an unclassified reason. |
A malformed request is dropped without a reply. A retransmitted INVITE, same Call-ID and same From-tag, gets the answer already given to the original and does not consume a second port or a second balance check.
How a call ends#
A BYE from either side ends the call normally and is relayed. Two limits can also end it from our side, and both send BYE to both parties:
| Limit | Value | Behaviour |
|---|---|---|
| Credit cut-off | what your remaining balance buys, never less than 60 s | Measured from answer. Re-INVITEs do not extend it. |
| Maximum call lifetime | 4 hours by default | An absolute upper bound on any dialog. |
Run a test call#
- OPTIONS. Send SIP OPTIONS from the switch to sip:ping@<ingress_host>:5060. A 200 OK proves the path and the port.
- Confirm the source address. Place one call and, if it is rejected 403, compare the address in your own SIP trace with the CIDR entries on the account.
- Place a real call over the new route with at least 30 seconds of two-way speech, and talk on both legs. A leg that never speaks produces a one-sided transcript.
- Check the media. Both parties should have audio in both directions. One-way audio is a firewall rule on the RTP range, not a signalling problem.
- Read the call record a minute after hang-up: the dialled number, the destination that was chosen, the route prefix and tech prefix applied, the SIP log and the verdict are all on it.
GET /calls: the routing decision that was actually applied
curl -s "https://ovs.telecomsxchange.com/api/calls?limit=1" -H "Authorization: Bearer $TOKEN"
# {"total":1,"items":[{"id":"...","ani":"15550001111","dialed":"15551234567",
# "dest_host":"203.0.113.10","dest_port":5060,"dest_transport":"udp",
# "route_prefix":"1800","dest_tech_prefix":null,"status":"completed",
# "sip_code":200,"end_reason":"bye","billsec":42,"analysis_status":"done"}]}
Before you send production traffic#
- The public egress address of every switch that will send to us is whitelisted.
- The ingress host is a trusted peer on your destination, for SIP and for RTP.
- UDP 30000–40000 is open outbound and inbound between you and the ingress.
- The trunk offers PCMA and PCMU, sends SDP in the INVITE, and does not register.
- A default destination exists, and any prefix rule you added resolves to the right one.
- The account has credit, and the plan has enough ports for your busy hour.
- Your fallback route is still in place for 503.
Frequently asked questions#
Does Open Voice Shield support SRTP or TLS towards the ingress?
No. The ingress accepts plain SIP over UDP or TCP on port 5060 and plain RTP, because encrypted media cannot be captured and decoded for transcription. TLS is supported outward, from us to your destination, SRTP is offered outward to a voice agent that requires it (OpenAI), and the portal and API are HTTPS.
Which codecs are analyzed?
G.711 (PCMU, PCMA), G.722, G.729 and G.723.1. Any other codec is relayed and billed normally but produces no transcript and no analysis, so put PCMU and PCMA at the top of the offer on this trunk.
Which ports do I open for media?
UDP 30000 to 40000 at the ingress address, in both directions. Signalling is UDP or TCP 5060. One-way audio is almost always a firewall rule on that RTP range, not a signalling problem.
Why is my call answered 403?
The source address of the INVITE is not inside an enabled CIDR entry on your account, the account is suspended, the caller ID is under an active block from one of your automation rules, or the request was a REGISTER. The reason phrase names which.
Next: Concepts explains what happens to the call after the BYE and how to read the verdict. If something is being rejected, Troubleshooting lists each code and the check that clears it.