@letustalkai/sdk
v0.1.1
Published
Official TypeScript SDK for the letustalk.ai voice AI platform
Downloads
19
Maintainers
Readme
@letustalkai/sdk
Official TypeScript / JavaScript SDK for the letustalk.ai voice AI platform.
npm install @letustalkai/sdk
# or: pnpm add @letustalkai/sdkWorks in Node 18+, Deno, Bun, Cloudflare Workers, and browsers.
Quick start
import { LetusTalk } from "@letustalkai/sdk";
const client = new LetusTalk({ apiKey: process.env.LETUSTALK_API_KEY! });
// Place an outbound call with per-call personalization
const call = await client.calls.outbound({
agentId: "agent_xxx",
phoneNumberId: "pn_xxx",
destination: "+919876543210",
dynamicVariables: {
first_name: "Arjun",
company: "Acme",
renewal_date: "May 28, 2026",
},
});
console.log("Call started:", call.callId);Resources
| Resource | Operations |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| client.agents | list, get, create, update, delete, connect, publishVersion, listVersions, getVersion, rollbackToVersion |
| client.calls | list (paginated), get, outbound, end, transfer, waitUntilEnded |
| client.campaigns | list, get, create, update, delete, start, pause, resume, stop, report, uploadLeadsCsv, uploadLeads, listLeads |
| client.phoneNumbers | list, update |
| client.dnc | list, add, bulkAdd, remove |
| client.eval | simulate, createTestCase, listTestCases, 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 (see below).
Features
- Auto-retry on 5xx, 429, and network errors with
Retry-Afterhonored and exponential backoff. Mutating requests (POST/PUT/DELETE) only retry when anidempotencyKeyis provided, so the SDK never silently double-charges. client.calls.waitUntilEnded(callId)— one-line polling helper for the "place call → wait → get result" pattern.AbortSignalsupport on every method viaRequestOptions.signal.- Per-org request override via
RequestOptions.organizationId. LetusTalkError.isRetryable/.isClientError/.retryAttemptsfor clean error branching.client.calls.list()returns aPaginated<CallRecord>envelope —{ data, total, limit, offset }— so you can drive UI pagination without a separate count query. Filters:status,agentId,channel,campaignId,failureReason,from,to,search,limit,offset.CallRecord.failureReason— populated whenever a call ends without producing 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(the union is exported asCallFailureReason).describeFailureReason(reason)returns{ title, cause, likelyBlame, whatToTry, retryable }— the same copy the dashboard banner uses. Drop into your CRM logs / Slack alerts for consistent wording.isRetryableFailure(reason)+RETRYABLE_FAILURE_REASONS— the closed set the campaign workers auto-retry. Use to gate custom retry logic.callConnected(record)— heuristic for "real conversation, not phantom call". Server-side equivalent isCallRecord.didConnect.
See the Failure Reasons docs for a full reason-by-reason guide.
Examples
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/letustalk",
});
await client.campaigns.uploadLeadsCsv(
camp.id,
`
phone,first_name,renewal_date
+919876543210,Arjun,2026-05-28
+919876543211,Meera,2026-06-02
`.trim(),
);
await client.campaigns.start(camp.id);
// Poll the report
const report = await client.campaigns.report(camp.id);
console.log(report.counts.byStatus);Listing 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`);
for (const call of data) {
console.log(call.id, call.durationSeconds, "s", call.disposition);
}Wait for a call to finish (without writing a poll loop)
const { callId } = await client.calls.outbound({
agentId: "agent_xxx",
phoneNumberId: "pn_xxx",
destination: "+919876543210",
});
// Default: poll every 5s, give up after 30 minutes. All configurable.
const final = await client.calls.waitUntilEnded(callId, {
pollIntervalMs: 5_000,
timeoutMs: 15 * 60 * 1000,
});
console.log("Final status:", final.status);
console.log("Duration: ", final.durationSeconds, "s");
console.log("Disposition: ", final.disposition);
console.log("Recording: ", final.recordingUrl);For production, prefer webhooks — see the receiver example below.
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.
Publish a new agent version and A/B test
// Publish current draft as v2
const v2 = await client.agents.publishVersion("agent_xxx", { label: "Tighter qualifier" });
// Call with v2 explicitly
await client.calls.outbound({
agentId: "agent_xxx",
phoneNumberId: "pn_xxx",
destination: "+919876543210",
agentVersion: v2.version,
});Simulate before going live
const sim = await client.eval.simulate("agent_xxx", [
{ role: "user", content: "Hi, who's calling?" },
{ role: "user", content: "Sure, tell me more about renewal options" },
]);
for (const turn of sim.transcript) {
console.log(`${turn.role}: ${turn.content}`);
}Verify webhooks (Node, sync)
import express from "express";
import { verifyWebhookSignature } from "@letustalkai/sdk";
const app = express();
// IMPORTANT: use express.raw() — NOT express.json(). The signature was
// computed over the exact bytes; re-serialized JSON will not match.
app.use("/webhooks/letustalk", express.raw({ type: "application/json" }));
app.post("/webhooks/letustalk", (req, res) => {
const ok = verifyWebhookSignature(
req.body,
req.header("x-letustalk-signature"),
process.env.LETUSTALK_WEBHOOK_SECRET!,
);
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString());
if (event.event === "call.completed") {
console.log("Call ended:", event.call.id, event.analysis?.disposition);
}
res.json({ ok: true });
});Verify webhooks in Workers / Deno / Bun (async, WebCrypto)
import { verifyWebhookSignatureAsync } from "@letustalkai/sdk";
export default {
async fetch(req: Request) {
const raw = await req.text();
const ok = await verifyWebhookSignatureAsync(
raw,
req.headers.get("x-letustalk-signature"),
env.LETUSTALK_WEBHOOK_SECRET,
);
if (!ok) return new Response("invalid signature", { status: 401 });
const event = JSON.parse(raw);
// …handle event
return new Response("ok");
},
};Error handling
import { LetusTalk, LetusTalkError } from "@letustalkai/sdk";
try {
await client.calls.outbound({
/* ... */
});
} catch (err) {
if (err instanceof LetusTalkError) {
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.");
} else {
throw err;
}
}| Status | Meaning | LetusTalkError 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 LetusTalk({
apiKey: "osm_live_…",
baseUrl: "https://api.letustalk.ai", // default
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, // optional, defaults to global fetch
});Per-request overrides:
await client.calls.outbound(input, {
idempotencyKey: `dest-${destination}-${date}`,
signal: controller.signal,
timeoutMs: 60_000,
organizationId: "org_yyy",
});License
MIT
