Product · the full tour
What Earshot does, from the dial tone to the build log.
Synthetic test calls, automated results. A real outbound call, real DTMF, local transcription, per-step evidence, a test log the engineer signs off, and a scenario that rewrites itself from the recording. Built and proven on NICE CXone; works against Avaya, Genesys Cloud, Amazon Connect, Cisco, Five9 and anything else that answers a call. Everything on this page runs on your Mac or PC, against your trunk, with nothing leaving the machine.
Synthetic testing, end to end.
Automate your testing and results. Earshot places synthetic calls on a schedule or on demand, walks the tree, grades every step, and files the results — the log, the verdict, the audio and the alert, with nobody on the phone.
- 01 · schedule
Nightly, every N minutes, or now
A tag, a time and a trunk. Nightly regression suites, a production monitor every fifteen minutes, or a run kicked off from the console, the CLI or the API.
- 02 · matrix
One scenario, every number
Import the DNIS sheet and run one scenario across every number and caller ID, inside and outside hours. A cutover surface of 1,800 numbers is a Saturday morning.
- 03 · ci
Fails a build
JUnit and JSON out, exit codes in. GitHub Actions, Jenkins and Azure DevOps treat a broken IVR like a broken unit test.
- 04 · alerts
A message, not a Monday complaint
Slack, Teams, a webhook, PagerDuty or email when a step fails or timing drifts. The alert carries the step, the transcript and a link to the slice.
- 05 · evidence
Filed, graded, reviewable
Every run lands in the test log with an auto verdict per scenario and per step, ready for the engineer's review and for the CSV that goes to the ticket.
Schedules
example consoleearshot run or POST /api/runPASSlast 22:10 · $142.17 read backRun now →Every scheduled run honours the same confirm gate, duration cap and spend cap as a run you start by hand. Serial by default; parallel channels are an Enterprise switch.
The results land in the test log; the schedule itself is a CLI command and an endpoint → CLI and CI
Write a rough test. Earshot listens to its own call and fixes the test.
This is exactly what an engineer does by hand — run it, listen back, move the press, rerun. Earshot does the listening and the moving.
A first-draft scenario is almost never right. The presses land during a prompt instead of after it. A wait_for phrase fuzzy-matches the wrong sentence. A fixed wait: 60 was a guess. You used to find all of that out by calling the number, listening to the recording, and editing.
Earshot already has the recording. After a run, it turns the whole call into an interaction timeline: every utterance with start and end in milliseconds, every DTMF it sent, every stretch of silence, every stretch of hold music. It aligns each scenario step to what actually happened, and rewrites the scenario from the evidence.
And: fixed waits become gates wherever the transcript shows a prompt worth gating on. Then it reruns. Up to N iterations, until the scenario is stable — two consecutive identical passes.
Suggest
Earshot proposes each change as a diff with the audio that justified it. You approve, edit or reject each one.Auto
Earshot applies the changes and reruns within your iteration budget. You review the final diff against the original.- 01
A press that landed early gets moved.
If DTMF went out while a different prompt was still playing, the press moves to after that prompt's end, plus a margin.
- 02
A phrase that matched the wrong prompt gets replaced.
The new phrase is text that occurred verbatim in the transcript at the right point in the call.
- 03
A phrase that was never heard gets dropped.
If it is not in any transcript for this case, it is not an expectation, it is a wish.
- 04
Timeouts are set from observed latency.
The gap that actually occurred, plus margin — not a number somebody typed once.
- 05
stop_onphrases are suggested from failure branches actually heard.A closed-office message heard in a previous run becomes a fail-fast guard, so the next run ends in seconds instead of burning the duration cap.
One run, five corrections, one pass
example runThe scenario was written from the call-flow document. It dials the simulator, keys the ANI and DNIS, waits ten seconds, and starts pressing.
- t+00:20.000press "2"observeddtmf_sent while utterance t+10.460 → t+24.800 was still playing. Digit dropped; menu replayed at t+25.100.
- t+01:57.200expect_promptexpectedyour call may be monitored or recorded for qualityobservedthis call may be monitored and recorded for quality assurancescore0.62 · threshold 0.75 · FAIL
- after wait 60expect_promptexpectedwe can call you back | press one to continue holdingobservedno speech in the window. The callback offer arrived at t+222.400, 100.5 s after the disclaimer ended.score— · FAIL
Earshot lays the run out on the call clock. Four things are visible immediately that no ticket would ever record:
- DTMF
2went out at t+20.000, while the greeting menu was still playing. The menu utterance did not end until t+24.800. The digit was dropped and the menu replayed from t+25.100. - The QA disclaimer was heard at t+117.200 — but as "this call may be monitored and recorded for quality assurance". The scenario expected "your call may be monitored or recorded for quality". Match score 0.62 against the 0.75 threshold. FAIL.
- The callback offer did not arrive 60 seconds later. It arrived at t+222.400 — 100.5 s after the disclaimer ended.
- An earlier run of the same case,
20260916-201144-9b31, transcribed "our office is closed" at t+12.400 and then sat on the line until the 600 s cap. Nothing in the scenario told it to stop.
Five changes, each with its evidence. Click a change to see what justified it.
QA disclaimer at 117 s. Hold music 130.5 s → 220.6 s, 90.1 s proven. Callback offer at 222 s. First delay message at 235 s. Music 256.9 s → 376.9 s, 120.0 s proven. Second delay message at 377 s. Released at 412.8 s.
Nobody sat on the phone with a stopwatch at 1 AM.
The API behind it → POST /api/adapt, and earshot adapt <run_id> on the command line.
A scenario, a real call, and a recording that grades itself.
Earshot is a local application. You write a scenario in YAML — what to dial, what to press, what you expect to hear. Earshot registers a SIP user agent against your trunk, places the call, sends DTMF as RFC2833 tones, and records the far-end audio to a WAV file.
When the call ends, the recording is segmented by voice-activity detection, each step's audio window is cut out and transcribed locally with whisper.cpp, and every expectation is scored against the transcript by fuzzy match. The result is a per-step PASS / FAIL / INFO / ERROR / SKIPPED ladder on a call clock, with the audio slice for each step attached to its row.
The limits, stated
Grading is post-call: Earshot records the whole call, then scores it. wait_for gates live on what it is hearing, but a scenario cannot choose a different branch mid-call — you script the digits in advance. Outbound only. Voice only. English transcription (base.en). Timing windows are padded ±1.5 s to absorb clock drift, so this proves "it said the right thing at roughly the right time", not "within 200 ms". Calls are serial unless you turn on parallel channels, and even then it is coverage, not load.
- 01
Dial
A SIP user agent registers against your trunk and places one outbound call. Serial by default, so a test suite cannot fan out into a production queue by accident.
max_duration_sec(default 240 s) is enforced by the runner and handed to the engine, so a stuck call cannot run up a bill. - 02
Drive
presssends DTMF digits exactly as a caller would,,inserting a 500 ms gap.wait_forblocks the script until the live audio actually contains one of your phrases, so the next press lands on the prompt rather than on a clock — which matters, because IVR prompt timing drifts with back-end latency. - 03
Hear
Voice-activity detection cuts each step's window out of
call.wav. whisper.cpp withggml-base.en.bintranscribes each slice on your machine. Nothing is uploaded. - 04
Assert
expect_promptfuzzy-matches text fragments.expect_valueexpands a typed value into every plausible spoken rendering — 142.17 becomes "one hundred forty two dollars and seventeen cents", "a hundred forty two seventeen", "142 dollars and 17 cents" — and matches any of them.expect_silenceandexpect_hangupassert the absence of speech and the far end's release.
A test is a YAML file with ten verbs in it.
One file per scenario. The loader is strict — an unknown key or an unknown verb is an error that names the file and the line, because a silently skipped test is worse than a loud failure.
Scenarios live in scenarios/*.yaml and are hand-editable, but you do not have to hand-edit them. The console has a builder: seven templates, one step card per verb, a DTMF keypad for press digits, and live validation through the same loader that runs at call time. You can also import a CSV or XLSX test-case sheet, or have your own AI draft the file — define your own scenarios.
Authenticate, then assert the balance the IVR reads back — as a number, not as a sentence.
name: billing_balance_happy
description: Account-number auth, then spoken balance readback
dial: "15135660137"
tags: [billing, happy]
steps:
- : 6
- : "for billing press 1"
- : "1"
- : 4
- : "enter your 10 digit account number"
- : "5135551234#"
- : 6
- :
type: currency
value: 142.17
context: "your total balance"
- : { within: 20 }A queue-treatment test through a carrier call simulator. Every press is gated on a phrase, not a clock, so back-end latency cannot slide the test off the rails.
name: bsc_billing_queue
description: >-
Queue treatment for BSC Billing: QA disclaimer, 90 s of hold music,
callback offer, 1st delay message, 120 s, 2nd delay message.
dial: "15133064718"
max_duration_sec: 600
tags: [live, queue, treatment]
steps:
- : 3
- : "3216352882" # ANI the simulator asks for
- : 7
- : "8086433456" # DNIS under test
- :
texts: ["new customer", "pending order", "press 1"]
timeout_sec: 45
min_score: 0.5
- : "2"
- :
texts: ["for technical support press 1", "order new service"]
timeout_sec: 130
min_score: 0.8
lookback_sec: 2
- : 6
- : "2"
- : 6
- : "1"
- :
texts: ["monitored and recorded", "survey"]
stop_on: ["office is closed", "no one is available"]
timeout_sec: 60
min_score: 0.5
- :
texts: ["hold your place in line", "call you back"]
stop_on: ["office is closed", "no one is available"]
timeout_sec: 130
min_score: 0.5
- :
texts: ["appreciate your patience", "every attempt to answer"]
stop_on: ["office is closed"]
timeout_sec: 230
min_score: 0.6
- : {}Say nothing. Prove the menu re-prompts and then bails out, instead of hanging on the line forever.
name: billing_menu_noinput
description: No DTMF at the main menu; expect reprompt then transfer
dial: "15135660137"
tags: [billing, unhappy, noinput]
steps:
- : 6
- : "for billing press 1"
- : 12 # sit past the menu's input timeout
- :
texts:
- "i'm sorry i didn't get that"
- "sorry, i didn't hear a selection"
window_sec: 12
- : 12
- : "please hold while i transfer you"
- : {} # don't tie up a real agentThree rules
Wait before you expect. An expect_* looks backwards over window_sec of audio from the moment it runs. If the prompt has not played yet, the window is silence.
Assert short, distinctive fragments. Fragments survive transcription errors. Whole sentences fail on one misheard word.
Use expect_value for anything spoken as data. Balances, dates, digit strings, counts. You assert the number; the matcher handles the wording.
Every verb, every field, every default → the DSL reference
Define your own scenarios. Four ways in, one loader out.
Your phone tree, your test cases. Build them in the console, write the YAML by hand, import the test-case sheet you already have, or describe the test in plain English and let the AI draft it. Every route runs through the same strict loader, so a scenario is valid before it is ever dialled.
Step cards and a keypad
Seven templates — happy path, no input, invalid entry, out of retries, caller abandons, barge-in, and blank. One card per verb, a DTMF keypad for press, a preview of the spoken forms expect_value will accept, live validation, and Save & Run.
Hand-edit the file
Every scenario is a readable YAML file in scenarios/. Edit it in your editor, keep it in git, review it in a pull request. The console and the file never disagree, because the file is the scenario.
From the sheet you already have
A CSV or XLSX test-case sheet — one row per scenario or one row per step — becomes a set of scenarios. Errors name the row. The UAT sheet that took a week to write is a suite in a minute.
Describe it, review it, run it
"Authenticate with account 5135551234 and check the balance reads $142.17." Your own Claude or GPT key drafts the scenario; you review, save and run. Bring your own AI →
The builder
example · keypad is live, the rest is a picturetemplate
- happy path
- no input
- invalid entry
- out of retries
- caller abandons
- barge-in
- blank
- 1wait6 stiming
- 2expect_prompt"for billing press 1" · window 8 s · min 0.75assert
- 3press"1" · from the keypaddtmf
- 4expect_prompt"enter your 10 digit account number"assert
- 5press"5135551234#"dtmf
- 6expect_valuecurrency 142.17 · context "your total balance"assert acceptsone hundred forty two dollars and seventeen centsa hundred forty two seventeen142 dollars and 17 centsone forty two point one seven
- 7expect_hangupwithin 20 sassert
dtmf keypad → step 3
Tap keys to build the press digits. Clear it and the validator objects.
import a sheet
CSV or XLSX, one scenario per row or one step per row:
Every verb, every field, every default → the DSL reference · the sheet format → scenario import
Bring your own AI. Works with the latest models.
Plug in your Claude or GPT API key and let Earshot test for you. It drafts scenarios from a sentence, plans a whole test set for a goal, explains a failed run and writes the log note — and every proposal is a diff you review before anything dials.
- 01
Draft a scenario from a plain-English description.
"Call the billing line, authenticate with 5135551234, expect the balance to read $142.17." Out comes a scenario with the right verbs,
expect_valuewhere a number is spoken, and ahangupbefore an agent picks up. - 02
Plan a whole test set for a goal.
Happy and unhappy paths — no input, invalid entry, out of retries, caller abandons. You review the set, save what you want and run it. It is a plan you approve, not a suite that appears.
- 03
Explain a failed run and draft the log notes.
Given the transcript, the step ladder and the timings, it says what most likely happened — a scenario error or an IVR error — and drafts the note in the test log for you to edit.
- 04
Review adaptive proposals.
Before you accept the five changes an adaptive run proposes, the assistant reads each diff against its evidence and flags the ones that would hide a real regression.
Your key, your machine, your provider
The key lives in a local file, ~/.config/ivr-tester/ai.env, never on Earshot's servers — there are none in the loop. Transcripts and step results go only to the provider you configured, only when you use an AI feature. Audio never leaves the machine.
Models: Claude (Sonnet 5 by default, Opus 5, Fable 5.1) and OpenAI GPT, configurable. Switch with one line in ai.env.
AI_PROVIDER=anthropic
ANTHROPIC_API_KEY=sk-ant-…
AI_MODEL=claude-sonnet-5
# or: AI_PROVIDER=openai, OPENAI_API_KEY=sk-…, AI_MODEL=<gpt model id>Plan with AI
example · nothing is sent from this pagebilling_balance_happy
dial: "15135660137"
steps:
- wait: 6
- expect_prompt: "for billing press 1"
- press: "1"
- press: "5135551234#"
- expect_value:
type: currency
value: 142.17
- hangup: {}billing_menu_noinput
dial: "15135660137"
steps:
- wait: 6
- expect_prompt: "for billing press 1"
- wait: 12
- expect_prompt: "didn't get that"
- wait: 12
- expect_prompt: "transfer you"
- hangup: {}billing_account_invalid
dial: "15135660137"
steps:
- wait: 6
- press: "1"
- press: "0000000000#"
- expect_prompt:
texts: ["not recognized",
"try again"]
- press: "0000000000#"
- expect_prompt: "transfer you"
- hangup: {}The endpoints and the config file → AI assistant reference
Point it at a number with no scenario at all.
Discover mode calls a number cold. It listens, transcribes the menu, and presses each option in turn — breadth-first, bounded by a depth limit and a call budget you set. What comes back is the tree: a set of generated scenarios, one per path, and a map of how the paths connect.
Two jobs it is built for:
Document the legacy platform before you migrate it. An Avaya or AAEP tree that grew over fifteen years is usually documented by a Visio nobody has updated and by whoever has been there longest. Discover mode produces what the tree actually does today, from the outside, from the customer's side of the line.
Verify the new platform against the old one. Run discover against both numbers and diff the maps. Prompts that changed wording, options that moved, branches that disappeared.
Bounded by design
Every discover run has a depth limit, a call budget and the same max_duration_sec cap as any scenario, and it never presses a digit past a prompt that sounds like an agent or a transfer. It maps menus, not people.
The output is ordinary scenarios, so the next step is the same as always: mark a run golden, and the tree becomes a regression suite.
Evidence, not opinions. Every assertion ships with the audio that proves it.
Every run writes an evidence record. The console reads it, the exported HTML report reads the same, and the artifact you attach to a change ticket looks like the screen you approved it on.
This is the file you attach to the ticket. It opens in any browser with no dependencies and prints one scenario per page.
The Evidence Library
The root page is one row per scenario: its latest verdict, a miniature of the call path it walked, and its recent pass/fail history. "Which flows are proven, which are broken, and did the fix take?" is answered without opening anything.
The call-path journey strip
One node per meaningful step — what was heard, what was pressed, what value was checked — colour-coded by outcome, with waits folded into the connectors. Click a node to jump to that step's row. Same picture in the console and in the exported report.
The step ladder
Call-clock start and end as t+mm:ss.mmm, the verb, a PASS/FAIL chip, expected against observed transcript side by side, the match score to two decimals, and a play button for that step's audio slice. When a step fails you listen to exactly that slice, not to four minutes of recording.
What failed, first
Each scenario's report opens with the verdict banner, the journey strip, the call metadata — dialed number, ANI, keys pressed, duration, who released the call — and then a What failed block listing every failed check with its expected value and the transcript that was actually heard.
Self-contained evidence report
Opens in any browser with no dependencies, prints one scenario per page. With HTML+audio, every slice under 2 MB is inlined as a data URI and plays in the page. A per-bug evidence zip bundles the slices, the transcript and the report for the people who cannot open your console.
One flat row per step
run_id, scenario, verdict, step_index, verb, status, t_start_ms, t_end_ms, expected, observed, score, audio_segment, detail. Raw milliseconds, scores to three decimals.
Pivoting across many runs. Diffing yesterday's regression suite against today's.
The complete RunResult
Everything the app knows, on the local API and in the export.
Scripting, archival, feeding your own dashboard.
Compare: any two runs of the same case open side by side, aligned step for step. Live bar: while a call is in flight a sticky bar shows the state, the elapsed clock, the step ladder advancing, and STOP — on every route, so you are never more than one click from ending a live call.
Every run is logged. The machine grades it; the engineer gets the last word.
Each synthetic call files itself in the test log with an auto PASS or FAIL per scenario and per step, plus notes. An engineer can keep the auto verdict or override it — PASS or FAIL — and edit the notes. Both verdicts travel together into the report and the CSV, so nobody has to wonder which one the ticket meant.
| date | number dialed | scenario | auto | reviewed | steps | notes |
|---|---|---|---|---|---|---|
| 2026-09-17 01:18 | 15133064718 | bsc_billing_queue | PASS | PASSauto | 11/11 | 90.1 s hold and 120.0 s cadence proven from the recording. |
| 2026-09-17 01:25 | 15133064718 | bsc_repair_queue | FAIL | FAILreviewed | 9/11 | Callback offer never played on the repair skill. IVR bug, not a scenario bug. Filed as 118. |
| 2026-09-17 01:32 | 15135660169 | ucc_sales_queue | PASS | PASSauto | 10/10 | |
| 2026-09-17 01:39 | 15135660169 | ucc_billing_queue | PASS | PASSauto | 11/11 | |
| 2026-09-16 23:39 | 15133064718 | bsc_billing_queue | FAIL | PASSreviewed | 3/7 | Scenario error, not IVR: press 2 landed while the menu was still playing. Adapted; the 01:18 rerun passed. |
| 2026-09-16 22:10 | 15135660137 | billing_balance_happy | PASS | PASSauto | 6/6 | $142.17 read back as 'one hundred forty two dollars and seventeen cents'. |
| 2026-09-16 22:16 | 15135660137 | billing_menu_noinput | PASS | PASSauto | 4/4 | |
| 2026-09-16 22:21 | 15135660137 | billing_account_invalid | FAIL | FAILauto | 3/4 | Reprompt heard once; the scenario expected two attempts before transfer. Needs review. |
reviewbsc_repair_queue
verdictPer scenario and per step
The runner grades each step from the recording and rolls the ladder up into a scenario verdict. The auto verdict is never edited; it is the machine's opinion, kept on the record.
Keep auto, PASS or FAIL
An engineer who listened to the slice can overrule — a scenario error that is not an IVR error, or a pass that hid a wrong prompt. The override carries who and when, and the notes travel with it.
Both columns, everywhere
The HTML report shows auto beside reviewed. The CSV has auto_verdict, reviewed_verdict and effective_verdict columns, so a pivot over a month of runs is honest about which verdicts a human touched.
The endpoints → GET /api/testlog, GET /api/testlog.csv and PATCH /api/runs/<id>/review
Mark a run golden. Everything after it is a diff.
A passing run becomes the baseline. Later runs diff against it on wording, order, spoken values and timing — and timing drift is a first-class failure, not a footnote.
Regression diffs
Prompt wording that changed, an option that moved, a value that reads back differently, hold music that went from 60 s to 90 s. Each diff points at the step, the two transcripts and the two audio slices.
Timing as an assertion
expect_gap for hold-music duration, delay-message cadence, maximum silence and prompt latency after DTMF. The 120 s cadence in the case study is one line, not a stopwatch.
One scenario, 1,800 numbers
Import the migration DNIS sheet as CSV, run one scenario across every number and caller ID, and read the result as a heat map. Hours-of-operation profiles schedule runs inside and outside hours and assert open against closed treatment.
A phone-tree suite that fails a build.
Everything the console does is an endpoint, and everything the endpoints do has a command. Point a nightly job at a tag, get JUnit XML back, and let your pipeline treat a broken IVR the way it treats a broken unit test.
$ earshot run --tag queue --junit out/queue.xml --html out/queue.html earshot 1.0 · engine pjsua · LIVE · trunk seattle.voip.ms 4 scenarios matched tag "queue" [1/4] bsc_billing_queue PASS 412.8s 11/11 checks [2/4] bsc_repair_queue FAIL 398.1s 9/11 checks [3/4] ucc_sales_queue PASS 377.4s 10/10 checks [4/4] ucc_billing_queue PASS 395.2s 11/11 checks run 20260917-011804-7c3a · 1584.5s · 41/43 checks · 1 failed junit → out/queue.xml html → out/queue.html audio → ~/Library/Application Support/IVRTester/recordings/20260917-011804-7c3a/ $ echo $? 1 $
example run · Exit codes: 0 every scenario passed · 1 at least one FAIL · 2 a call or configuration error.
JUnit output — a failure carries the transcript and the path to the audio, so the build log is enough to triage without opening the report:
<testsuites name="earshot" tests="4" failures="1" errors="0" time="1584.5">
<testsuite name="queue" tests="4" failures="1" errors="0" time="1584.5"
timestamp="2026-09-17T01:18:04">
<testcase classname="earshot.queue" name="bsc_billing_queue" time="412.8"/>
<testcase classname="earshot.queue" name="bsc_repair_queue" time="398.1">
<failure type="wait_for"
message="step 15 wait_for: no phrase matched within 130.0s (best 0.41)">
expected : "hold your place in line" | "call you back"
observed : "we appreciate your patience every attempt is being made to answer your call"
window : t+00:03:42.180 → t+00:05:52.180
audio : recordings/20260917-011804-7c3a/bsc_repair_queue/seg_015.wav
</failure>
</testcase>
<testcase classname="earshot.queue" name="ucc_sales_queue" time="377.4"/>
<testcase classname="earshot.queue" name="ucc_billing_queue" time="395.2"/>
</testsuite>
</testsuites>Deliberate, once
Works from GitHub Actions, Jenkins and Azure DevOps. Because the calls cost money, the runner honours a per-run spend cap and the same confirm gate the console uses — a pipeline sets it once, deliberately.
Everything the CLI does is also an endpoint on the local JSON API: the API reference.
Scheduled monitoring
Run a production IVR every N minutes and alert on a failure or on timing drift — Slack, Teams, a webhook, PagerDuty or email. A prompt that goes missing at 2 AM is a message, not a Monday complaint.
One download. No Python, no Homebrew, no cloud.
The same engine on macOS and Windows, as a native installer. Built and proven on NICE CXone. whisper.cpp and the transcription model are bundled. Add a SIP trunk and it dials; add nothing and it runs in SIM mode.
macOS
Download for macOSRequirements
- macOS 13 Ventura or later
- 4 GB free disk for the app, the bundled whisper.cpp model and recordings
- A SIP trunk account for live calls (none needed for SIM mode)
Install
- Open the .dmg and drag Earshot to Applications.
- First launch opens the console in your browser at 127.0.0.1:8474.
- Settings shows the mode (SIM until you add a trunk) and a readiness check.
Where things live
- Scenarios, runs and recordings: ~/Library/Application Support/IVRTester/
- Logs: ~/Library/Logs/Earshot/
- Trunk, platform and AI credentials: the macOS Keychain, or 600-permission files under ~/.config/ivr-tester/
Uninstall: drag Earshot out of Applications; remove the folders above if you want the evidence gone too.
Windows
Download for WindowsRequirements
- Windows 10 (22H2) or Windows 11, 64-bit
- 4 GB free disk for the app, the bundled whisper.cpp model and recordings
- A SIP trunk account for live calls (none needed for SIM mode)
Install
- Run Earshot-Setup.exe. It installs per user; no administrator rights needed.
- First launch opens the console in your browser at 127.0.0.1:8474.
- Allow Earshot on the private network in the Windows Defender Firewall prompt so the SIP engine can register with your trunk.
Where things live
- Scenarios, runs and recordings: %LOCALAPPDATA%\IVRTester\
- Logs: %LOCALAPPDATA%\Earshot\Logs\
- Trunk, platform and AI credentials: Windows Credential Manager, or files under %APPDATA%\ivr-tester\
Uninstall: Settings › Apps › Earshot, or the uninstaller in the Start menu; remove the folders above if you want the evidence gone too.
Licence: the download is free and runs in SIM mode without a key. A subscription gives you a licence key for live calls — start a 14-day free trial. Trunk: voip.ms, Twilio, Telnyx, Bandwidth or any SIP provider; see SIP trunk setup. Engine: pjsua bundled on both platforms; baresip supported.
Your trunk. Your machine. Your recordings.
Earshot is a desktop application, not a service. The console is a local web UI bound to 127.0.0.1:8474 — loopback only, not configurable to anything else, so it cannot be exposed off the machine by an environment variable.
Runs, recordings, transcripts and exports are stored in the per-user application folder. Nothing is written inside the application folder itself, so the tool is safe to keep in a synced or version-controlled directory. There is no cloud component and no telemetry.
The trunk is yours. Earshot registers as a plain SIP user agent with credentials you supply and never routes your audio through anyone else's infrastructure. Team mode's shared evidence server is optional and runs where you put it, including air-gapped.
Credential handling
- Trunk and platform API credentials live in the macOS Keychain or Windows Credential Manager. If you choose a credentials file instead, it lives outside the application folder as
chmod 600, and the console warns if it is group- or other-readable. - The SIP password is never logged, never written to the database, never included in an export, and never returned by the web API. The API hands out a redacted view only.
| Rail | What it does |
|---|---|
| Confirm before dial | A live run shows the destination, the ANI and the max duration, and will not dial until you confirm. Pipelines confirm once, in config. |
| Hard duration kill | max_duration_sec, default 240 s, enforced by the runner and by the engine. |
| Serial by default | One call at a time unless parallel channels are switched on, and then per-trunk rate limits and spend caps apply. |
| Stop button | On every route. The engine releases the call. |
| SIM mode | No credentials, no telephony, no cost — and the identical pipeline. |
Questions engineers ask
Does Earshot need access to my contact-center platform?
No. It needs a phone number and a SIP trunk. That is the whole prerequisite, and it is why the same scenario works against CXone, Avaya, Genesys, Amazon Connect or an Asterisk box. Platform API integrations add context to the evidence; they are never required to run a test.
Is it really placing a call, or simulating one?
Really placing one. A SIP user agent registers against your trunk and dials the PSTN, sends RFC2833 DTMF, and records the far-end RTP. There is also a SIM mode that runs the identical pipeline — record, VAD, transcribe, match, report — against a local fake IVR with no credentials and no cost, for demos and for proving the plumbing.
What does a run cost?
Whatever your trunk charges for the minutes. US routes are typically around a cent a minute — check the rate for the numbers you actually dial, because toll-free and international differ. With the default 4-minute cap a test call costs pennies, but a nightly suite of fifty scenarios is still real money, so set the spend cap.
Will it dial production by accident?
The rails are deliberate. Live mode shows a confirm modal with the destination, the ANI and the max duration before it dials. max_duration_sec is enforced twice, by the runner and by the engine. Calls are serial by default. A stop button is on every route. And with no credentials present the app is in SIM mode and cannot dial at all.
Can a scenario branch on what it hears?
Partly, and it is worth being precise. wait_for blocks the script until the live audio contains one of your phrases, and stop_on aborts the run the moment a failure phrase is heard — so timing is driven by what is actually playing, not by a clock. But grading is post-call. A scenario cannot choose a different menu branch mid-call. You script the digits in advance, and if the flow takes an unexpected turn the run fails, which is the correct outcome. Discover mode is the exception: it exists to explore, and it explores breadth-first within a budget.
How accurate is the transcription on 8 kHz phone audio?
Good enough to assert fragments, not good enough to assert sentences — which is why the guidance is to assert short distinctive fragments and to use expect_value for anything spoken as data. The model is ggml-base.en, chosen deliberately: a larger model is slower per step for little gain on this material, and tiny starts mangling digits, which is the one thing you cannot afford here.
Does my audio leave the machine?
No. Transcription is local. Storage is local. There is no cloud component and no telemetry. The console is bound to loopback only and refuses non-loopback requests. The one exception is yours to switch on: if you put a Claude or OpenAI key in ai.env, the text of a transcript goes to that provider when you use an AI feature — the audio still never does, and nothing goes to Earshot.
Do I need an AI key to use Earshot?
No. Every AI feature is optional and off until ai.env exists. Scenarios can be built in the console, written by hand or imported from a sheet, and the grading, adapting and reporting never call a model. With a key present, the assistant drafts and explains; it never dials and never changes a verdict.
What if the transcript is right but the step still fails?
Read the observed transcript column, not the audio — that is what the matcher saw. It is usually one of four things: the expectation string is too long, the window is too early, the transcriber misheard a number where expect_value would have caught it, or the threshold is too strict for a noisy line. Run adapt in suggest mode before you touch a threshold; lowering the threshold hides real regressions.
Does it do load testing?
No. Parallel channels exist for coverage — running a scenario against many numbers faster — not for load generation. Per-trunk rate limits and spend caps are on by default when parallelism is.
Which languages?
English transcription, on every platform. Spanish and other prompt languages are on the horizon below.
Where it goes next.
Five things we intend to add. No dates, and nothing here is on a price list.
- 01Multi-language transcription — Spanish and other prompt languages, and language-offer testing.
- 02Prompt inventory audit — every prompt heard across runs catalogued; missing wavs, wrong-brand prompts and bare TTS fallbacks flagged.
- 03Call-flow map export — the discovered tree as draw.io, Visio or SVG, and a diff of two maps.
- 04Voice-quality metrics per call — MOS estimate, silence ratio, clipping, DTMF detection confidence.
- 05Mid-call branching — a scenario that picks its next press from what it just heard, inside a call budget.
Download it, point it at a number, read the evidence.
Free download, SIM mode with no trunk, 14-day free trial for live calls. Or book a walkthrough and we will run it against your own IVR with you.