My Voice AI Demo Was Hit by a $982 Traffic-Pumping Attack! Here’s What I Learned

October 06, 2026
Written by
Reviewed by
Paul Kamp
Twilion

Have you ever assumed a demo was safe because you never shared its URL publicly? I did. I built a voice AI assistant for events and conferences, left it running on the internet, and later discovered that someone had used it to generate hundreds of unauthorized international calls.

Twilio describes voice toll fraud – also known as International Revenue Sharing Fraud (IRSF) – as abusing an application to generate calls to international premium-rate numbers controlled by a bad actor. This incident’s volume, destinations, durations, and automated audio were consistent with that pattern, although my logs could not prove who controlled the numbers or received revenue.

The attack against my voice AI demolasted about four hours. It cost approximately $971.50 in Twilio usage and another $10.58 in Google Gemini usage. In this post, I'll show you how I reconstructed what happened, how the attackers kept the calls alive, and the controls every public voice AI demo should have. Let's dive in!

The project

The project is a Twilio Conversation Relay voice assistant built with FastAPI. Conversation Relay connects a phone call to a WebSocket, handles speech recognition and text-to-speech, and lets the application send the caller's transcribed speech to a Large Language Model (LLM).

The application supports OpenAI, Google Gemini, and Amazon Bedrock. A web interface lets someone configure the model, personality, and voice, then enter a phone number to receive an outbound call.

The important endpoint looked like this (warning: don’t copy this one!):

@app.post("/api/call")
async def make_call(call_request: CallRequest):
    call = twilio_client.calls.create(
        to=call_request.phoneNumber,
        from_=os.getenv("TWILIO_PHONE_NUMBER"),
        url=f"https://{DOMAIN}/twiml",
        method="GET",
    )

That flow was convenient during a live demo: enter a number, select Call Me Now, and talk to the AI assistant. It also meant that anyone who could reach /api/call could ask my server to make a real phone call with my Twilio credentials.

I never promoted the hosted URL

I didn't share the application on social media. I used it at events and conferences, and I assumed the URL had a small audience.

My GitHub repository was public, though, and the deployment was connected to a public custom domain. The URL could have been found through the repository, code search, DNS or certificate records, an event attendee, or routine internet scanning. I can't prove which path was used because the application didn't retain the original client IP or referrer.

That became one of my first lessons: an internet-accessible demo is public even when its URL isn't promoted. A URL is an address, not an access control.

What happened

On August 15, 2026, an automated client began calling /api/call. The traffic ran from 10:32 a.m. until 2:39 p.m. Central Time.

Signal Finding
Requests to /api/call 1,008
Successful call creations 921
Failed requests 87
Completed calls 903
Calls to Israel 920
Unique Israeli destinations 156
Twilio fraud-related cost $971.50
Google Gemini cost $10.58
Combined impact $982.08

This wasn't a single person repeatedly calling my Twilio number. Every fraudulent call was created through the REST API from one of my Twilio numbers. Of the 921 attempts, 920 went to the same narrow Israeli destination range beginning with +972229.

The pattern was highly concentrated. One hundred and four destination numbers were each called seven to ten times, accounting for 851 calls. The completion rate was approximately 98 percent, and the calls produced almost 30 hours of connected traffic in a four-hour window.

Following the evidence with the Twilio CLI

I started with the Twilio CLI and queried the Voice call records for the incident date:

twilio api:core:calls:list \
  --start-time-after 2026-08-15 \
  --start-time-before 2026-08-16 \
  --no-limit \
  --output json

The useful fields were direction, from, to, status, startTime, duration, and price. Grouping those records immediately showed that every call had the direction outbound-api, every call used the same source number, and nearly every destination began with Israel's +972 country code.

The usage records separated the cost into its major parts:

Product usage Cost
Outbound connectivity $828.68
ConversationRelay $137.34
Voice Insights $5.49
Fraud-related Twilio total $971.50

An additional charge appeared on August 24, which initially looked like a second incident. The call records showed otherwise: it matched six calls that began within 21 seconds of one another near the end of the August 15 incident. They lasted between 9 minutes 21 seconds and 10 minutes 1 second, averaging approximately 9 minutes 48 seconds. That tight clustering suggested coordinated automation rather than random caller behavior.

This is a good reminder to correlate usage records with the underlying resource logs instead of relying on the billing date alone.

Correlating the calls with Azure logs

The application was running in Azure Container Apps and sending console logs to Log Analytics. I queried the incident window for requests to the call endpoint:

az monitor log-analytics query \
  --workspace <workspace-id> \
  --analytics-query '
ContainerAppConsoleLogs_CL
| where TimeGenerated between (
    datetime(2026-08-15T15:25:00Z) ..
    datetime(2026-08-15T19:40:00Z)
  )
| where ContainerAppName_s == "twilio-cr-byom"
| where Log_s contains "/api/call"
| summarize
    requests=count(),
    successful=countif(Log_s endswith "200 OK"),
    failed=countif(Log_s endswith "500 Internal Server Error")
'

The result was the connection I needed: 1,008 POST requests, 921 successful responses, and 87 failures. The 921 successful application requests matched the 921 Twilio call records exactly.

Twilio's Request Inspector provided another link in the chain. A representative fraudulent call fetched the hosted application's /twiml endpoint and connected to its ConversationRelay WebSocket. This ruled out an unrelated Twilio application in the same account.

Unfortunately, the application logs contained Azure's internal ingress address rather than the attacker's public IP. The original X-Forwarded-For value wasn't logged, and there was no proxy in front of Container Apps retaining access logs. I could identify the exploited endpoint, but not the person or machine behind the requests.

Was this traffic pumping or a scam campaign?

The next question was whether the calls were attempting to scam real people or were designed to inflate traffic.

Twilio's Anti-Fraud Developer's Guide describes voice traffic pumping as outbound calls directed to destinations controlled by a fraudster. Those calls may be answered by automated recordings or silence to keep them connected and generate per-minute revenue.

The call pattern already matched that description: one international range, repeated calls to the same numbers, a very high completion rate, and sustained automated traffic.

The application's speech-to-text logs made the intent even clearer. Across 8,363 transcribed prompts, there were only 541 distinct lines. The remote side replayed the same conversation-extension prompts hundreds of times:

Remind me again. What services did you say you provide?
What was that last service again?
Say that one again.
Go over them one more time for me.
Walk me through it again.
I want to make sure I understood you correctly before I say anything else.
Tell me again which one you think fits me best.

Other calls played unrelated prerecorded audio, including a discussion about the 2012 Nibiru prediction and religious commentary. Some recordings repeatedly said, "Thanks for calling our numbers. We wish you a happy trafficking."

I also searched the 7,791 assistant responses for common vishing themes. There was no repeated prize, warrant, jury-duty, gift-card, Bitcoin, wire-transfer, or "send money" language. The evidence strongly supported international artificially inflated traffic (AIT) rather than a scam campaign aimed at consumers.

The available evidence did not identify who ultimately benefited financially, so I cannot conclusively attribute motive or ownership. What I can say is that the behavior was consistent with automated traffic pumping.

The AI provider was affected too

Every connected session used Google Gemini 2.5 Flash. The Azure logs showed 614 Conversation Relay sessions, 8,363 prompts processed, 7,791 responses sent, and no model configuration changes during the incident.

Google AI Studio showed one cost spike around the attack date, totaling $10.58. That was small compared with the voice connectivity cost, but it demonstrated an important point: a compromised voice workflow can create charges across several providers at the same time.

For a voice AI application, your financial boundary includes telephony, speech services, the LLM, infrastructure, logging, and any downstream tools the model can call.

Why Twilio signature validation wasn't enough

Twilio signs inbound webhook and WebSocket requests with X-Twilio-Signature. Applications should validate that signature using the server-side SDK, as described in Twilio's webhook security documentation.

Signature validation is required, but it wouldn't have stopped this incident. The request flow looked like this:

Attacker
   -> POST /api/call
      -> My server authenticates to the Twilio REST API
         -> Twilio creates a legitimate call
            -> Twilio sends a correctly signed /twiml request
               -> Twilio opens a correctly signed WebSocket

The attacker didn't need to impersonate Twilio – my application did the privileged work on the attacker's behalf. The resulting Twilio callbacks were genuine and would have passed signature validation.

Signature validation belongs on /twiml and the ConversationRelay WebSocket handshake. Authentication and authorization belong on /api/call and /api/config. They solve different problems, and this application needed both.

The first controls I implemented

My first change was a hard two-minute maximum call duration:

MAX_CALL_DURATION_SECONDS = 120
call = await asyncio.to_thread(
    twilio_client.calls.create,
    to=phone_number,
    from_=os.getenv("TWILIO_PHONE_NUMBER"),
    url=f"https://{DOMAIN}/twiml",
    method="GET",
    time_limit=MAX_CALL_DURATION_SECONDS,
)

The application also schedules a server-side timer that ends the call through the Twilio API. The Twilio time_limit and application timer provide two opportunities to stop an unexpectedly long call.

Next, I added a combined inbound and outbound daily limit for each remote number:

DAILY_CALL_LIMIT = int(os.getenv("DAILY_CALL_LIMIT", "5"))

Before creating an outbound call, the application checks both Twilio's call history and a local set of newly created Call SIDs. The local set closes the delay between creating a call and seeing it in Twilio's list API, while an asynchronous lock prevents simultaneous requests from racing past the check. The same quota is enforced when /twiml handles an inbound call.

Using Twilio history means an application restart doesn't reset the daily count. Once the limit is reached, outbound requests receive HTTP 429, inbound calls are rejected, and existing outbound flows are disconnected.

These controls reduce exposure, but they are not the complete solution. The attacker rotated through 156 destination numbers. A five-call limit for each destination would still permit hundreds of calls without a global ceiling.

A safer baseline for voice AI demos

If I were deploying this demo from scratch today, I would use several independent controls.

Require access before action

Protect every endpoint that can spend money or alter runtime behavior. /api/call needs authentication and authorization, and /api/config should not be writable by anonymous visitors. For event demos, use an expiring access code, authenticated operator session, or destination allowlist. Keep outbound calling disabled by default and enable it only for the demo window.

Limit several dimensions

A per-number rate limit is useful, but add per-IP, per-user, global concurrent-call, global daily-call, and daily-cost limits. Think of these limits as circuit breakers: if any one dimension behaves unexpectedly, the application stops creating calls.

Restrict destinations

Enable only the countries and number ranges the demo requires. Twilio recommends limiting proof-of-concept applications to required low-risk destinations through Voice Dialing Geographic Permissions. A US event demo that only calls verified US attendees has no reason to permit arbitrary international dialing.

Validate Twilio requests

Validate X-Twilio-Signature on /twiml and on the Conversation Relay WebSocket handshake. This prevents direct clients from pretending to be Twilio, forging call parameters, or opening unauthorized model sessions. It complements application authentication rather than replacing it.

Add financial guardrails

Create Twilio Usage Triggers for calls, minutes, and price, and configure budgets or quotas with every AI provider. An alert should also feed an automated response, such as disabling outbound calls when a threshold is crossed. Alerts alone may arrive after costs have started accumulating.

Isolate demos

Use a separate Twilio subaccount, API key, AI-provider project, and infrastructure environment for each public demo. Grant only the permissions and destinations it needs. When the event ends, revoke the keys or shut the environment down.

Protect credentials

Keep Twilio, AI-provider, and cloud credentials in a secrets manager. If using GitHub to host the code, enable GitHub secret scanning and push protection.Use scoped API keys and least privilege. Rotate or revoke demo credentials after events.

And never commit .env files.

Log for investigations without logging everything

Record the trusted forwarded IP, authenticated user, endpoint, Call SID, hashed destination, status, duration, and model selection. Retain those structured events long enough to investigate an incident.

The full conversation logs were valuable during this investigation, but they can contain sensitive speech. In production, avoid logging raw prompts and responses by default. Use short retention, access controls, and redaction when content logging is necessary.

What this incident changed for me

I love building demos because they make an API tangible. You press a button, your phone rings, and you're suddenly talking to an AI model – that immediate feedback is what makes developer tools exciting!

This incident helped change how I think about the boundary around those demos. The code may be experimental, but the credentials, phone network, cloud infrastructure, and billing accounts are real. If a public endpoint can trigger a paid action, it deserves the same threat modeling as a production endpoint.

Learning in public also means sharing the failures, not only the polished launch. My hope is that this story helps you review your own callbacks, dialers, OTP forms, AI agents, and conference demos before a bot does it for you!If you’d like to explore another voice AI implementation, check out my Twilio Agent Connect with AWS Bedrock AgentCore demo. It combines Twilio ConversationRelay with an AWS-hosted agent and a Lambda webhook proxy, with the infrastructure and deployment steps included in the repository.

Conclusion

This voice AI demo turned into a practical lesson in traffic pumping, observability, and defense in depth. Twilio logs identified the international call pattern, Azure logs proved which endpoint was exploited, and the speech transcripts revealed an automated script built to keep calls connected.

Duration and per-number limits are a good start, but authentication, global circuit breakers, geographic restrictions, signature validation, provider budgets, and demo isolation are what make the system resilient. Keep building, keep learning, and treat every internet-accessible demo like someone you didn't invite will eventually find it – because they eventually will.

Want actionable updates on the fraud trends Twilio is seeing? Check out our Quarterly Fraud updates for more.

Rishab Kumar is a Developer Evangelist at Twilio and a cloud enthusiast. Get in touch with Rishab on Twitter @rishabincloud and follow his cloud, DevOps, and DevRel adventures at rishabkumar.com .