Back to Blog

India Hung Up on My AI Agent (So I Built It a House in Mumbai)

engineering
engineeringeducationbackend-developmentvoip
3,912 words16 min read
India Hung Up on My AI Agent (So I Built It a House in Mumbai)

TL;DR

Try making an AI voice call to an Indian phone number with Vapi and the call dies instantly with 403 Domestic Anchored Terms Not Met. Indian telecom rules require both the SIP signalling and the RTP audio for domestic calls to originate from infrastructure physically inside India — and Vapi runs in the US. The fix: deploy a media anchor in Mumbai — Kamailio as the SIP proxy and RTPEngine as the media relay on an EC2 box in ap-south-1 — so Plivo sees an Indian IP in both the signalling and the SDP, and lets the call through. Along the way we'll hit three genuinely evil SIP bugs: calls dropping at exactly ~17 seconds (ACK handling), at exactly ~900 seconds (session timers), and Plivo rejecting the INVITE unless we rewrite where it's addressed to.

If you've never touched VoIP before, don't worry — we build up SIP, RTP, and SDP from scratch. By the end you'll know exactly why every line of the config exists.

Vapi (US)
   ↓
Kamailio + RTPEngine (EC2 Mumbai)
   ↓
Plivo SIP Trunk
   ↓
Indian Phone 📞

The failure

Here's the setup. Vapi gives you an AI voice agent — speech-to-text, an LLM brain, text-to-speech — that can make actual phone calls. Plivo gives you Indian phone numbers and a SIP trunk into the Indian phone network. Wire them together, point the agent at an Indian mobile number, hit call, and…

403 Domestic Anchored Terms Not Met

Not 403 Forbidden. Not 503 Service Unavailable. A very specific, very bureaucratic-sounding rejection. Plivo's own hangup code documentation files it under fatal signalling errors, and their integration guides spell out the cause: for calls to/from India, your deployment must be in India, or calls will fail with this exact error.

Your AI can talk. India just won't pick up. Let's understand why, and then fix it properly.

A crash course in internet phone calls

Every VoIP call — WhatsApp, Zoom, your bank's IVR, this AI agent — is built on three protocols. You need all three to debug anything in this space, so let's take them one at a time, each with the real explanation and the human one.

Protocol Job One-liner

SIP

Signalling "Can we talk?"

RTP

Media The actual talking

SDP

Negotiation "Here's my address, send audio here"

SIP — Session Initiation Protocol

The real explanation. SIP (RFC 3261) is a text-based signalling protocol for starting, managing, and ending calls. It looks and behaves a lot like HTTP — requests with methods, responses with status codes:

INVITE        →  "I want to start a call"
180 Ringing   →  "the other phone is ringing"
200 OK        →  "they picked up"
ACK           →  "great, I acknowledge — let's talk"
BYE           →  "hang up"

A basic call setup:

Caller → INVITE → Callee
Callee → 180 Ringing → Caller
Callee → 200 OK → Caller
Caller → ACK → Callee
        ... audio ...
Caller → BYE → Callee

Once that handshake completes, the audio stream begins — but notice that no audio flows over SIP itself. SIP only negotiates.

The simple explanation. Imagine calling a friend. First you ask "can we talk?" They reply "hang on, my phone's ringing", then "okay, picked up." SIP is the entire conversation before the actual conversation.

RTP — Real-time Transport Protocol

The real explanation. RTP (RFC 3550) carries the actual audio packets during the call. Once SIP establishes the session, RTP streams encoded audio frames between the two ends, dozens of times per second.

Key characteristics:

  • runs over UDP (losing a packet is better than waiting for a retransmit — you'd rather hear a 20ms glitch than a 2-second freeze)
  • uses high, dynamically negotiated port ranges — typically something like 30000–40000
  • each packet carries a sequence number and timestamp so the receiver can reorder and pace playback
Microphone → encode (codec) → RTP packet → UDP → internet → decode → speaker

The simple explanation. If SIP is asking "can we talk?", RTP is the talking. Your voice gets chopped into tiny pieces and flung across the internet 50 times a second.

SDP — Session Description Protocol

The real explanation. SDP (RFC 8866) is a blob of metadata that rides inside SIP messages (in the body of the INVITE and the 200 OK) and describes the media session: which codec, which IP, which port.

c=IN IP4 YOUR_EC2_PUBLIC_IP
m=audio 32000 RTP/AVP 0

That c= (connection) line is the single most important line in this entire blog post. It tells the other side:

"Send your RTP audio to this IP address."

The simple explanation. SDP is handing someone your address before the conversation starts:

Send my voice to:
House: YOUR_EC2_PUBLIC_IP
Room:  32000

Remember this: SIP decides whether a call happens. SDP decides where the audio goes. RTP is the audio. Every bug in this post is one of these three misbehaving.

Why India rejects the call

India's telecom regime — the Department of Telecommunications (DoT) sets licensing rules, TRAI regulates — has long-standing rules against toll bypass: routing what is effectively a domestic Indian call through foreign infrastructure, dodging interconnect charges and lawful-interception requirements. The practical consequence, which licensed carriers like Plivo must enforce on their Zentrunk SIP trunks:

For a domestic Indian call, both the SIP signalling and the RTP media must originate from infrastructure physically located in India.

Now look at what happens when Vapi calls Plivo directly:

Vapi (US, ~52.x.x.x) ──INVITE──▶ Plivo India

Plivo opens the SDP inside that INVITE and sees:

c=IN IP4 52.x.x.x        ← a US IP address

US signalling. US media. Textbook toll bypass from the carrier's point of view. So Plivo does exactly what its license obligations require:

403 Domestic Anchored Terms Not Met

The call never even rings.

The fix: a media anchor in Mumbai

We can't move Vapi to India. But we can put a box in India that the call passes through — so that by the time Plivo sees the traffic, both the signalling and the media genuinely originate from Indian soil. In VoIP jargon this is a media anchor. In "explain it to me like I'm five" terms: it's a VPN server for phone calls.

Two open-source pieces make it work:

  • Kamailio — a battle-hardened SIP proxy (it routes signalling for a good chunk of the world's carriers). It receives Vapi's INVITE and forwards it to Plivo.
  • RTPEngine — a kernel-accelerated RTP relay from Sipwise. It intercepts the audio and, crucially, rewrites the SDP so the c= line advertises its Indian IP instead of Vapi's US one.
Vapi (US)
     │
     │ SIP INVITE (SDP says: send audio to US)
     ▼
EC2 Mumbai (ap-south-1)
├── Kamailio   — proxies the SIP signalling
└── RTPEngine  — relays RTP + rewrites SDP to say: send audio to Mumbai
     │
     ▼
Plivo Zentrunk ──▶ Indian Phone

Now Plivo sees c=IN IP4 3.6.x.x — an Indian IP — and Indian signalling. Anchoring terms met. Call goes through.

Here's the full ladder diagram of a working call:

Vapi          Kamailio (EC2)       Plivo           Your Phone
  │                │                  │                 │
  │──── INVITE ───▶│                  │                 │
  │                │──── INVITE ─────▶│                 │
  │                │◀─── 100 Trying ──│                 │
  │◀── 100 Trying ─│                  │────── Ring ────▶│
  │                │◀─── 180 Ringing ─│                 │
  │◀── 180 Ringing─│                  │                 │
  │                │                  │   (you pick up) │
  │                │◀─── 200 OK ──────│                 │
  │◀─── 200 OK ────│                  │                 │
  │──── ACK ───────────────────────────────────────────▶│
  │                │                  │                 │
  │         [audio flows via RTP through RTPEngine]     │
  │                │                  │                 │
  │──── BYE ──────▶│                  │                 │
  │                │──── BYE ────────▶│                 │
  │                │                  │────── End ─────▶│

The cast, briefly

Why Vapi? It bundles the entire AI-voice stack — speech recognition (Deepgram etc.), the LLM (OpenAI etc.), speech synthesis (ElevenLabs etc.) — behind one API, with first-class BYO SIP trunk support. You get a phone-capable AI agent in an afternoon.

Why Plivo? It's the licensed carrier side: Indian phone numbers (DIDs), PSTN connectivity, and Zentrunk SIP trunks that bridge your internet calls onto the actual Indian phone network.

What's a SIP trunk? A virtual phone line — instead of a copper wire from the phone company, you get a SIP endpoint that accepts your calls and terminates them onto the real telephone network. Simple version: a pipe that carries phone calls.

Building it

EC2 setup

Region is non-negotiable:

ap-south-1 (Mumbai)

That's the entire point — the box must be physically in India. A t3.medium is comfortable; audio relaying is cheap, but you don't want CPU credits throttling mid-call.

Security group inbound rules:

5060  TCP/UDP   SIP signalling
30000-40000 UDP RTP media
22    TCP       SSH (lock to your IP)

Gotcha: forget the UDP port range and you get the most confusing failure mode in VoIP — the call connects perfectly and there is dead silence. SIP (5060) negotiated fine; RTP (30000+) is being silently dropped by the firewall.

RTPEngine configuration

/etc/rtpengine/rtpengine.conf:

[rtpengine]
 
interface = YOUR_EC2_PRIVATE_IP!YOUR_EC2_PUBLIC_IP
listen-ng = 127.0.0.1:2223
port-range = 30000-40000
 
log-level = 6
log-stderr = false
foreground = false
 
timeout = 300
silent-timeout = 300
final-timeout = 180

Note on the two 180s: Plivo's 200 OK advertises Session-Expires: 1800;refresher=uas, but we strip session timers because Vapi doesn't implement them. We enforce our own hard cap — 180 seconds — via Kamailio's dialog module later. RTPEngine's final-timeout should be long enough to outlive any call you want to keep.

The line that makes everything work:

interface = PRIVATE_IP!PUBLIC_IP

EC2 instances live behind 1:1 NAT — the machine only knows about its private address; the public IP is mapped outside the box. The ! syntax tells RTPEngine: bind to the private IP, but advertise the public IP in any SDP you rewrite. So every SDP that passes through comes out saying:

c=IN IP4 YOUR_EC2_PUBLIC_IP      ← Mumbai. Anchored. Happy carrier.

listen-ng is RTPEngine's control socket — Kamailio sends it commands ("new call, rewrite this SDP, open these ports") over this local UDP socket using the NG protocol.

Kamailio configuration

The full /etc/kamailio/kamailio.cfg — read the comments, then we'll dissect the three pieces of logic that took real debugging to get right:

#!KAMAILIO

##############################################
# Kamailio + RTPEngine - Vapi → Plivo Bridge
# EC2 Public IP  : YOUR_EC2_PUBLIC_IP
# EC2 Private IP : YOUR_EC2_PRIVATE_IP
# Plivo Domain   : YOUR_PLIVO_DOMAIN.zt.plivo.com
##############################################

# ---- Global Parameters ----
debug=2
log_stderror=no
log_facility=LOG_LOCAL0
fork=yes
children=4

listen=udp:YOUR_EC2_PRIVATE_IP:5060 advertise YOUR_EC2_PUBLIC_IP:5060
listen=tcp:YOUR_EC2_PRIVATE_IP:5060 advertise YOUR_EC2_PUBLIC_IP:5060

tcp_accept_no_cl=yes
auto_aliases=no

# ---- Modules ----
loadmodule "tm.so"
loadmodule "tmx.so"
loadmodule "sl.so"
loadmodule "rr.so"
loadmodule "pv.so"
loadmodule "maxfwd.so"
loadmodule "textops.so"
loadmodule "siputils.so"
loadmodule "xlog.so"
loadmodule "sanity.so"
loadmodule "nathelper.so"
loadmodule "kex.so"
loadmodule "rtpengine.so"
loadmodule "dialog.so"

# ---- Module Parameters ----
modparam("rr", "add_username", 1)
modparam("nathelper", "natping_interval", 0)
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
modparam("dialog", "enable_stats", 0)
modparam("dialog", "default_timeout", 180)

# ---- Routing Logic ----
request_route {

    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483", "Too Many Hops");
        exit;
    }

    if (!sanity_check()) { exit; }

    # ACK for 200 OK must be handled BEFORE t_newtran()
    # It is outside the transaction — route directly via loose_route
    if (is_method("ACK")) {
        if (has_totag()) {
            if (loose_route()) {
                route(RELAY);
                exit;
            }
        }
        if (t_check_trans()) { t_relay(); }
        exit;
    }

    if (is_method("CANCEL")) {
        if (t_check_trans()) { t_relay(); }
        exit;
    }

    # Absorb retransmissions, create new transaction for new requests
    if (t_precheck_trans()) { t_check_trans(); exit; }
    if (!t_newtran()) { sl_reply_error(); exit; }

    if (is_method("INVITE")) {
        record_route();
        dlg_manage();
    }

    # In-dialog routing (BYE, re-INVITE, UPDATE)
    if (has_totag()) {
        if (loose_route()) {
            if (is_method("INVITE") || is_method("UPDATE")) {
                rtpengine_manage("trust-address replace-origin replace-session-connection");
                remove_hf("Session-Expires");
                remove_hf("Min-SE");
                remove_hf("Supported");
            }
            route(RELAY);
            exit;
        }
        sl_send_reply("404", "Not Found");
        exit;
    }

    if (is_method("INVITE")) {

        xlog("L_INFO", "Incoming INVITE from $si — To: $ru\n");

        force_rport();
        fix_nated_contact();

        # Strip session timer headers
        # Plivo adds Session-Expires: 1800;refresher=uas — if left in,
        # the session must be refreshed every 900s. Vapi doesn't do
        # session timers → call dies at ~900s.
        remove_hf("Session-Expires");
        remove_hf("Min-SE");
        remove_hf("Supported");

        # Tell RTPEngine to rewrite SDP c= and o= to EC2 India IP
        rtpengine_manage("trust-address replace-origin replace-session-connection");

        # Rewrite R-URI: address INVITE to Plivo domain, not EC2 IP
        # Without this, Plivo receives INVITE@YOUR_EC2_PUBLIC_IP and rejects it
        $ru = "sip:" + $rU + "@YOUR_PLIVO_DOMAIN.zt.plivo.com;transport=udp";
        $du = "sip:YOUR_PLIVO_DOMAIN.zt.plivo.com;transport=udp";

        # Caller ID — must match your Plivo DID
        $fn = "+91XXXXXXXXXX";
        $fU = "+91XXXXXXXXXX";

        remove_hf("Route");

        # Register onreply handler to process Plivo responses
        t_on_reply("REPLY_HANDLER");

        if (!t_relay()) { sl_reply_error(); }
        exit;
    }

    sl_send_reply("405", "Method Not Allowed");
}

route[RELAY] {
    if (!t_relay()) { sl_reply_error(); }
    exit;
}

# Fires on every reply from Plivo before forwarding to Vapi
onreply_route[REPLY_HANDLER] {
    xlog("L_INFO", "Reply $rs from Plivo — Call-ID: $ci\n");

    if (t_check_status("1[0-9][0-9]|2[0-9][0-9]")) {
        rtpengine_manage("trust-address replace-origin replace-session-connection");
    }

    # Strip session timers from Plivo replies too
    remove_hf("Session-Expires");
    remove_hf("Min-SE");
    remove_hf("Require");
}

reply_route {
    if (!sanity_check("17604", "6")) { drop; }
}

# dialog module fires this when default_timeout (180s) expires
# Kamailio sends BYE to both legs — clean 3-minute call limit
event_route[dialog:end] {
    xlog("L_INFO", "Dialog ended — Call-ID: $ci\n");
}

Note the same NAT trick as RTPEngine, this time for signalling:

listen=udp:YOUR_EC2_PRIVATE_IP:5060 advertise YOUR_EC2_PUBLIC_IP:5060

Bind private, advertise public — so the Via and Record-Route headers Kamailio stamps into messages carry the Indian public IP.

The three bugs that made this hard

The config above looks obvious in hindsight. Each of these three blocks exists because a call failed in a weird, timed, reproducible way. This is the part of the post I wish someone had written before me.

Bug 1 — Calls drop at exactly ~17 seconds (ACK handling)

Call connects. Audio flows. Both sides talking happily. Then at ~17 seconds, dead — Plivo sends a BYE out of nowhere.

Here's the deep SIP lore behind it. In RFC 3261, the ACK that follows a 2xx response to an INVITE is not part of the INVITE transaction. ACKs for failure responses (3xx–6xx) are hop-by-hop and belong to the transaction; the ACK for a 200 OK is end-to-end and stands alone, precisely so it can be delivered reliably all the way to the far end (there's a whole clarification RFC about this corner: RFC 6026).

My original config let ACKs fall through to t_newtran(), Kamailio's "create a new transaction" call. Kamailio dutifully created a transaction for the ACK, absorbed it… and never forwarded it to Plivo.

From Plivo's perspective: it sent 200 OK ("call answered!") and never got the ACK. So it does what the RFC says a UAS should do — retransmit the 200 OK on an exponential timer and, if no ACK ever arrives, give up and tear the session down with a BYE:

200 OK  →  (no ACK)  →  retransmit at 0.5s, 1s, 2s, 4s, 8s…  →  BYE

Add up those retransmits and you land at roughly 15–17 seconds. That's where the eerily consistent ~17-second call drop comes from. (The RFC allows a UAS to keep trying for up to 64×T1 = 32 seconds; Plivo pulls the plug earlier.)

The fix: handle ACK at the very top of request_route — before any transaction machinery. In-dialog ACKs get routed directly via loose_route(), and we exit before t_newtran() can ever see them.

if (is_method("ACK")) {
    if (has_totag()) {
        if (loose_route()) { route(RELAY); exit; }
    }
    if (t_check_trans()) { t_relay(); }
    exit;
}

The one thing to remember: if your SIP calls die at a suspiciously consistent ~15–32 seconds, the ACK is not making it end-to-end. Always.

Bug 2 — Calls drop at exactly ~900 seconds (session timers)

Fixed the ACK, celebrated, ran a long test call. Dead at 15 minutes, on the dot.

This one is RFC 4028 — SIP Session Timers. Plivo's 200 OK always includes:

Session-Expires: 1800;refresher=uas

Session timers are SIP's keep-alive mechanism: the session must be refreshed (via a re-INVITE or UPDATE) before the interval expires, and per the RFC the refresher fires halfway through the interval — so 1800 seconds means a refresh is due at 900 seconds. If the refresh never happens, the session is considered dead and gets torn down with a BYE.

Vapi simply doesn't implement session timers. It will never participate in that refresh dance. So at 1800/2 = ~900 seconds, the call dies.

The fix: strip the session-timer negotiation entirely, in both directions — Session-Expires, Min-SE, and Supported from the outgoing INVITE, and the same (plus Require) from Plivo's replies. With the headers gone, neither side arms the timer, and calls run as long as they like — or rather, as long as we like, because we replace the mechanism with our own hard limit via Kamailio's dialog module (default_timeout, 180s here).

Bug 3 — Plivo returns 404/408 for every INVITE (R-URI rewrite)

Vapi addresses its INVITE to your gateway IP, because that's all it knows:

INVITE sip:919876543210@YOUR_EC2_PUBLIC_IP

If Kamailio forwards that as-is, Plivo receives an INVITE whose Request-URI points at… your EC2 box. Plivo has no idea what YOUR_EC2_PUBLIC_IP is supposed to mean as a destination domain, and rejects it.

The fix: rewrite the Request-URI ($ru) so the call is addressed to Plivo's Zentrunk domain, and set the destination URI ($du) so it's physically routed there too:

$ru = "sip:" + $rU + "@YOUR_PLIVO_DOMAIN.zt.plivo.com;transport=udp";
$du = "sip:YOUR_PLIVO_DOMAIN.zt.plivo.com;transport=udp";

($rU is just the user part — the phone number — preserved from the original.)

Plugging it into Vapi

Vapi's BYO SIP trunk flow is two API calls. First, register the gateway as a credential:

curl -X POST https://api.vapi.ai/credential \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_VAPI_PRIVATE_API_KEY" \
  -d '{
    "provider": "byo-sip-trunk",
    "name": "Kamailio India Trunk",
    "gateways": [
      {
        "ip": "YOUR_EC2_PUBLIC_IP",
        "port": 5060,
        "inboundEnabled": false
      }
    ]
  }'

Then attach your Plivo DID to it:

curl -X POST https://api.vapi.ai/phone-number \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_VAPI_PRIVATE_API_KEY" \
  -d '{
    "provider": "byo-phone-number",
    "name": "Plivo India Number",
    "number": "+91XXXXXXXXXX",
    "numberE164CheckEnabled": false,
    "credentialId": "CREDENTIAL_ID"
  }'

Don't forget the other side: add your EC2 IP (YOUR_EC2_PUBLIC_IP/32) to the IP Access Control List on your Plivo Zentrunk. Plivo drops INVITEs from unknown IPs silently — no logs, no errors, just void.

Debugging toolkit

VoIP debugging is 90% packet capture. Everything you need:

# See all SIP messages in real time
sudo tcpdump -i any port 5060 -A 2>/dev/null | \
  grep -E "(INVITE|BYE|200|ACK|From:|To:|Call-ID:)"
 
# THE check: is RTPEngine rewriting SDP? Must show your EC2 India IP
sudo tcpdump -i any port 5060 -A 2>/dev/null | grep "c=IN IP4"
 
# Watch for session timer headers sneaking through
sudo tcpdump -i any port 5060 -A 2>/dev/null | grep "Session-Expires"
 
# Kamailio logs
sudo tail -f /var/log/syslog | grep kamailio

Confirming TRAI compliance: after a test call, open the call log in the Plivo console. The SIP PCAP field should say Ap-South-1 (India), not Us-East-1. That single field is the ground truth for whether your anchoring actually worked.

Common errors cheat-sheet

Symptom Cause Fix

403 Domestic Anchored Terms Not Met

RTPEngine not rewriting SDP Check interface in rtpengine.conf, verify RTPEngine is running

408 Request Timeout

INVITE not reaching Plivo Check $ru rewrite in kamailio.cfg, verify Plivo domain

Call drops at ~17 seconds

ACK not forwarded to Plivo Handle ACK before t_newtran() in request_route

Call drops at ~900 seconds

RFC 4028 session timers unanswered Strip Session-Expires / Min-SE / Supported both directions

No Plivo logs at all

EC2 IP not in Plivo ACL Add EC2_IP/32 to the Zentrunk IP Access Control List

SIP PCAP shows Us-East-1

RTPEngine down or misconfigured sudo systemctl status rtpengine, check interface advertise config

Call connects, dead silence

RTP ports blocked Open 30000-40000 UDP in the EC2 security group

Production considerations

  • Elastic IP — non-negotiable. Without one, the public IP changes on every stop/start, simultaneously breaking the Plivo ACL, the Kamailio config, the RTPEngine advertise address, and the Vapi credential. Four outages for the price of one reboot.
  • Call duration capmodparam("dialog", "default_timeout", 180) gives you a clean server-side limit; Kamailio BYEs both legs when it fires. Change 180 to whatever your use case (and phone bill) can bear.
  • Cost — a t3.medium in Mumbai is $30/month on-demand. Low volume? A t3.small ($15/month) handles it fine.
  • High availability — run two instances in different Mumbai AZs behind an AWS Network Load Balancer. SIP over UDP behind an NLB has its own sharp edges (that's a future post).

The final call flow

Everything assembled:

1.  Vapi           →  INVITE sip:91XXXXXXXXXX@YOUR_EC2_PUBLIC_IP          →  Kamailio
2.  Kamailio       →  rtpengine_manage() rewrites SDP c= to YOUR_EC2_PUBLIC_IP
3.  Kamailio       →  INVITE sip:[email protected]  →  Plivo
4.  Plivo          →  100 Trying                                   →  Kamailio
5.  Plivo          →  183 Session Progress                         →  Kamailio  →  Vapi
6.  Phone rings
7.  User answers
8.  Plivo          →  200 OK                                       →  Kamailio  →  Vapi
9.  Vapi           →  ACK                                          →  Kamailio
10. Kamailio       →  ACK (via loose_route)                        →  Plivo
11. RTP audio flows: Phone ↔ RTPEngine (EC2 Mumbai) ↔ Vapi
12. At 180s: Kamailio dialog timeout → BYE to both legs

Key takeaways

  1. Indian telecom rules require domestic anchoring — for domestic calls, both SIP signalling and RTP media must come from infrastructure in India. Carriers enforce this at the trunk level, hence the 403.
  2. A media anchor is the fix, not a hack — Kamailio + RTPEngine in ap-south-1 makes the traffic genuinely originate in India. The SIP PCAP: Ap-South-1 field in Plivo's logs is your proof.
  3. SDP rewriting is the core trick — one config line, interface = PRIVATE_IP!PUBLIC_IP, changes what c=IN IP4 says, and that line is what the carrier judges you by.
  4. SIP's weirdest rule will bite you — the ACK for a 200 OK lives outside the INVITE transaction. Mishandle it and every call dies at ~17 seconds.
  5. Timed failures are protocol timers — ~17s means ACK, ~900s means RFC 4028 session timers. When a call dies at a consistent timestamp, stop debugging your code and start reading timer specs.

That's all in this one, folks. If your AI agent is now cold-calling you at dinner time, that's between you and your Kamailio config.

References