Documentation
Troubleshooting
Every symptom below is listed as your switch or your portal shows it, with the cause and the one check that confirms it. Start with the code your switch logged on the INVITE, or with the call record in Calls — between them they identify almost every failure without anyone reading a log.
Three places the answer is#
Nearly every problem is identified by one of these before anybody opens a log file.
- The response your switch logged on the INVITE. The code and its reason phrase name the check that failed.
- The call record. Calls in the portal, or GET /calls. The fields that matter are status, sip_code, end_reason, billsec, transcript_status, analysis_status and processing_error.
- The evidence on the call. Each call record carries the SIP message flow, the recording, the transcript and the media capture, and can be exported as one offline bundle.
the fields worth reading first
API=https://your-ovs-host.example # your API base URL
OVS_KEY=ovs_... # an API key from Developers in the portal
curl -s "$API/api/calls?limit=5" -H "X-API-Key: $OVS_KEY"
# {"total":1,"items":[{"id":"...","status":"completed","sip_code":200,"end_reason":"bye",
# "billsec":42,"transcript_status":"done","analysis_status":"done","processing_error":null, ...}]}
A keepalive proving nothing
SIP OPTIONS is answered 200 OK for any source address. A green trunk in your switch's monitor proves the network path and the port — it does not prove your IP is whitelisted. Only an INVITE is checked.
Calls rejected with 403#
Three different checks answer 403, and they mean opposite things: one is your configuration, one is your own automation working as intended, and one is your account. The reason phrase tells them apart, and so does whether a call record appears.
| Reason phrase | Means | Appears in Calls? |
|---|---|---|
| IP not whitelisted | The source address of the INVITE is not inside any enabled entry on your IP list. | No record at all. The call is refused before anything is written. |
| Blocked by rule | The caller's number is under an active control created by one of your automation rules. | Yes — status: failed, sip_code: 403, end_reason: blocked_by_rule, nothing billed. |
| Account suspended | The account is suspended. Every authenticated API request answers 403 Account suspended too. | No new record. Contact your operator. |
403 IP not whitelisted#
Whitelisting is by the public source address of the packet — the address after NAT. On most deployments that is the SBC's egress address, not the PBX's own address, and the two are frequently different.
GET /customer/ips — what is actually on the list
curl -s $API/api/customer/ips -H "X-API-Key: $OVS_KEY"
# [{"id":"...","cidr":"203.0.113.10/32","label":"sbc-1","destination_id":null,
# "enabled":true,"created_at":"..."}]
- Is the entry enabled? A disabled entry is skipped entirely. enabled must be true.
- Is the address the one that leaves your network? Confirm it from an outbound test on the switch itself, not from its interface configuration.
- Did the switch fail over? A second SBC, a new node or a changed provider address sends from an address nobody added.
- Did an addition fail? 422 CIDR too broad: minimum prefix length is /16 means the range is wider than policy allows (/16 for IPv4, /48 for IPv6); 409 CIDR overlaps a range registered to another account means the range is already claimed — narrow it to the exact address you send from.
403 Blocked by rule#
This is your own automation. A rule of the form "probability at or above N and the recommendation is block → block this caller for H hours" puts the caller's number under a control, and every later call from that number is refused at the ingress until the control expires or you release it.
which number is controlled, and why
curl -s $API/api/automation/controls -H "X-API-Key: $OVS_KEY"
# {"total":1,"limit":50,"offset":0,"items":[{"id":"...","ani":"+15550100","action":"block",
# "reason":"...","expires_at":"...","released_at":null,"created_at":"..."}]}
curl -s -X POST $API/api/automation/controls/<control_id>/release -H "X-API-Key: $OVS_KEY"
By default the list shows only controls that are in force right now; add include_released=true to see released and expired history. A blocked attempt is deliberately written to the call record so the block is auditable — that record is how you prove to a caller that they were rejected, and when.
Other codes your switch sees#
| Response | Cause | Check or fix |
|---|---|---|
| 402 Payment Required | The prepaid balance does not cover one minute at your rate, or a subscription renewal failed. | GET /customer/balance and the ledger. Top up or subscribe. |
| 486 Busy Here | Calls already ringing or active for the account have reached the plan's concurrent limit. | GET /customer/live-calls. If nothing is really up, unanswered rows clear themselves about five minutes after they started. |
| 488 Not Acceptable Here | The INVITE carried no SDP. A late offer is not supported. | Configure the trunk to send an early offer. |
| 403 Destination Not Allowed | Your destination points at loopback, link-local, multicast or at the ingress itself. | PATCH /customer/destinations/{id} to a routable public address. |
| 403 Registration Not Supported | The trunk is trying to REGISTER. There is no registrar and no SIP password anywhere. | Switch the trunk to IP authentication with registration off. |
| 405 Method Not Allowed | An out-of-dialog request other than INVITE, OPTIONS or REGISTER. | Nothing to fix on our side — check what the switch is sending. |
| 483 Loop Detected | The route set points back at the ingress: the destination is us, not your PBX. | Correct the destination so the onward leg terminates on your equipment. |
| 478 | The destination hostname does not resolve. | Use an address that resolves publicly, or configure the destination by IP. |
| 408 Request Timeout | Your destination never sent a final response. The onward INVITE times out after two minutes. | Confirm the destination accepts SIP from the ingress address on its signalling port — this is a firewall rule people forget on the return path. |
| 503 Service Unavailable | We could not accept the call or could not relay it. | Let the call take your fallback route, then raise it with your operator with the time and the SIP Call-ID. |
| any other 4xx / 5xx / 6xx | Your destination's own response, relayed back unchanged. | Read it as if you had dialled the destination directly — 404 is an unknown number there, 403 usually means it does not trust the ingress address yet. |
| no reply at all | The request never arrived, or was malformed and dropped. | Check that your firewall allows outbound SIP to the ingress on the transport you configured, and that you are sending to the port your account was given. |
Keep a fallback route
Route the traffic you want analyzed through us, and leave your direct route in place behind it. A 503 should cost you an analysis, never a call.
No audio, or audio in one direction only#
The media relays through us, so both endpoints must be able to reach our media address, and we must be able to reach both of them. One-way audio is almost always one firewall rule.
| Check | Why |
|---|---|
| Is the RTP port range open inbound? | Signalling and media are different ports. Opening 5060 alone gives you a call that connects in complete silence. The range is on the Network page — see Getting started. |
| Is it open from any address? | Media can arrive from a different host than the signalling. A rule that only permits the signalling address drops the audio. |
| Does your destination accept media from us? | The onward leg's RTP comes from our media address, not from your switch. The far end needs the same rule. |
| Is SRTP or ICE still enabled on the trunk? | Encrypted media is not supported towards the ingress and ICE candidates are stripped. Offer plain RTP. |
| Did the analysis say so already? | processing_error: no_rtp_streams means no decodable audio ever arrived; a transcript with one silent side means one direction never did. |
The call connected but there is no verdict#
A verdict normally appears within about a minute of hang-up. When it does not, analysis_status and processing_error on the call record say why. Analysis runs only for answered calls that finished and lasted at least the minimum analysis length.
| What you see | Means | What to do |
|---|---|---|
| skipped | The call was never answered, or was shorter than the minimum analysis length (five seconds by default). | Expected. Test with at least 30 seconds of two-way speech. |
| pending | Queued. The first attempt starts a few seconds after the call ends. | Give it a minute. Longer than that on every call is an operator-side problem — report it. |
| processing | Transcription or analysis is running. Long calls take proportionally longer. | Wait. Stalled work is re-queued automatically. |
| unsupported_codec:<n> | The call negotiated something other than G.711. It was still relayed and still billed, but it cannot be transcribed. | Offer only PCMA and PCMU towards the ingress, then ask your operator to re-run the call. |
| no_rtp_streams | The capture holds no decodable audio: media never reached us, or it was encrypted. | Check the media path. |
| recording_not_found | No capture was found for the call after several retries. | Usually media that never flowed. If audio was fine on the call, report it with the call id. |
| storage_error: … | Storage of the recording failed. Retried with backoff. | Operator-side. Report it with the call id. |
| stt_error: … | Transcription failed. empty_wav means the audio had no samples at all. | For empty_wav, check the media path; anything else is operator-side. |
| analysis_error: … | The analysis call failed and was retried to exhaustion. | Operator-side. Report it with the call id. |
| done, recommendation review | A verdict exists but the analysis declined to score the content and fell back to review. | Read the transcript and judge it by hand. |
GET /calls/{id}/analysis answers 404 Analysis not available (status: …) and GET /calls/{id}/transcript answers 404 Transcript not available (status: …) whenever the status is not done — the status in the message is the same one the table above explains.
A call sits in ringing and never clears#
ringing means we relayed your INVITE and are still waiting for a final response from your destination. It resolves one of three ways:
- The destination answers late
- Normal. The onward INVITE is given two minutes before it times out with 408.
- Nothing ever comes back
- The row is closed automatically five minutes after it started, with status: stale and billsec 0. Nothing is charged. Repeated stale rows mean your destination is not reachable from the ingress address, or is silently dropping the INVITE.
- The record is holding a port
- Unanswered rows count towards your concurrent limit until they are closed, so a run of unreachable calls can produce 486 Busy Here on good ones. Fix the destination and the backlog clears itself.
An answered call that ends as stale is different: the call really happened, the end-of-call notification did not arrive, and the record was finalized afterwards — billed up to the limit that was applied to it and still analyzed.
A recording, transcript or capture is missing#
| Symptom | Cause |
|---|---|
| GET /calls/{id}/recording → 404 Recording not available | The retention window for recordings has passed, or the call was never analyzed. The call record keeps its metadata either way. |
| GET /calls/{id}/pcap → 404 PCAP not available | Media captures have their own, much shorter retention window and are removed once they have been converted. |
| The SIP flow on a call is empty | The call predates SIP tracing on your deployment, or its rows have passed the SIP-log retention window. An empty flow is not an error. |
| A transcript with one silent side | A channel that is silent throughout is not transcribed. One direction of audio never arrived — check the media path. |
| 429 too_many_exports | You are exporting evidence bundles faster than the rate limit allows. The Retry-After header says how long to wait. |
Retention windows are set by the operator of your deployment, separately for recordings, media captures and SIP logs. If you need a call kept, export it — the bundle is self-contained and opens with no account and no network. See Security for how deletion works.
Alert e-mails are not arriving#
Work down this list. The test button under Alerts settles most of it in one click.
POST /customer/alert-rules/{id}/test — send a sample to the rule's recipients
curl -s -X POST $API/api/customer/alert-rules/<rule_id>/test -H "X-API-Key: $OVS_KEY"
# {"sent":true,"error":null,"latency_ms":412,"recipients":["ops@acme.example"]}
# 409 when e-mail delivery is not configured on the deployment
- A 409 on the test means outbound e-mail is not configured on your deployment at all. That is an operator setting — raise it with them.
- Is the rule enabled, and does the call actually match it? A rule fires only when the probability is at or above its threshold and the category and recommendation are in its lists. An empty list means "any".
- Recipients. A rule with no recipients sends to the account e-mail address, not to nobody.
- Throttling. A rule with a throttle window suppresses a repeat to the same recipients inside that window. Set it to zero while testing.
- Only analyzed calls alert. A call that was skipped or failed analysis has no verdict to alert on — start there instead.
- The obvious one. Check the spam folder and any inbound filter for the sending address before blaming the platform.
Every new account starts with one enabled rule that e-mails the account address on calls scoring 90 or above with a block recommendation. If you never edited it, that is the only rule you have.
Webhooks are not delivered#
Send one test delivery. It uses the same envelope, signature and headers as a real event and reports exactly what happened.
POST /customer/webhook/test
curl -s -X POST $API/api/customer/webhook/test -H "X-API-Key: $OVS_KEY"
# {"delivered":true,"status_code":200,"error":null,"latency_ms":183}
# {"delivered":false,"status_code":null,"error":"webhook_not_configured","latency_ms":0}
| Result | Means | Fix |
|---|---|---|
| webhook_not_configured | No URL, or no secret. Both are required. | PUT /customer/webhook with url and secret. |
| blocked_destination: … | The URL failed the outbound safety check: it is not https, or its hostname resolves to a private, loopback or link-local address. | Publish the receiver on a public https endpoint. This failure is permanent and is never retried. |
| a 4xx status | Your endpoint rejected the request — wrong path, authentication in front of it, or a body it would not parse. | Fix the receiver. A 4xx is treated as permanent; only 408, 425 and 429 are retried. |
| a 5xx status, or a network error | Transient. A delivery is attempted up to five times with a widening backoff, then abandoned. | Stabilize the receiver; the request times out after ten seconds. |
| delivered, but nothing appears | Something in front of your endpoint absorbed it, or you followed a redirect that was never taken. | Redirects are not followed. Point the URL at the final address. |
If deliveries arrive but your receiver rejects them, check the signature on the raw body, before any JSON parsing or re-serialization: X-OVS-Signature is sha256= followed by the hex HMAC-SHA256 of the body under your webhook secret. X-OVS-Event names the event and X-OVS-Delivery is a delivery id you can use to make your handler idempotent — a retry repeats it.
An API request is rejected as unauthorized#
| Response | Means |
|---|---|
| 401 {"detail":"Not authenticated"} | No credential reached the API. Usually the header name: it is X-API-Key for an API key, Authorization: Bearer for an access token. |
| 401 {"detail":"Invalid API key"} | Unknown or revoked key — or the portal user who minted it has been deactivated, which kills their keys with them. |
| 401 {"detail":"Token expired"} | The access token's lifetime ran out. They are short-lived by design — minutes, not days. For anything unattended, use an API key instead. |
| 401 {"detail":"Invalid token"} | The token is not a valid access token, or it was issued before a password change or a sign-out-everywhere. Sign in again. |
| 401 {"detail":"Invalid authorization header"} | The Authorization header is malformed. It must read Bearer <token>, with the scheme and one space. |
| 403 {"detail":"password_change_required"} | The account still has its temporary password. Nothing outside /auth/* answers until it is changed. |
| 403 {"detail":"user_deactivated"} | The portal user behind this credential has been deactivated by the account owner. |
| 403 {"detail":"owner_required"} | The action is reserved to the account owner — minting and revoking API keys, managing passkeys and managing users all are. |
| 403 Account suspended | The account is suspended. INVITEs are refused with the same reason. |
| 403 {"detail":"csrf_header_required"} | A session endpoint was called without X-Requested-With: XMLHttpRequest. Those endpoints are for the portal; scripts should use an API key. |
| 429 {"detail":"too_many_attempts","retry_after":N} | Too many failed sign-ins from this address or for this e-mail. Wait the seconds given in Retry-After; a successful sign-in clears the counter for the address. |
| 404 Call not found | Not necessarily gone — every lookup is scoped to your account, and another account's object is indistinguishable from one that does not exist. |
Quick answers#
- Why is my call rejected with 403 IP not whitelisted?
- The public source address of the INVITE is not inside any enabled entry on your IP list. It is the address after NAT, which is often your SBC's egress address rather than the PBX's own, and an OPTIONS ping succeeding does not prove it: OPTIONS is answered for any address, only an INVITE is checked against the list.
- How do I tell an IP rejection from an automation block? Both answer 403.
- Look in Calls. A caller blocked by one of your automation rules leaves a call record with status failed and end reason blocked_by_rule; a call refused because the source IP is not whitelisted leaves no record at all. The reason phrase on the SIP response also differs: Blocked by rule versus IP not whitelisted.
- Why does a call have a recording but no analysis?
- Check analysis_status and processing_error on the call. Common causes are a call shorter than the minimum analysis length (skipped), a codec other than G.711 (unsupported_codec), and audio that only ever flowed in one direction (no_rtp_streams).
- Why are my webhooks not arriving?
- Send a test delivery from the portal or POST /customer/webhook/test: it reports delivered, the upstream status code and the error. A non-https URL, or a hostname that resolves to a private or loopback address, is refused permanently and is never retried.
What to send when you escalate#
Include enough to identify the call without a conversation:
- The call id from the portal, or the SIP Call-ID if the call never produced a record.
- The time and time zone, and the source address the INVITE came from.
- The exact response your switch logged, reason phrase included.
- What you already checked from this page.
Do not attach call content
Never send recordings, transcripts or credentials in a support message. Reference the call id — whoever helps you can open the call from it.