SDK Examples
Four end-to-end TypeScript projects using @osmapi/osmtalk-sdk. Clone, fill in .env, run.
Looking to start from working code instead of a blank file? osm-API/osmtalk-examples is the official runnable-examples repo. Four self-contained TypeScript projects you can clone today.
git clone https://github.com/osm-API/osmtalk-examples.git
cd osmtalk-examples
nvm use # picks Node 20 from .nvmrcEvery example follows the same recipe:
cd <example-folder>
cp .env.example .env # fill in real values
npm install
npm run check # PREFLIGHT — verify env + auth + IDs (no money spent)
npm start # the actual thingnpm run check validates everything that could fail at runtime — env vars, API key auth, that your agent/phone-number IDs actually exist in your org, even your CSV format. If it passes, npm start should just work. That's the "idiot-proof" part.
Which example do I want?
| If you want to… | Use | Cost per run |
|---|---|---|
| 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 server | 03-webhook-receiver/ | Free |
Recommended order: 04 (simulate) → 01 (real call) → 02 (scale up) → 03 (receive results).
Example 01 — Personalized Outbound Call
Place one phone call where the agent addresses the recipient by name and references their account. dynamicVariables substitute placeholders in the agent's prompt at call time, so one agent can call 1,000 different people — each with their own context — without any prompt rewriting.
What it demonstrates
client.calls.outbound()withdynamicVariablesandassistantOverride- E.164 phone validation locally before round-tripping to the API
- Pre-flight
client.platform.getModelHealth()so you don't burn credit on a call doomed by a provider outage Idempotency-Keyfromdestination + date— re-running won't double-dial the same person todayclient.calls.waitUntilEnded()whenWAIT_FOR_RESULT=1is set- Friendly error mapping per HTTP code (401 → "regenerate key", 402 → "top up credits", etc.)
The 30-second story
import { Osmtalk } from "@osmapi/osmtalk-sdk";
const client = new Osmtalk({ apiKey: process.env.OSMTALK_API_KEY! });
const call = await client.calls.outbound(
{
agentId: "agent_FIjNhxo8…",
phoneNumberId: "pn_Qd_NZEbZ…",
destination: "+919876543210",
dynamicVariables: {
first_name: "Arjun",
company: "Acme Corp",
renewal_date: "May 28, 2026",
"Client Name": "Arjun", // spaces in keys allowed since May 2026
},
assistantOverride: {
welcomeMessage: "Hi {{first_name}}, this is Maya about your {{company}} renewal…",
},
},
{ idempotencyKey: `dest-${destination}-${new Date().toISOString().slice(0, 10)}` },
);
console.log(`Your phone rings in ~3-5 seconds. Track at app.osmtalk.com/calls/${call.callId}`);The agent's existing prompt sees the rendered substitutions — {{first_name}} becomes Arjun, etc.
Example 02 — Bulk Campaign from CSV
Scale from one call to thousands. Upload a CSV of leads (one phone number per row plus any custom columns), and OsmTalk's campaign engine handles concurrency, dialing-window enforcement, retry policy, and DNC filtering for you.
What it demonstrates
client.campaigns.create()withschedule.timezone/windowStart/windowEnd,retryPolicy.maxAttempts,maxConcurrentclient.campaigns.uploadLeadsCsv()— server-side CSV parsing and validation- Live polling via
client.campaigns.report()— see done/queued/dialing counts in real time - Auto-pause on SIGINT — Ctrl+C pauses the campaign so it doesn't keep dialing after you walk away
- E.164 validation on a sample of rows before upload to catch format issues fast
- Per-row error reporting (
csvErrors[]) — which rows failed and why
The 30-second story
const camp = await client.campaigns.create({
name: "Q2 Renewals",
agentId: "agent_xxx",
phoneNumberId: "pn_xxx",
maxConcurrent: 3, // 3 calls in parallel
schedule: {
timezone: "Asia/Kolkata",
windowStart: "10:00",
windowEnd: "19:00",
},
retryPolicy: {
maxAttempts: 3,
backoffMinutes: 60,
retryOn: ["no_answer", "busy"],
},
webhookUrl: "https://your-server.example.com/webhooks/osmtalk",
});
await client.campaigns.uploadLeadsCsv(camp.id, fs.readFileSync("./leads.csv", "utf-8"));
await client.campaigns.start(camp.id);
// Optional: poll the report every 5s and exit when done
const report = await client.campaigns.report(camp.id);
console.log(report.counts.byStatus); // { dialing, queued, completed, failed }
console.log(report.counts.byDisposition); // { qualified, not_interested, voicemail, … }The lead CSV uses any columns you like; every non-phone column becomes a {{variable}} available in the agent's prompt:
phone,first_name,company,renewal_date
+919876543210,Arjun,Acme Corp,2026-05-28
+919876543211,Meera,Wonderlabs,2026-06-02Example 03 — Verified Webhook Receiver
Express server that receives call.completed, call.failed, campaign.lead_completed, and call.analysis_completed events from OsmTalk. Verifies HMAC signatures so a leaked webhook URL can't be used to forge events.
What it demonstrates
verifyWebhookSignature(rawBody, header, secret)from the SDK- The
express.raw()trick — signatures are over exact bytes, so you can't parse JSON before verifying - Replay/dedup cache — hashes each raw body and skips duplicates within 24h. Stops retry storms when downstream is briefly slow
- Acknowledge fast (200), do slow work in background — OsmTalk retries non-2xx with exponential backoff
- Weak-secret warning if the configured secret is under 16 chars
- Graceful shutdown on SIGTERM
The 30-second story
import express from "express";
import { verifyWebhookSignature } from "@osmapi/osmtalk-sdk";
const SECRET = process.env.OSMTALK_WEBHOOK_SECRET!;
const app = express();
app.post(
"/webhooks/osmtalk",
express.raw({ type: "application/json", limit: "1mb" }), // IMPORTANT
(req, res) => {
const ok = verifyWebhookSignature(
req.body, // Buffer
req.header("x-osmtalk-signature"),
SECRET,
);
if (!ok) return res.status(401).send("invalid signature");
// From here, payload is authentic
const event = JSON.parse(req.body.toString("utf-8"));
handleEvent(event).catch(console.error); // do slow work async
res.status(200).json({ ok: true }); // ack within 2s
},
);
async function handleEvent(event: any) {
switch (event.event) {
case "call.completed":
console.log("Call ended:", event.call.id, event.analysis?.disposition);
break;
case "campaign.lead_completed":
console.log("Lead done:", event.lead.phoneNumber, event.lead.disposition);
break;
}
}Local testing: use ngrok http 3030 to get an HTTPS URL OsmTalk's cloud can reach. Paste that URL + the same OSMTALK_WEBHOOK_SECRET into app.osmtalk.com → Settings → Webhooks.
Example 04 — Simulate Before Going Live
Run scripted conversations against your agent through client.eval.simulate(). No phone rings. No credits spent. No audio. Just the LLM portion of the conversation, replayed in under 2 seconds with the full transcript printed to your terminal.
Why this exists
Most people building voice agents iterate by placing real outbound calls to themselves. That's:
- ❌ Slow (10–30s per turn)
- ❌ Costs money (~₹3–10 per attempt)
- ❌ Rings your phone constantly
Simulation runs the LLM portion of the conversation only — in 2 seconds, for free.
What it demonstrates
client.eval.simulate()— feed scripted user turns, get the agent's responses backdynamicVariablessubstitution works in simulation too — test prompt variables without spending money- The "preflight check" pattern — verify env config before running anything that could fail
- The iteration loop: edit prompt → simulate → fix → repeat
The 30-second story
const result = await client.eval.simulate("agent_xxx", [
{ role: "user", content: "Hi, who's calling?" },
{ role: "user", content: "What's your typical timeline for a small app?" },
{ role: "user", content: "Can you email me pricing?" },
{ role: "user", content: "Thanks, bye!" },
], {
dynamicVariables: { first_name: "Arjun", company: "Acme" },
});
for (const turn of result.transcript) {
console.log(`${turn.role.toUpperCase().padStart(9)} | ${turn.content}`);
}
// ₹0.00 spent · 📵 no phones rang · 1.84s wall timeWhen to use simulation vs real calls
| Phase | Use |
|---|---|
| Iterating on system prompt | Simulation — fast, free, repeatable |
| Testing edge cases / red-teaming | Simulation — try 50 weird inputs in a minute |
| Testing tool calls (HTTP/MCP) | Simulation — tools fire in simulate too |
| Testing TTS voice quality | Real call — simulation has no audio |
| Testing VAD / turn-taking | Real call — needs actual audio timing |
| Testing pronunciation issues | Real call — pronunciation dict only affects TTS |
| Final pre-launch QA | Real call |
The rule: iterate in simulation → validate in real calls.
Beyond the examples
The examples repo also ships:
- AGENT_CONFIG.md — 12-section reference for every agent setting (VAD, LLM, STT, TTS, tools, interruptions, idle detection, recording, post-call analysis, tuning recipes for common scenarios)
- CONTRIBUTING.md — how to add a 5th example
- CHANGELOG.md — what changed when
- CI — every push types each example against the live published SDK, so we catch SDK upgrades that break examples before merge
Next
- SDK reference — every method documented
- Outbound Calls — the call lifecycle in depth
- Webhooks — event types + delivery semantics
- VAD & Turn Detection — tuning audio for clean conversations