osmTalk Docs
SDKs

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 .nvmrc

Every 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 thing

npm 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…UseCost per run
Iterate on your agent's prompt without spending money04-simulate-before-going-live/Free
Place one outbound call from your code01-personalized-call/~₹3-10 per call
Run an outbound campaign from a CSV of leads02-bulk-campaign/~₹3-10 × leads
Receive call.completed events into your server03-webhook-receiver/Free

Recommended order: 04 (simulate) → 01 (real call) → 02 (scale up) → 03 (receive results).


Example 01 — Personalized Outbound Call

📂 01-personalized-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() with dynamicVariables and assistantOverride
  • 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-Key from destination + date — re-running won't double-dial the same person today
  • client.calls.waitUntilEnded() when WAIT_FOR_RESULT=1 is 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

📂 02-bulk-campaign/

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() with schedule.timezone/windowStart/windowEnd, retryPolicy.maxAttempts, maxConcurrent
  • client.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-02

Example 03 — Verified Webhook Receiver

📂 03-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

📂 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 back
  • dynamicVariables substitution 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 time

When to use simulation vs real calls

PhaseUse
Iterating on system promptSimulation — fast, free, repeatable
Testing edge cases / red-teamingSimulation — try 50 weird inputs in a minute
Testing tool calls (HTTP/MCP)Simulation — tools fire in simulate too
Testing TTS voice qualityReal call — simulation has no audio
Testing VAD / turn-takingReal call — needs actual audio timing
Testing pronunciation issuesReal call — pronunciation dict only affects TTS
Final pre-launch QAReal 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