Idiot-proof, runnable examples for building voice AI agents with
OsmTalk and the
@osmapi/osmtalk-sdk.
Each example is a standalone Node + TypeScript project. Clone, fill in a
.env, run two commands. Real code, real APIs, no fake/mocked anything.
Get a real voice agent calling your phone in under a minute.
# 1. Clone + cd
git clone https://github.com/osm-API/osmtalk-examples.git
cd osmtalk-examples/01-personalized-call
# 2. Config — get OSMTALK_API_KEY from app.osmtalk.com → Settings → API Keys
cp .env.example .env
$EDITOR .env
# 3. Install + verify everything before paying for a call
npm install
npm run check # validates env + auth + IDs + provider health
# 4. Place a real call (your phone rings ~3s later)
npm startIf anything errors, npm run check tells you exactly what to fix. It's
designed so you never burn credits or ring phones because of a typo.
| If you want to… | Use | Cost |
|---|---|---|
| Iterate on your agent's prompt without spending money | 04-simulate-before-going-live/ |
Free |
| Place one outbound call from your code | 01-personalized-call/ |
~₹3-10 per call |
| Run an outbound campaign from a CSV of leads | 02-bulk-campaign/ |
~₹3-10 × leads |
Receive call.completed events into your own server |
03-webhook-receiver/ |
Free (just receives events) |
Recommended order for first-time users:
04 (simulate) → 01 (real call) → 02 (scale up) → 03 (close the loop).
If you want the deep dive into "what does this setting actually do?":
📖 AGENT_CONFIG.md — the full reference.
Covers:
- The pipeline (how a call flows through STT → LLM → TTS)
- VAD settings (when is the user talking?) — section 6
- Smart Turn (when is the user DONE talking?) — section 7
- LLM tuning (model choice, reasoning, tokens)
- STT tuning (provider, language, boosted keywords)
- TTS tuning (voice, speed, SSML)
- Interruptions, muting, idle detection
- Recording, transcripts, analysis
- Tools (HTTP, client, MCP)
- Tuning recipes for common scenarios (fastest, most natural, Indic, noisy line, elderly callers, etc.)
Examples link to specific sections of this file where relevant.
Place one phone call. The agent addresses the recipient by name and references
their account, using dynamicVariables to substitute placeholders in the
agent's prompt at call time.
await client.calls.outbound({
agentId, phoneNumberId, destination,
dynamicVariables: { first_name: "Arjun", company: "Acme" },
});Scale to thousands of leads via CSV. Demonstrates client.campaigns.* with
dialing-window enforcement, retry policy, concurrency limit, and auto-pause on
Ctrl+C.
Express server that verifies HMAC signatures using
verifyWebhookSignature(rawBody, header, secret) from the SDK. Includes
in-memory replay-dedup so duplicate retries don't double-process events.
04-simulate-before-going-live/
Run scripted conversations through client.eval.simulate(). No phone rings,
no credits charged. The fastest way to iterate on system prompts.
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ 04 Simulate │ │ 01 Personalized │ │ 02 Bulk Campaign │
│ (free iteration) │ │ Call │ │ From CSV │
│ │ │ │ │ │
│ client.eval │ ──► │ client.calls │ ──► │ client.campaigns │
│ .simulate() │ │ .outbound() │ │ .create() + start() │
│ │ │ │ │ │
│ → text only │ │ → 1 real call │ │ → many real calls │
└─────────────────────┘ └──────────┬──────────┘ └──────────┬──────────┘
(prompt iteration) │ │
└──── results back via ──────┘
webhook (Example 03)
│
▼
┌─────────────────────┐
│ 03 Webhook │
│ Receiver │
│ │
│ verifyWebhookSig │
│ ← call.completed │
│ ← call.failed │
└─────────────────────┘
- Node.js 20+ (every example enforces this via
engines.node) - An OsmTalk account → https://app.osmtalk.com (free signup)
- An API key —
Settings → API Keys(thecallsscope is enough for Examples 01/02/04; Example 03 needs only a webhook secret) - At least ₹50 of credit if you want to run Examples 01/02 (test calls are cheap — ~₹3-10 each). Example 04 simulation is free.
Every example has the same 3-step recipe:
cd osmtalk-examples/<example-folder>
cp .env.example .env # then edit it with real values
npm install
npm run check # PREFLIGHT — verify before running anything real
npm start # the actual thingThe npm run check preflight is the killer feature. It catches:
- Missing/placeholder env vars
- Wrong-shape values (e.g. agent ID not starting with
agent_) - API key auth failures
- Non-existent agent / phone number IDs (uses SDK to verify they exist in your org)
- Down/degraded provider warnings
- (Example 02) CSV format issues + phone-number E.164 validation
- (Example 03) Weak webhook secret warnings
So npm start only fails for actual runtime issues, not config typos.
Q: I get HTTP 401 "Invalid or revoked API key" — what now?
A: Regenerate at Settings → API Keys, tick the calls scope, paste the new
value into .env. Older keys may have been issued before the API accepted
osm_ tokens on main routes — anything you generate today works.
Q: My agent's prompt has {{Client Name}} (with a space) and the SDK errors with Invalid dynamicVariables —
A: Old API rejected spaces in keys. Updated May 2026 — keys accept letters,
digits, underscores, hyphens, dots, and single inline spaces. If you still see
this error, your API server is on an older commit; tell ops to redeploy.
Q: How do I find my agent ID / phone number ID without clicking around the dashboard? A: Use the SDK:
console.log(await client.agents.list()); // agent IDs + names
console.log(await client.phoneNumbers.list()); // number IDs + E.164There's a copy-paste version in Example 01's README under "Discover IDs via the SDK".
Q: My agent uses gpt-5.4-nano. Will it work?
A: Yes — the bot auto-routes GPT-5 / o-series models to OpenAI's /v1/responses
API since chat/completions rejects them with function tools. You don't have to
configure anything. See AGENT_CONFIG.md section 3.
Q: My phone rings but the agent never speaks — what happened?
A: Almost always a TTS provider outage. Run client.platform.getModelHealth()
— if ElevenLabs or Deepgram is degraded/down, swap the agent's TTS provider
in the dashboard temporarily. The preflight in npm run check warns about
this before you place the call.
Q: I want the lowest possible latency. What should I configure? A: Groq Llama-3.3-70b + Deepgram nova-3 + Deepgram aura-2 TTS. ~700ms TTFB total. See AGENT_CONFIG.md → Tuning recipes.
Q: How do I tune VAD so the bot stops interrupting users mid-sentence?
A: Raise vadStopSecs to 0.4-0.6. Full VAD guide:
AGENT_CONFIG.md → VAD settings.
Q: Can I test without spending money?
A: Yes — Example 04. client.eval.simulate() runs the LLM portion of the
conversation only. No audio, no STT/TTS, no phone, ₹0 spent.
Q: My webhook signature check always fails.
A: Almost always: you're using express.json() instead of express.raw() on
the webhook route. The signature is over the raw bytes; re-serialized JSON
won't match. Example 03 has this set up correctly — copy the pattern.
Q: Can I publish these examples as my own packages?
A: They're licensed MIT, so yes. But they're marked "private": true in
package.json to prevent accidental npm publish — flip that off if you
actually want to publish.
Q: How do I add a "press 1 for sales, 2 for support" menu?
A: Configure DTMF routes in the dashboard under your agent → Advanced →
Keypad. The bot routes the digit to a sub-agent or human phone number without
involving the LLM (no extra latency). Set enableDtmf: true in advanced
settings.
Current pin: @osmapi/osmtalk-sdk@^0.5.0
dynamicVariables— per-call prompt personalization (Example 01)assistantOverride— per-call agent setting overrides (Example 01)Idempotency-Key— safe re-runs without double-dialing (Example 01)- Auto-retry on 5xx/429 — built in since 0.3.0, with
Retry-Afterhonored client.calls.waitUntilEnded()— one-line poll-to-completion (Example 01)client.calls.list({ filters, limit })— paginated envelope (0.4.0); now also supportscampaignId+failureReasonfilters (0.5.0) for triageclient.phoneNumbers.list()— discover org-owned numbers (0.4.0)client.eval.simulate()— test prompts without spending money (Example 04)client.campaigns.*lifecycle — start/pause/resume/stop (Example 02)describeFailureReason(reason)— human-readable failure explainer withtitle/cause/likelyBlame/whatToTry/retryable(Examples 02 + 03)isRetryableFailure(reason)— closed-set check for the kinds of failures a campaign worker should retry automatically (Examples 02 + 03)callConnected(record)— heuristic for "was this a real conversation" vs phantom call. Server-side equivalent isCallRecord.didConnect.verifyWebhookSignature(rawBody, header, secret)— HMAC verification (Example 03)AbortSignal— cancellation on every methodOsmtalkError.isRetryable / .isClientError— typed error branching
A call that ends without a real conversation gets status: "failed" plus a
machine-readable failureReason — one of:
| Reason | What it means | Likely blame | Retryable? |
|---|---|---|---|
no_audio_output |
Bot couldn't speak (TTS connection failed) | Platform | ✅ |
no_audio_either_direction |
Nobody produced audio | Environment | ✅ |
idle_timeout |
Pipeline hit the configured idle window | Caller | ✅ |
provider_circuit_open |
Bot tripped its 3-errors-in-60s breaker | Platform | ✅ |
sip_no_answer |
Carrier reported no answer / busy | Carrier | ✅ |
sip_rejected |
Carrier rejected the call | Carrier | ❌ |
bot_startup_failed |
Bot didn't ready within startup window | Platform | ✅ |
caller_hung_up_silently |
Caller answered + hung up immediately | Caller | ❌ |
stale_sweep |
Bot died mid-call, sweeper cleaned up | Platform | ✅ |
unknown |
Could not classify | Unknown | ❌ |
describeFailureReason() returns the same {title, cause, whatToTry} strings
the dashboard banner uses and the
docs site lists. Use it
in your own logs / Slack alerts so testers see consistent wording everywhere.
Every push and PR runs tsc --noEmit on each example against the live
published SDK. Catches the next class of bugs where an SDK upgrade silently
breaks an example.
- AGENT_CONFIG.md — every knob explained
- CONTRIBUTING.md — how to add a 5th example or fix a bug
- CHANGELOG.md — what changed when
- Open an issue for anything unclear
MIT — copy, modify, ship.