All posts

Voice AI Production in India | Compliance, Reliability, Scale | Exotel

Saurabh Sharma
Saurabh Sharma
·

 

Shipping the first call is easy. Surviving the first outbound campaign — a Friday deploy, a cold start, a carrier blip and a stuck model — is the real test. So is the compliance review that arrives the week before launch.


Part 1 — Compliance

Who holds the licence

Enterprise reviews in banking, insurance, healthcare and government usually open with one question: who holds the telecom licence for this call? If the answer runs through a chain of partners, the review gets long.

We operate as a licensed telecom infrastructure provider with UL-VNO across 11 telecom circles, and we operate under TRAI and DoT obligations. AgentStream runs on that infrastructure.

Area What you get
Telecom licence UL-VNO across 11 circles, held by Exotel
Data residency India-resident media and storage for Indian workloads
Recording for audit Secure per-account options, mono or stereo, delay-start
Structured outcomes Call and stream callbacks you can feed into CDR, CRM and compliance systems

Two honest generalisations about the market, without naming anyone: global CPaaS platforms often do not hold an Indian UL-VNO themselves, and Indian traffic may run over partner arrangements; AI-native vendors may price aggressively but publish little about licensing, residency or audit posture. A prototype can live with that ambiguity. A bank running collections cannot.

Which number you call from decides what you may do

Most teams pick a number for cost or availability, then discover it constrains the campaign. In India the calling-line series is a regulatory category, not a label.

The regulation requires registered telemarketers to originate promotional, transactional and service voice calls from the 140-level series or any other series directed by the Authority or DoT (TCCCPR 2018, Schedule-I, Section 2). The 1600 and 1601 allocations are that second clause in action.

Number type What it is for What constrains it
Mobile DID Inbound, and outbound where a mobile caller ID is appropriate — support callbacks, agent dial-out, two-way conversations with existing customers. Usually the best answer rates, because recipients recognise the format Not the number for promotional campaigns. TRAI’s May 2024 direction names the practice explicitly: entities using “normal 10-digit mobile/landline numbers for making promotional calls” are operating outside the registered framework, and closing that gap is the stated purpose of Voice DLT. Also the highest spam-reputation exposure — see below
Landline DID Geographic presence, inbound support lines, contact-centre pilots Same exclusion for promotional use. Some recipients screen unknown landline numbers, and the region prefix implies a location you may not want to imply
140 series Promotional and telemarketing voice calls by registered telemarketers Registration on DLT, and delivery is governed by each recipient’s registered time-band and content-category preferences rather than a window you choose
1600 series Service and transactional calls by BFSI and Government entities Must not be used for promotional voice calls
1601 series Service and transactional calls by entities in sectors other than BFSI and Government Must not be used for promotional voice calls. Introduced in phases — Phase-I covers utilities (electricity distribution, water, city gas, LPG) and courier and logistics (courier, express logistics, parcel delivery, freight), with operators directed to complete migration of eligible Phase-I entities within 90 days of the 10 August 2026 order

On the naming, because it causes real confusion in design docs: TRAI’s May 2024 Voice DLT direction refers to a 160-series for new service and transactional calls. The August 2026 order then allocates within that family — 1600 for BFSI and Government, 1601 for other sectors. So “160-series” is the family and 1600/1601 are the specific allocations. If a ticket says 160, establish which allocation is meant before building against it.

The practical consequence: promotional and transactional traffic are different products on different numbers, and you cannot mix them on one caller ID. A collections reminder and a marketing offer are not the same call type even when the bot is identical. Decide the series before you write the dialer, because it determines your registration path, your delivery rules, and what your bot is allowed to say.

One design decision on our side worth knowing about: we derive the call type from the caller-ID series rather than accepting a “promotional” or “transactional” flag in the API request. We considered letting callers declare it and rejected it — a self-declared flag is trivially misusable and the exposure lands on everyone. So the number you dial from is the source of truth, and calling-window behaviour follows from it rather than from a parameter you set.

There is no single “calling window” — and this is where dialers get built wrong

The common belief is that promotional calling in India is allowed between 9 AM and 9 PM, so a global clock check in the dialer is enough. That is not what the regulation says, and a dialer built on it will still get traffic blocked.

TCCCPR 2018 works on per-recipient time bands (Regulation 6 and Schedule-II). The day is divided into bands, and a band is off by default for every subscriber — including subscribers who never registered any preference — until that subscriber switches it on. Schedule-II, Note-1 is explicit: time bands “shall be default OFF for all customers irrespective of the status of registration of customer… unless customer has registered its preference(s) and switched ON.”

The night bands — 00:00–06:00, 06:00–08:00 and 21:00–24:00 — are the ones blocked by default, with the selectable bands running in two-hour slots across 08:00 to 21:00. That is where the 9-to-9 folklore comes from, and it is roughly the right shape, but the mechanism is not a shared window. It is a per-number permission.

Recipients also register content preferences, and the regulation gives them two blocking modes plus eight categories:

  • Fully Blocked — all commercial communication except transactional
  • Block Promo — promotional blocked, service and transactional allowed
  • Or opt out of any of: banking, insurance, financial products and credit cards · real estate · education · health · consumer goods and automobiles · communication, broadcasting, entertainment and IT · tourism and leisure · food and beverages

What this means for your build: you cannot determine deliverability from your own clock and contact list. Two recipients dialled in the same second may have different permitted bands and different category blocks. Eligibility is evaluated per recipient against their registered preferences and consents — so treat a rejected or undelivered call as a normal, expected outcome for a compliant campaign rather than an error, and record the disposition rather than retrying blindly into the same block.

What the network actually does about it

Enforcement sits with the access providers, not with any one platform, and TCCCPR 2018 is prescriptive about it.

Scrubbing is mandatory, per call. Regulation 9: “Every Access Provider shall ensure that no commercial communication is made to any Recipient, except as per the preference(s) or digitally registered consent(s).” In practice that means checking the recipient’s number and the content category against the preference register, checking any claimed consent against the consent register, and checking the time band and day type before delivery.

Detection is active, not complaint-driven. Regulation 12 and Schedule-IV require operators to run systems that identify senders by signature, deploy honeypots, detect bulk senders, and interface with other operators’ detection systems. Volume patterns are being looked for whether or not anyone complains.

Action against senders escalates (Regulation 25). For an unregistered sender: a warning on first violation; a usage cap for six months on second; and on a third, “all telecom resources of the sender shall be disconnected for a period up to two years”, with blacklisting.

And the operator has its own money at stake (Regulations 27 and 28), which is the part that explains the behaviour you actually experience. Access providers face financial disincentives for failing to control unsolicited traffic — graded per UCC count and capped per month per licence service area, plus a per-instance penalty for failing to restrict unregistered senders in time.

That last point is worth sitting with. Because the operator is penalised for carrying your non-compliant traffic, the network’s incentive is to stop it early and quietly rather than to warn you and wait. This is why a campaign that crosses a line tends to fail as silent drops and collapsing answer rates rather than as a clear error telling you what you did wrong.

The part that actually breaks your campaign: number reputation

Regulation is the part teams plan for. Reputation is the part that takes the campaign down, and it operates independently of whether you are compliant.

Carrier and DND enforcement systems watch call volume per number. A single number carrying a disproportionate share of your outbound traffic gets flagged as a spam or robocall source, and once that happens:

  • Downstream carriers blacklist it. Calls drop silently, or come back as SIP 403 or 404
  • Recipients see a “Spam Likely” style label, or the call never connects
  • Your answer rate collapses, and nothing in your application logs explains why
  • Neither you nor we get told. It surfaces as a support escalation days later

There is a second, separate failure with the same root cause. Carrier trunks impose a per-number concurrent session limit, set in the carrier’s own terms and typically in the single digits to low tens. Funnel a burst through one number and you hit it immediately, and the carrier returns SIP 503. Those get misread as platform instability when the cause is number configuration.

Concentration is also a compliance exposure in its own right: regulated programmes may be asked to show that no single number carried a disproportionate share of contacts.

What to do about it

Practice Why
Use a pool, not a number One number per campaign concentrates every spam signal on one line. Split by line of business or region so a flag on one number does not take down the programme
Rotate the number that appears on the network, not the dashboard label This is the mistake we see most. The number shown in your campaign configuration and the number that actually presents on the network are not necessarily the same. Swapping the label while the same underlying line carries the call leaves the reputation exactly where it was, and the problem persists
Spread volume over time, not just across numbers Sending the next call through the least-recently-used number keeps any one line from looking like a burst source. Concentration in time is a spam signal too
Enforce a rest period before reuse A number pulled out for reputation needs to sit idle before returning, measured in days rather than hours. Without an enforced rest period a large pool behaves like a small one, because the same tired numbers keep recirculating
Monitor answer rate per number Your leading indicator. One number’s answer rate falling while the rest of the pool holds steady means that number is flagged — and it tells you well before a support ticket does
Track cumulative dials per number Flagging tends to follow cumulative volume on a line. Instrument it and learn your own threshold; it varies by carrier, region and recipient behaviour, so treat any number you are given as an order of magnitude rather than a limit to trust

None of this is Voice AI specific — it applies to any outbound programme at volume. It hits Voice AI programmes harder, because the point of putting a bot on the phone is making more calls than a human team could, which means reaching the reputation ceiling faster than a manual operation would.

What we are not claiming

An honest post needs this section.

NDNC and DND. Outbound campaigns in India carry Do-Not-Call obligations. How scrubbing is applied depends on your account configuration and your campaign type, so confirm the specifics for your setup with us rather than assuming it is handled automatically on every path. This is regulatory exposure, not a product inconvenience, and it deserves a real answer rather than a checkbox in a blog post.

Licence coverage is not a blanket compliance answer. Our licence covers the telephony we operate. It does not cover what your application does with the call, what you send to a model, or what you retain afterwards.

Consent, disclosure and DLT content registration stay with you and your use case.

Residency has a boundary, and your bot is probably on it

India-resident media covers the telephony side: the media path we operate and the recordings we store. It stops at your endpoint. Once audio reaches your socket, residency is your design problem, and it leaks in three places:

Where it leaks What to check
Bot hosting region A bot outside India pulls Indian call audio across a border on every frame, whatever the telephony does
Model provider region Your STT/LLM/TTS vendor’s inference region and its retention default is a second, separate boundary
Your own retention Transcripts, prompt logs, tool-call payloads and eval corpora in your data lake are usually the largest uncontrolled copy

The practical consequence: running your bot in-region is a compliance control as well as a latency decision. Both arguments point the same way, which helps when you are asking for infrastructure budget.

Ask your model vendor for its inference region and retention posture in writing. That answer, not your telephony diagram, is what an auditor will eventually probe.

Build the audit trail on identifiers, not timestamps

Compliance reviews rarely fail on architecture. They fail on reconstruction — someone asks what a specific call said and did, and nobody can assemble it.

  • Treat the call identifier and stream identifier as the join keys across telephony callbacks, your bot’s logs, your CRM, your recording store and your SIEM.
  • Log both on every hop, including your model calls. Correlating on timestamps alone breaks as soon as clocks drift or a retry lands out of order.
  • Record UTC everywhere and convert at the presentation layer.
  • Assume callbacks arrive more than once and out of order. Make handlers idempotent and dedupe on identifiers, not arrival order.

Do this and an audit request is a query. Skip it and it is a week.

What stays yours

Your responsibility Why
Consent and disclosure copy Industry and use-case specific
Confirming NDNC handling for your account Depends on configuration and campaign type
What you send to the model PII minimisation, prompt policy
Retention in your systems CRM, data lake, ticket store
Idempotent webhooks Callbacks retry; dedupe on identifiers

On the Flow path, Passthru is the pre/post webhook for CRM and compliance logging. Other entries use their status and completion callbacks — same job, different surface. Passthru is not available on Connect Voice AI.

WhatsApp calling

Where WhatsApp calling is enabled, treat it as an IP channel with platform policy constraints. WhatsApp voice is not bridged to the PSTN on Exotel. Design handoffs to SIP or IP destinations accordingly, and check current product docs before promising a PSTN transfer from a WhatsApp voice leg.


Part 2 — Failure

Connecting to your bot

Your bot should assume transient failure is normal. So do we.

Behaviour What it means for you
Bounded handshake timeout A stuck TLS or proxy upgrade does not hang forever
A small number of retries with a short delay Deploy restarts, cold starts and brief DNS blips can still succeed
Read and write deadlines Stalled sockets do not sit open indefinitely
Clean failure if the bot never becomes ready The stream ends with a diagnosable outcome rather than a silent half-open call

Current behaviour is a handshake timeout of a few seconds and up to three attempts a few hundred milliseconds apart, then a clean failure. Design against the shape of that rather than the exact numbers — treat the values in the docs as current behaviour, not a contract.

The practical consequence is the same either way: you get one short window to be reachable. Cold-start functions and aggressive load-balancer idle timeouts are the two biggest causes of setup failure, and both are on your side.

Two signals, not one

This is where triage usually goes wrong.

A failed stream produces two independent signals on two different sides of the boundary, and they do not share a vocabulary:

Signal What it is Where you read it
WebSocket close code The standard RFC 6455 code when the socket ends Your own WebSocket server, plus your proxies and load balancers
Stream outcome Success or failure with optional detail Passthru and other callbacks, alongside status, disposition and who disconnected

Neither is sufficient alone. Correlate them on the call identifier and UTC time.

Close codes you will actually see

Code Meaning First thing to check
1000 Normal close Nothing. A healthy call ending
1001 Endpoint going away Your own restarts and rolling deploys
1002–1003 Protocol or data-type problem Framing, and what your library sends on error
1006 Abnormal close, no close frame The most common one. Network drop, TLS, timeout, load-balancer idle reap, or a crash. Check the path and timeouts first, not your application logic
1007–1009 Payload, policy or message size Oversized frames, unexpected encoding
1011 Server error handling the connection Your handler threw
1012–1013 Restart or try again Stack-dependent; treat as transient

1005 and 1015 are reserved or synthetic in many libraries — you may still see them, and they usually mean your library never received a real code. Codes in the 3000s or the private-use 4000–4999 range come from your own infrastructure, not from us.

The point worth internalising: 1006 is not a bug report. It is what you get when nobody had a chance to say goodbye, and the cause is almost always in the network path or a timeout setting rather than in your bot’s logic. Teams lose days reading application code because a close code looked like an application error.

Stream error detail

When streaming fails, the callback’s error field may carry a short diagnostic string. Treat it as opaque — send us the full value rather than parsing it, because parsing a diagnostic string is a dependency on something that was never a contract.

Interpret it together with the stream status, who disconnected, and the disposition. If status and error detail seem inconsistent, trust the error detail and escalate with both.

What to capture before escalating

Have these ready and a support conversation takes one round trip instead of four:

  1. The call identifier and stream identifier
  2. UTC timestamp of the failure
  3. The close code and its reason string
  4. The full stream error text, unparsed
  5. A short server log excerpt around the failure, secrets redacted

Log all of these by default. Reconstructing them afterwards is the expensive path.

Triage by symptom

Symptom Check first
Many setup failures Bot URL, TLS chain, IP allow-list, cold start, load-balancer idle timeout
Calls connect but the bot is silent Start-event handling, sample rate, reverse path for duplex
Talk-over, no barge-in Interrupt handling; a model call blocking the read loop
“Sounds laggy” under load Bot CPU, concurrency headroom, worker count, GC pauses
Callback storms or duplicates Idempotency keys; do not treat retries as new calls
Failures clustered at deploy time Connection draining. A dropped socket is a lost conversation, not a retried request

Session ready before media matters

We aim to have your WebSocket connected and past the start event before meaningful media exchange, so stream and call identifiers, the sample rate and your custom parameters all exist before audio flows. If the bot cannot be reached after retries, the stream fails cleanly rather than sending audio nowhere.

The design implication: do not act on media before the start event. Treat start as the point at which the session’s identifiers and format are trustworthy.


Part 3 — Scale

Capability What it gives you
Live concurrency visibility Gate outbound waves; size bot pools against your account limit
Explicit over-limit signalling Backpressure you can act on instead of silent drops
Active-Active and DR The default posture on our side
Multi-operator telephony failover A carrier issue is not automatically your outage
Proven volume The platform currently carries 80M+ streaming legs a month, with PSTN call volume around four times that. A meaningful share of Voice AI streaming in India runs on this stack

Treat concurrency as a control loop, not a limit

The common failure is checking the concurrency number once before a campaign, then dialling open-loop for two hours. Concurrency is a shared, account-level budget that inbound traffic, retries and other campaigns all draw from at the same time.

  1. Reserve headroom before you start. Inbound traffic does not stop because you launched a campaign, and retries consume budget too.
  2. Ramp rather than step. A step change hides which resource saturated first.
  3. Sample continuously and shed in your own dialer. You control your call rate; you do not control inbound volume. The dialer is the only place backpressure applies cleanly.
  4. Watch your bot’s queue latency, not just CPU. Rising queue latency is the earliest honest signal you are past capacity. CPU saturates later and recovers more slowly.
  5. Know your real ceiling. It is the minimum of your account concurrency, your worker pool, and your model provider’s rate limit. Most teams are throttled by the third and blame the first.

Published playbook: scaling Voice AI from 100 to 10,000 concurrent.


Production checklist

  1. Choose the calling-line series before you write the dialer — 140 for promotional, 1600 or 1601 for service and transactional, and not a mobile or landline DID for promotional campaigns.
  2. Do not mix promotional and transactional traffic on one caller ID.
  3. Do not gate promotional delivery on a global 9-to-9 clock. Eligibility is per recipient, against their registered time bands and content categories, and a blocked call is an expected outcome to record rather than an error to retry.
  4. Get a written answer on NDNC handling for your campaign type before launch.
  5. Check residency across all three boundaries — your bot’s region, your model vendor’s region, your own retention.
  6. Log the call identifier and stream identifier on every hop, in UTC.
  7. Make webhook handlers idempotent; dedupe on identifiers, not arrival order.
  8. Set idle timeouts above your maximum call length.
  9. Keep the accept path warm; you get one short window to be reachable.
  10. Handle connection-ready and start before assuming media.
  11. Never block the socket on slow model calls, and make TTS cancellable.
  12. Log the close code and full error text on every stream, and correlate the two signals.
  13. Sample concurrency continuously during campaigns, not once before.
  14. Know which of the three ceilings actually binds you.
  15. Drain connections on deploy, and rehearse a rolling restart at low volume while watching failure rates.
  16. Run a number pool, rotate the number that appears on the network, and enforce a rest period before reuse.
  17. Alert on per-number answer rate — it is your earliest signal that a number has been flagged.

In short

The telephony layer gives you retries and deadlines on the path to your bot, concurrency you can operate against, multi-operator failover underneath, and two clear failure signals so bot problems and network problems stay separable.

What it does not give you is a compliant application. Consent, what you send to a model, what you retain, and confirming your NDNC obligations are yours — and in our experience they are where launches actually get held up, not the telephony.


Related reading

Regulatory sources

The regulation has been amended repeatedly and the numbering series are still being rolled out in phases. Check the current text before relying on any of this, and confirm your own account’s configuration with us.