1. Spam protection
CallerAPI Documentation
  • Quickstart
  • Use cases
    • For carriers (MNOs/MVNOs)
    • CPaaS platforms
    • Cloud communications providers
    • SIP trunking providers
    • PBX/Cloud PBX
    • UCaaS vendors
  • Account
    • Balance and email
      GET
  • Spam protection
    • Voice firewall
    • Daily spam reports
      • Webhook
        • Subscribe to daily reports
        • Unsubscribe from daily reports
        • List webhook subscriptions
        • Manual dispatch of reports
        • Test webhook
      • REST
        • Fetch daily spam reports
    • Spam score + HLR
      GET
    • 15 days spam CSV snapshot
      GET
  • Mobile SDK
    • Get started
    • Quickstart
      • What you get
      • Get your keys
      • Android
      • iOS
      • Flutter
      • React Native
      • Check it works
    • Call screening
      • What each platform can do
      • Android call screening
      • iOS prerequisites
      • iOS add the extension
      • iOS test on device
    • Reference
      • Methods
      • Errors
      • Limits and billing
      • Troubleshooting
  • Data partners
    • Partner tems & docs
    • Upload spam reports
      POST
    • Upload contacts
      POST
  • Fraud prevention
    • Ported date
      GET
    • Porting history
      GET
    • Online presence
      GET
    • KYC user identity
      POST
  • Schemas
    • Spam protection
      • Spam score request
      • Business info
      • Carrier info
      • Complaint (without number)
      • Daily spam reports request
      • Complaint (with phone)
  1. Spam protection

Voice firewall

This guide is for the engineer who connects a switch, a PBX, a carrier platform, or a call application to the CallerAPI voice scam filter. It covers the connection paths, the wire protocol, the events, the verdict, and the block action. The honeypot shares the same connections. Section 13 shows how to hand a call to it.
Base URL: https://api.callerapi.com/api/voice
Socket URL: wss://api.callerapi.com/api/voice

1. What the filter does#

The filter listens to a live call. It transcribes the caller, scores the call while it runs, and tells you when the score changes. At the end of the call it runs one full analysis on the whole transcript and stores the result.
The filter returns:
A score from 0 to 1. This is the probability that the caller is a scammer.
A level: clean, suspicious, likely_scam, or scam.
A category: one exact CallerAPI complaint subject, or none.
Tactics, signals, and a short summary.
Intel: callback numbers, names, payment rails, remote tools, and other facts the caller exposed.
When you set report=true, a final score of scam files the caller's number and every callback number as a complaint. That block applies across CallerAPI: the SIP firewall declines the number and the SDK block lists include it.

2. What the filter does not do#

It does not answer the call and does not speak. The honeypot does that.
It does not change the call. What you do with a verdict is your decision.
It does not need your audio when you already run speech recognition. Send text instead. See section 8.
It does not prove fraud. A verdict is a probability. scam means a score of 0.85 or more.

3. Before you start#

1.
Get your API key from the dashboard. It is the same key as the rest of the CallerAPI API.
2.
Check what the deployment supports:
{
  "status": "success",
  "capabilities": {
    "scan": true,
    "live_filter": true,
    "honeypot": true,
    "llm": "openai:gpt-4.1-mini",
    "analysis_llm": "openai:gpt-5.4-mini",
    "stt": "grok:stt",
    "tts": "openai:tts-1",
    "personas": 8,
    "prices": {
      "credit_usd": 0.0039,
      "filter_per_minute": 3,
      "honeypot_per_minute": 8,
      "scan": 2,
      "scan_audio_minute": 1,
      "managed_telephony_per_minute": 4
    }
  }
}
live_filter must be true for the socket. When it is false, the socket answers 503.
3.
Make sure the account has credits. The filter charges at the start of the call and once per started minute. When the account runs out of credits, the session ends. The final session.ended event is still sent.

Authentication#

Send the key in the X-Auth header. The socket and the TwiML and TeXML endpoints also accept ?key=YOUR_KEY in the URL, because telephony vendors cannot set headers on a media socket.
Every JSON response has "status": "success" or "status": "error" with a message.

4. Choose a connection#

You haveUseSection
A switch, PBX, SBC, or media server that can open a WebSocketPlain WebSocket protocol5
A Twilio numberTwiML endpoint6
A Telnyx numberTeXML endpoint7
Your own speech recognition and no audio to sendPlain WebSocket protocol, text messages8
A recording or a transcript after the callPOST /scan9
A SIP trunk and you want the call answered by a personaSIP INVITE to the honeypot13
The plain WebSocket protocol is the general path. Everything else is a wrapper around it.

5. Connect from your switch (plain WebSocket)#

5.1 Open the socket#

wss://api.callerapi.com/api/voice/filter/stream?key=YOUR_KEY
Optional URL parameters:
ParameterValuesDefaultMeaning
webhookHTTPS URLnoneReceives events. HTTP URLs are ignored.
languageBCP-47 code, for example enautoTranscription hint. Also turns spoken numbers into digits, which improves extraction. Set it when you know the language.
reporttrue or falsefalseFiles the caller when the final score is scam.
You may send the same options in the params object of the start message instead. A value in the start message wins over the URL.
The server takes at most 200 live sessions per node. Above that the socket answers 503. Retry on a new connection. The load balancer sends it to another node.

5.2 Send the start message#

Send one text frame within 15 seconds of the connection. Otherwise the server closes the socket.
{
  "type": "start",
  "call_id": "your-call-id",
  "from": "+15559990000",
  "to": "+15551234567",
  "sample_rate": 8000,
  "encoding": "pcm16",
  "params": {
    "webhook": "https://example.com/callerapi-voice",
    "language": "en",
    "report": "true"
  }
}
FieldRequiredNotes
typeyesAlways start.
call_idnoYour identifier for the call. It is echoed in every event and stored with the session.
fromfor reportCaller number in E.164 with a leading +. The block action uses it. Without it, only callback numbers can be filed.
tonoCalled number in E.164.
sample_rateno8000 to 48000. Default 8000.
encodingnopcm16 (16-bit little-endian, default) or mulaw (G.711 mu-law).
paramsnoSame keys as the URL parameters. Values are strings.

5.3 Send audio#

Send the caller's audio as binary frames in the encoding and sample rate you declared. Nothing else is needed. The server does not acknowledge audio frames.
Rules:
Send the remote caller's audio only. On this protocol every binary frame is treated as the caller.
Do not mix both parties into one channel. A mixed channel confuses transcription and the verdict applies to the caller alone.
Send frames of 20 ms to 200 ms. Shorter frames waste bandwidth. Longer frames add latency to the verdict.
Send frames in real time. The transcriber is a streaming service and expects live pace.
Empty binary frames are ignored.
The bit rate is 128 kbit/s for pcm16 at 8 kHz and 64 kbit/s for mulaw.

5.4 Optional client messages#

Text frames, one JSON object each.
MessageMeaning
{"type":"dtmf","digit":"1"}The caller pressed a key. Digits are collected into intel.dtmf.
{"type":"transcript","text":"...","track":"inbound","final":true,"end_of_turn":false}Words you transcribed yourself. See section 8.
{"type":"stop"}The call ended. Closing the socket has the same effect.
Unknown text frames are ignored.

5.5 Receive events#

The server sends events as text frames. Each is one JSON object with a type field. Section 10 lists them. On this protocol you receive every event on the socket, including transcript.line.
The filter never sends binary frames. When you reuse the same client for the honeypot, expect binary audio frames and the {"type":"clear"} message there.

5.6 End the call#

Send {"type":"stop"} or close the socket. The server then:
1.
Flushes transcription.
2.
Runs the final analysis on the full transcript. This takes up to 20 seconds.
3.
Files the caller when report=true and the final score is scam.
4.
Sends session.ended on the socket and to the webhook.
5.
Closes the socket.
Keep the socket open until you receive session.ended, or read it from the webhook or from GET /sessions/{id}.
A filter session ends by itself after 60 minutes.

5.7 Example: FreeSWITCH with mod_audio_fork#

mod_audio_fork opens a WebSocket, sends one text frame with the metadata you give it, then streams 16-bit PCM. Pass our start message as the metadata. Check the argument order against your module version.
uuid_audio_fork <uuid> start wss://api.callerapi.com/api/voice/filter/stream?key=YOUR_KEY mono 8k {"type":"start","call_id":"<uuid>","from":"+15559990000","to":"+15551234567","sample_rate":8000,"encoding":"pcm16","params":{"webhook":"https://example.com/hook","language":"en"}}
Run it on the caller's channel with mono, so only the caller's speech is sent. Events arrive on the socket. Most forks do not expose them to the dialplan, so use the webhook parameter to act on them.
For Asterisk, Kamailio with RTPEngine, or a custom media server, write a small bridge that receives RTP, decodes to PCM or passes G.711 through as mulaw, and speaks the protocol above.

6. Connect from Twilio#

Set the voice URL of the number to:
https://api.callerapi.com/api/voice/filter/twiml?key=YOUR_KEY&forward=%2B15551234567&webhook=https://example.com/hook&language=en&report=true
ParameterRequiredMeaning
forwardyesThe E.164 number the call rings, URL-encoded.
webhookrecommendedTwilio drops the socket on unknown messages, so no events reach you on the stream. The webhook is the only live channel.
tracks=bothnoAlso transcribes the called party, labelled callee. Transcription cost doubles.
language, reportnoAs in section 5.1.
The endpoint returns TwiML that starts a Media Stream to the filter and dials forward. The caller's number and your options ride on the stream as <Parameter> elements.
If you write your own TwiML, point <Stream url> at wss://api.callerapi.com/api/voice/filter/stream?key=YOUR_KEY, use track="inbound_track", and add <Parameter name="from" value="+15559990000"/> with the From value from the Twilio voice webhook. Twilio does not send the caller's number in the start frame, so without the parameter from is empty and the block action has no caller number.

7. Connect from Telnyx#

Create a TeXML application and set its voice URL to:
https://api.callerapi.com/api/voice/filter/texml?key=YOUR_KEY&forward=%2B15551234567&webhook=https://example.com/hook
The same parameters as section 6 apply. Telnyx sends from and to in the start frame, so they are always present. The stream uses codec="PCMU". PCMA is also accepted.

8. Send text instead of audio#

When your platform already runs speech recognition, send the words and skip the audio. This removes the transcription cost from the call and works on any protocol that lets you open a WebSocket.
Open the socket and send the start message as in section 5. Then send one text frame per utterance:
{"type":"transcript","text":"This is the IRS. There is a warrant for your arrest.","track":"inbound","final":true}
FieldDefaultMeaning
textrequiredThe words. Empty text is ignored.
trackinboundinbound is the remote caller. outbound is the local party, labelled callee.
finaltruefalse marks an interim result. Interims are not scored and not stored.
end_of_turnfalseThe honeypot uses it to answer sooner. The filter ignores it.
You can send audio and text on the same socket. Both feed one transcript.

9. Score a recording or a transcript (POST /scan)#

Use this after the call, or for a recording from another system.
JSON body:
FieldTypeNotes
transcriptstringText of the call. This or audio is required.
audio_base64stringBase64 audio.
encodingstringwav (default), pcm16, or mulaw.
sample_rateintRequired for pcm16. Default 8000 for mulaw.
languagestringBCP-47 hint.
fromstringCaller number in E.164. Required for report.
tostringCalled number in E.164.
reportboolFile the caller when the score is scam. Default false.
Multipart is also accepted: a file field named audio plus the same fields as form values. The body limit is 25 MB. The request times out after 90 seconds.
Response:
{
  "status": "success",
  "session_id": "3f0c1a2e-...",
  "verdict": {
    "score": 0.93,
    "level": "scam",
    "category": "Calls pretending to be government, businesses, or family and friends",
    "tactics": ["authority_impersonation", "threat", "untraceable_payment", "urgency"],
    "signals": ["invokes Social Security", "asks for gift cards", "threatens arrest or legal action"],
    "summary": "Caller impersonates Social Security and demands gift cards under threat of arrest.",
    "source": "combined"
  },
  "intel": {},
  "transcript": "...",
  "reported": true,
  "reported_numbers": 1
}

10. Events#

One event shape serves the socket and the webhook.

10.1 Common fields#

FieldMeaning
typeEvent type. See 10.2.
session_idThe session. Use it with GET /sessions/{id}.
kindfilter or honeypot.
call_idYour call_id, the Twilio CallSid, the Telnyx call control id, or the SIP Call-ID.
from, toE.164 numbers when known.
atRFC 3339 time.

10.2 Event types#

typeWhenExtra fieldsWebhook
session.startedThe start message was accepted.persona (honeypot only)yes
transcript.lineOne utterance is final.speaker, textno
verdictThe score or category changed enough to matter.verdictyes
session.errorA provider refused a request.error, textyes
session.endedThe call ended and the final analysis is done.verdict, intel, transcript, duration_seconds, reported, reported_numbersyes
speaker is caller, callee, or the persona's name.
session.error ends a honeypot session when the greeting could not be spoken. In every other case it is advisory and the session continues.

10.3 When a verdict event is sent#

The filter scores the caller's text after every final utterance with the rule engine, and with the model every 30 seconds when at least 120 new characters arrived. A verdict event is sent when:
The first score reaches 0.3 or more.
The level changes.
The category changes.
The score moves by 0.15 or more.
A rules-only score below 0.5 is never sent. A rules-only score never lowers a score the model produced. Expect zero events on a clean call and two to five on a scam call.

10.4 Examples#

verdict:
{
  "type": "verdict",
  "session_id": "7463336c-c965-450f-b188-e6908e969395",
  "kind": "filter",
  "call_id": "your-call-id",
  "from": "+15559990000",
  "to": "+15551234567",
  "at": "2026-09-13T15:04:11Z",
  "verdict": {
    "score": 0.72,
    "level": "likely_scam",
    "category": "Advance Fee Loan",
    "tactics": ["untraceable_payment", "urgency"],
    "signals": ["pitches a loan", "asks for money before a loan"],
    "source": "heuristic"
  }
}
session.ended:
{
  "type": "session.ended",
  "session_id": "7463336c-c965-450f-b188-e6908e969395",
  "kind": "filter",
  "call_id": "your-call-id",
  "from": "+15559990000",
  "to": "+15551234567",
  "at": "2026-09-13T15:07:52Z",
  "verdict": {
    "score": 0.94,
    "level": "scam",
    "category": "Advance Fee Loan",
    "tactics": ["untraceable_payment", "urgency", "callback_pressure"],
    "signals": ["pay us up front, and then we would give you the loan", "send us a payment right now to +19998887766"],
    "summary": "Caller offers a $10,000 loan and demands an upfront payment to a phone number within the hour.",
    "source": "combined"
  },
  "intel": {
    "names": ["Dmitry"],
    "organizations": ["Bankloans.com"],
    "callback_numbers": ["+19998887766"],
    "amounts": ["$10,000"]
  },
  "transcript": "caller: Hi, this is Dmitry from Bankloans.com...\n",
  "duration_seconds": 221,
  "reported": true,
  "reported_numbers": 2
}

10.5 Webhook delivery#

Set webhook to an HTTPS URL. The server posts every event except transcript.line.
HeaderValue
Content-Typeapplication/json
X-Voice-EventThe event type.
X-Voice-Signaturesha256=<hex>. HMAC-SHA256 of the raw request body. The key is your API key.
Rules:
Answer with a 2xx status within 5 seconds. The server does not wait longer.
There is no retry. Store the session_id and read GET /sessions/{id} when a post is missing.
Events for one session may arrive out of order under load. Use at to order them.
The post is made from the API nodes. Do not depend on a fixed source IP. Verify the signature instead.
Verify the signature. Python:
Node.js:
Compute the HMAC over the raw bytes of the body, before any JSON parsing.

11. Read a verdict#

11.1 Fields#

FieldMeaning
score0 to 1. Probability that the caller is a scammer.
levelBucket of the score. See 11.2.
categoryOne exact CallerAPI complaint subject, or none. See 11.3.
tacticsManipulation techniques observed. See 11.4.
signalsShort pieces of evidence. Above a score of 0.6 they quote the caller's words.
summaryTwo sentences at most. Present when a model produced the verdict.
sourceheuristic (rules only), llm (model only), combined (both), or stored (read back from the session record).

11.2 Levels#

LevelScoreSuggested action
cleanbelow 0.3Nothing.
suspicious0.3 to 0.59Tag the CDR. Show a soft warning if you have a UI.
likely_scam0.6 to 0.84Warn the called party. Consider recording.
scam0.85 and aboveWarn or drop the call. This is the level that files a complaint with report=true.
The actions are suggestions. The filter never acts on the call.

11.3 Categories#

category is one of these exact strings. They are the CallerAPI complaint subjects, so a filed complaint carries the same value.
Advance Fee Loan
Bank/Credit Card Company Imposter
Business Email Compromise
Calls pretending to be government, businesses, or family and friends
Charity
Computer & Technical Support
Counterfeit Product
COVID-19
Credit Cards
Credit Repair/Debt Relief
Cryptocurrency
Debt Collection
Lotteries, Prizes & Sweepstakes
Medical & Prescriptions
Moving
No Subject Provided
Phishing
Reducing Your Debt (Credit Cards, Mortgage, Student Loans)
Rental
Romance
Tax Collection
Travel/Vacation/Timeshare
Utility
Warranties & Protection Plans
Work From Home & Other Ways To Make Money
Yellow Pages/Directories
Hang Up
Scammers
Advertising
Surveys
Financial Services
Store
Company
Other
none means the call is clean. It is not a complaint subject and is never filed.
Two pairs are easy to confuse:
Advance Fee Loan is "pay a fee first, then we send you a loan". Reducing Your Debt is forgiveness, consolidation, or a lower rate on a debt the person already has.
Tax Collection is used when the story is the IRS or back taxes. The general imposter subject covers other agencies, businesses, and family emergencies.
GET /categories returns the list with a description of each, as JSON.

11.4 Tactics#

urgency
secrecy
authority_impersonation
remote_access
untraceable_payment
pii_harvest
otp_request
threat
prize_lure
family_fear
refund_bait
callback_pressure

11.5 Intel fields#

Every field is optional and omitted when empty.
names, organizations, callback_numbers (E.164), emails, urls, crypto_wallets, bank_accounts, card_numbers, gift_card_brands, payment_apps, remote_tools, remote_codes, case_numbers, amounts, addresses, scripts (verbatim lines of the pitch), dtmf (every digit pressed, in order).

12. Block a caller (report=true)#

With report=true the server files a complaint at the end of the session when all of these are true:
The final score is 0.85 or more. On the honeypot the threshold is 0.8.
The category names a fraud. none, Advertising, Surveys, Hang Up, and No Subject Provided are never filed. Other needs a score of 0.85 or more on both products.
There is at least one number to file: from in E.164 with a leading +, or a callback number the caller gave.
One complaint is filed per number. The subject is the category. The comment holds the summary and the intel, so an analyst sees why the number is there.
Effect: the SIP firewall declines the number, the SDK block lists include it, and lookups show it as reported. reported and reported_numbers in session.ended tell you what was filed.
Filing is a consequential action. Turn it on only after you have watched the verdicts on your own traffic.

13. Honeypot: hand the call over#

The honeypot answers the call as a persona, keeps the caller busy, extracts intel, and files the number. It uses the same connections as the filter.
PathHow
Plain WebSocketOpen wss://api.callerapi.com/api/voice/honeypot/stream?key=YOUR_KEY. Same start message as section 5.2. Send caller audio as binary frames. The server sends persona audio as binary frames in your encoding and rate, and {"type":"clear"} when you must drop queued playback. Do not send the persona's own voice back.
SIPSend an INVITE to sip:honeypot@HONEYPOT_HOST:5080 with G.711 (PCMU or PCMA) in the SDP offer. Add X-CallerAPI-Key: YOUR_KEY. No SRTP. RFC 4733 DTMF is understood. The caller's number is read from P-Asserted-Identity, then From. The honeypot ends the call with BYE.
SIP firewallIf your domain is on the CallerAPI SIP firewall, set spam_action=honeypot with PUT /api/domains/{id}. Spam calls are answered by a persona instead of a 603. No INVITE from you is needed.
TwilioVoice URL https://api.callerapi.com/api/voice/honeypot/twiml?key=YOUR_KEY.
TelnyxVoice URL https://api.callerapi.com/api/voice/honeypot/texml?key=YOUR_KEY.
Options, as URL parameters, params in the start message, or SIP headers:
OptionSIP headerDefaultMeaning
personaX-PersonarandomA persona id from GET /personas.
webhookX-WebhooknoneHTTPS URL for events.
reportX-ReporttrueFile the caller when the final score clears 0.8.
A honeypot call lasts at most 15 minutes. The persona hangs up after three unanswered silences of 8 seconds.

14. Sessions API#

GET /sessions?limit=50
GET /sessions/{id}
limit is 1 to 200. Any other value becomes 50. Sessions belong to the key that created them.
A session record:
FieldMeaning
idSession id.
kindfilter, honeypot, or scan.
protocolraw, twilio, telnyx, sip, or http.
call_id, from, toAs given or read from the vendor.
persona_idHoneypot only.
started_at, ended_at, duration_secondsTiming.
turnsNumber of transcript lines.
transcriptspeaker: text per line.
score, level, category, summaryThe final verdict.
intel_countNumber of distinct intel facts.
reportedtrue when at least one complaint was filed.
GET /sessions/{id} also returns verdict and intel as objects.

15. Prices and limits#

One credit is $0.0039.
ProductCreditsPer minute
Filter3 per started minute$0.0117
Filter with tracks=bothtranscription cost doubles
Honeypot8 per started minute$0.0312
Scan, transcript2 per request
Scan, audio2 per request plus 1 per started minute of audio
Number provided by CallerAPI4 more per minute on that number$0.0156
Current prices are in GET /capabilities.
Limits:
LimitValue
Live sessions per node200. Above it the socket answers 503. Retry on a new connection.
Start message timeout15 seconds after the connection.
Filter session60 minutes.
Honeypot session15 minutes.
Sample rate8000 to 48000 Hz.
Scan body25 MB.
Scan request90 seconds.
Webhook post5 seconds, no retry.

16. Errors#

HTTP endpoints:
StatusMeaning
400Bad input. The message says which field.
401Missing or wrong key.
402Insufficient credits.
404Session not found, or it belongs to another key.
502Transcription failed on /scan.
503The feature is off on this deployment, or the node is at its session limit.
Socket:
BehaviourCause
Upgrade refused with 401Missing or wrong key.
Upgrade refused with 503live_filter is false, or the node is full.
Socket closed with no eventsNo start message within 15 seconds, a binary frame before the start message, or an unsupported encoding or sample_rate.
Session ended early with session.endedCredits ran out, or the session hit its time limit.
session.errorA speech or model provider refused. The error field carries the provider's message.

17. Production checklist#

1.
GET /capabilities returns "live_filter": true.
2.
Your socket client sends the start message first and audio second.
3.
Only the caller's audio goes on the socket. Both parties are never mixed.
4.
from is E.164 with a leading + on every call. Without it nothing is filed.
5.
language is set when you know it.
6.
The webhook is HTTPS, answers 2xx in under 5 seconds, and verifies X-Voice-Signature.
7.
Missing webhook posts are recovered from GET /sessions/{id}.
8.
A 503 on connect is retried on a new connection.
9.
report=true is off until you have reviewed verdicts on your own traffic.
10.
Credits are monitored. A session ends when the account runs out.
Modified at 2026-09-14 00:50:43
Previous
Balance and email
Next
Subscribe to daily reports
Built with