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 addWorks in Node 18+, Deno, Bun, Cloudflare Workers, and any environment with a global fetch. Zero runtime dependencies.
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, deleteTemplatePlus 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, orunknown(union exported asCallFailureReason). 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 isCallRecord.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 priorCallRecord[]type was a lie that crashed.slice()calls at runtime. client.phoneNumbersresource —list()andupdate()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-Afterhonored and exponential backoff with jitter. Mutating requests (POST/PUT/DELETE) only retry when you passidempotencyKeyso the SDK never silently double-charges. client.calls.waitUntilEnded(callId)— one-line polling helper that returns the finalCallRecordonce the call hits a terminal status.AbortSignalsupport on every method viaRequestOptions.signal.User-Agentheader sent automatically — easier to identify SDK traffic in API logs.- Per-call org override via
RequestOptions.organizationId. OsmtalkError.isRetryable / .isClientError / .retryAttemptsfor 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.");
}
}| Status | Meaning | OsmtalkError flag |
|---|---|---|
| 400 | Validation — err.body.details has zod field errors | isClientError |
| 401 | Bad API key | isClientError |
| 402 | Insufficient credits | isClientError |
| 404 | Resource not found | isClientError |
| 408 | Request timeout | isRetryable |
| 429 | Concurrency or rate limit | isRetryable |
| 5xx | Server error / provider outage | isRetryable |
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
- SDK Examples — four end-to-end scenarios with code
- Outbound Calls — placing calls from the SDK in depth
- Webhooks — configuring delivery + receiving events
- GitHub: osm-API/osmtalk-examples — runnable repo
- CHANGELOG — full version history