osmTalk Docs
SDKs

TypeScript / Node SDK

Official @osmapi/osmtalk-sdk for Node.js, Deno, Bun, Cloudflare Workers, and modern browsers.

npm install @osmapi/osmtalk-sdk
# or pnpm add / yarn add / bun add

Works in Node 18+, Deno, Bun, Cloudflare Workers, and any environment with a global fetch. Zero runtime dependencies.

npm

Looking for runnable code? Check out osm-API/osmtalk-examples — four end-to-end TypeScript projects using this SDK. Also see SDK Examples for the scenario walk-throughs.

Quick start

import { Osmtalk } from "@osmapi/osmtalk-sdk";

const client = new Osmtalk({ apiKey: process.env.OSMTALK_API_KEY! });

const call = await client.calls.outbound({
  agentId: "agent_xxx",
  phoneNumberId: "pn_xxx",
  destination: "+919876543210",
  dynamicVariables: {
    first_name: "Arjun",
    "Client Name": "Arjun",          // keys with spaces accepted (May 2026)
    appointment_date: "Tuesday May 12 at 3 PM",
  },
});

console.log("Call started:", call.callId);

Resources

client.agents          // list, get, create, update, delete, connect
                       // publishVersion, listVersions, getVersion, rollbackToVersion

client.calls           // list (paginated), get, outbound, end, transfer
                       // waitUntilEnded — poll until terminal status

client.campaigns       // create, list, get, update, delete
                       // start, pause, resume, stop, report
                       // uploadLeadsCsv, uploadLeads, listLeads

client.phoneNumbers    // list, update                      ← new in 0.4.0

client.dnc             // list, add, bulkAdd, remove

client.eval            // simulate                           ← test without spending money
                       // createTestCase, runTestCase, runAll, listRuns, getRun

client.settings        // get, getStorage, updateStorage
                       // getWebhook, updateWebhook
                       // getCompliance, updateCompliance

client.platform        // getRates, listProviders, getPresets, getModelHealth
                       // getTemplates, getTemplate, saveTemplate, deleteTemplate

Plus the standalone helpers verifyWebhookSignature / verifyWebhookSignatureAsync for webhook receivers.

What's new in 0.5.0

  • CallRecord.failureReason — populated whenever a call ends without a real conversation. One of: no_audio_output, no_audio_either_direction, idle_timeout, provider_circuit_open, sip_no_answer, sip_rejected, bot_startup_failed, caller_hung_up_silently, stale_sweep, or unknown (union exported as CallFailureReason). See the Failure Reasons reference for a per-reason guide.
  • describeFailureReason(reason) — returns { title, cause, likelyBlame, whatToTry, retryable }. The same copy renders in the dashboard banner and in the example projects, so testers see consistent wording everywhere.
  • isRetryableFailure(reason) + RETRYABLE_FAILURE_REASONS — the closed set the campaign workers retry automatically. Use to gate your own retry logic.
  • callConnected(record) — heuristic for "this was a real conversation, not a phantom call." Server-side equivalent is CallRecord.didConnect.
  • client.calls.list({ campaignId, failureReason }) — two new filters, ideal for triage dashboards and the per-campaign breakdown report in Example 02.

What's new in 0.4.0

  • Breaking type fix: client.calls.list() returns a paginated envelope { data, total, limit, offset }. The server always returned this — the prior CallRecord[] type was a lie that crashed .slice() calls at runtime.
  • client.phoneNumbers resource — list() and update() so you don't have to copy IDs from the dashboard URL bar.
  • calls.list() filters: status, agentId, channel, from, to, search, limit, offset. Drives dashboard-style search.

What's new in 0.3.0

  • Auto-retry on 5xx, 429, and network errors with Retry-After honored and exponential backoff with jitter. Mutating requests (POST/PUT/DELETE) only retry when you pass idempotencyKey so the SDK never silently double-charges.
  • client.calls.waitUntilEnded(callId) — one-line polling helper that returns the final CallRecord once the call hits a terminal status.
  • AbortSignal support on every method via RequestOptions.signal.
  • User-Agent header sent automatically — easier to identify SDK traffic in API logs.
  • Per-call org override via RequestOptions.organizationId.
  • OsmtalkError.isRetryable / .isClientError / .retryAttempts for cleaner error branching.

Common patterns

Place a call and wait for the result

const { callId } = await client.calls.outbound({
  agentId: "agent_xxx",
  phoneNumberId: "pn_xxx",
  destination: "+919876543210",
});

// Default: poll every 5s, give up after 30 minutes. Configurable.
const final = await client.calls.waitUntilEnded(callId, {
  pollIntervalMs: 5_000,
  timeoutMs: 15 * 60 * 1000,
});

console.log("Status:    ", final.status);     // "completed" / "failed" / etc.
console.log("Duration:  ", final.durationSeconds, "s");
console.log("Recording: ", final.recordingUrl);

For production, prefer the webhook receiver (see Example 03 in the examples repo) — waitUntilEnded is best for scripts, demos, and one-off jobs.

Handle failed calls correctly

A "failed" call is the SDK telling you: the call ended without producing a real conversation. Use describeFailureReason() for human copy and isRetryableFailure() to decide whether to redial:

import {
  describeFailureReason,
  isRetryableFailure,
  callConnected,
} from "@osmapi/osmtalk-sdk";

const final = await client.calls.waitUntilEnded(callId);

if (final.status === "completed" && callConnected(final)) {
  console.log("✅ Real conversation, log the outcome");
} else if (final.status === "failed") {
  const g = describeFailureReason(final.failureReason);
  console.warn(`❌ ${g.title}`);
  console.warn(`   ${g.cause}`);
  console.warn(`   → ${g.whatToTry}`);

  if (isRetryableFailure(final.failureReason)) {
    // Network / TTS storm / idle timeout / no-answer — try again.
    await redial(final);
  } else {
    // Carrier rejected / caller silently hung up — escalate to a human.
    await flagForHumanReview(final);
  }
}

Full per-reason reference: Failure Reasons.

List recent calls with filters

const { data, total } = await client.calls.list({
  status: "completed",
  channel: "phone",
  from: "2026-05-01",
  limit: 25,
});

console.log(`Showing ${data.length} of ${total} matching calls`);

Discover your phone-number IDs (instead of copying from dashboard)

const numbers = await client.phoneNumbers.list();
for (const n of numbers) {
  console.log(`${n.id}  ${n.phoneNumber}  outbound→${n.outboundAgentId ?? "—"}`);
}

Test prompts without spending money

const sim = await client.eval.simulate("agent_xxx", [
  { role: "user", content: "Hi, who's calling?" },
  { role: "user", content: "Tell me about your refund policy" },
], {
  dynamicVariables: { first_name: "Arjun" },
});

for (const turn of sim.transcript) {
  console.log(`${turn.role}: ${turn.content}`);
}
// ₹0 spent, no phones rang, ~2s of wall time.

Cancel an in-flight request

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 2_000);

try {
  await client.agents.list({ signal: ctrl.signal });
} catch (err) {
  if (ctrl.signal.aborted) console.log("Cancelled by us");
  else throw err;
}

signal, timeoutMs, and organizationId are accepted on every method via the trailing RequestOptions argument.

Run an outbound campaign

const camp = await client.campaigns.create({
  name: "Q2 Renewals",
  agentId: "agent_xxx",
  phoneNumberId: "pn_xxx",
  maxConcurrent: 5,
  schedule: { timezone: "Asia/Kolkata", windowStart: "10:00", windowEnd: "18:00" },
  retryPolicy: { maxAttempts: 3, backoffMinutes: 60, retryOn: ["no_answer", "busy"] },
  webhookUrl: "https://your-crm/webhooks/osmtalk",
});

await client.campaigns.uploadLeadsCsv(camp.id, fs.readFileSync("./leads.csv", "utf8"));
await client.campaigns.start(camp.id);

const r = await client.campaigns.report(camp.id);
console.log(`Qualified: ${r.counts.byDisposition.qualified ?? 0}`);

Publish + run a specific agent version

const v = await client.agents.publishVersion("agent_xxx", { label: "Tighter qualifier" });
await client.calls.outbound({
  agentId: "agent_xxx",
  phoneNumberId: "pn_xxx",
  destination: "+919876543210",
  agentVersion: v.version,
});

Verify a webhook (Node)

import express from "express";
import { verifyWebhookSignature } from "@osmapi/osmtalk-sdk";

const app = express();
// IMPORTANT: use express.raw(), NOT express.json(). The signature is
// computed over raw bytes; re-serialized JSON will not match.
app.use("/webhooks/osmtalk", express.raw({ type: "application/json" }));

app.post("/webhooks/osmtalk", (req, res) => {
  const ok = verifyWebhookSignature(
    req.body,
    req.header("x-osmtalk-signature"),
    process.env.OSMTALK_WEBHOOK_SECRET!,
  );
  if (!ok) return res.status(401).end();

  const event = JSON.parse(req.body.toString("utf-8"));
  if (event.event === "call.completed") {
    console.log("Call ended:", event.call.id, event.analysis?.disposition);
  }
  res.json({ ok: true });
});

Verify webhooks in Deno / Bun / Cloudflare Workers

import { verifyWebhookSignatureAsync } from "@osmapi/osmtalk-sdk";

export default {
  async fetch(req: Request, env: { OSMTALK_WEBHOOK_SECRET: string }) {
    const raw = await req.text();
    const sig = req.headers.get("x-osmtalk-signature");
    if (!(await verifyWebhookSignatureAsync(raw, sig, env.OSMTALK_WEBHOOK_SECRET))) {
      return new Response("unauthorized", { status: 401 });
    }
    // ... handle JSON.parse(raw)
    return new Response("ok");
  },
};

TypeScript types

All inputs and outputs are typed. Import directly:

import type {
  Osmtalk,
  CallRecord,
  CampaignRecord,
  AgentRecord,
  PhoneNumberRecord,
  DynamicVariables,
  AssistantOverride,
  Paginated,
  CallListOptions,
  RequestOptions,
  OsmtalkError,
} from "@osmapi/osmtalk-sdk";

Error handling

import { OsmtalkError } from "@osmapi/osmtalk-sdk";

try {
  await client.calls.outbound(/* ... */);
} catch (err) {
  if (err instanceof OsmtalkError) {
    console.log("HTTP", err.status, err.body);
    console.log("Retries attempted:", err.retryAttempts);
    if (err.isRetryable) console.log("Server might recover — try again later.");
    if (err.isClientError) console.log("Bad input — check err.body.details.");
  }
}
StatusMeaningOsmtalkError flag
400Validation — err.body.details has zod field errorsisClientError
401Bad API keyisClientError
402Insufficient creditsisClientError
404Resource not foundisClientError
408Request timeoutisRetryable
429Concurrency or rate limitisRetryable
5xxServer error / provider outageisRetryable

The SDK already auto-retries 408/429/5xx and network errors twice by default. Mutating requests (POST/PUT/DELETE) are only retried when you pass idempotencyKey so the SDK never silently double-charges.

Options

new Osmtalk({
  apiKey: "osm_…",
  baseUrl: "https://api.osmtalk.com",   // default; override for self-hosted
  timeoutMs: 30_000,                    // per-request, 0 to disable
  maxRetries: 2,                        // auto-retry count for 5xx/429
  retryInitialDelayMs: 250,             // doubles per retry, jittered
  organizationId: "org_xxx",            // for multi-org accounts
  defaultHeaders: { "X-Trace-Id": "…" },// added to every request
  fetch: customFetch,                   // for Workers / tests
});

Per-request overrides on any method:

await client.calls.outbound(input, {
  idempotencyKey: `dest-${destination}-${date}`,
  signal: controller.signal,
  timeoutMs: 60_000,
  organizationId: "org_yyy",
});

Next steps