Skip to main content

Install

npm install @yotel/client
Runs in Node 18+, Deno, Bun, and modern browsers (though you probably don’t want to call the API from a browser — the key would be exposed).

Quickstart

import { YotelClient } from "@yotel/client";

const client = new YotelClient({
  apiKey: process.env.YOTEL_KEY!,
});

// Create campaign
const c = await client.campaigns.create({
  name: "Q4 outreach",
  dial_mode: "predictive",
  predictive_target_abandon: 0.02,
});

// Push lead
const lead = await client.leads.create({
  campaign_id: c.id,
  phone: "9876543210",
  name: "Priya",
  email: "priya@example.com",
});

// Bulk push
const result = await client.leads.bulk({
  campaign_id: c.id,
  leads: [
    { phone: "9876543211" },
    { phone: "9876543212", name: "Arjun" },
  ],
});
console.log(result.valid, "added,", result.duplicates, "skipped");

Verifying webhooks

import express from "express";
import { verifyWebhook, InvalidSignature } from "@yotel/client";

const app = express();

app.post(
  "/webhooks/yotel",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = verifyWebhook({
        rawBody: req.body,
        headers: req.headers,
        signingSecret: process.env.YOTEL_WEBHOOK_SECRET!,
      });

      if (event.event_type === "call.ended") {
        const callId = event.data.call_id;
        // ...
      }

      res.status(200).send();
    } catch (e) {
      if (e instanceof InvalidSignature) {
        return res.status(401).send();
      }
      throw e;
    }
  },
);

Rate-limit handling

The client auto-retries 429s up to 3 times, respecting Retry-After:
// default
const client = new YotelClient({ apiKey: "..." });

// disable
const client = new YotelClient({ apiKey: "...", retryOn429: false });

Error handling

import {
  YotelClient,
  AuthenticationError,
  RateLimitError,
  ValidationError,
  APIError,
} from "@yotel/client";

try {
  await client.leads.create({ campaign_id: "c-1", phone: "bad" });
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(e.errors);
  } else if (e instanceof RateLimitError) {
    console.log("retry after", e.retryAfter);
  } else if (e instanceof AuthenticationError) {
    // ...
  } else if (e instanceof APIError) {
    console.log(e.statusCode, e.detail);
  }
}

Types

Every request + response is typed. Hover in VS Code for docs on each field.
import type { Campaign, Lead, PredictiveStats } from "@yotel/client";

OAuth helpers

For partner apps that act on behalf of individual Yotel users, the SDK ships PKCE-ready OAuth helpers — works in browser + Node.
import { AuthorizationCodeFlow, generateCodeVerifier } from "@yotel/client";

const flow = new AuthorizationCodeFlow({
  clientId: "sfdc_yotel",
  clientSecret: process.env.YOTEL_CLIENT_SECRET!,
  redirectUri: "https://app.example/cb",
  scopes: ["campaigns:read", "openid", "offline_access"],
});
const verifier = generateCodeVerifier();
const url = await flow.buildAuthorizeUrl({ state: "csrf-1", codeVerifier: verifier });
// ... redirect user, callback handler ...
const tokens = await flow.exchangeCode({ code, codeVerifier: verifier });
const next = await tokens.refresh(); // rotated refresh_token
JWTBearerFlow is Node-only (it signs with node:crypto). Browser bundles that only use the authorization-code flow won’t include any Node-specific imports. See OAuth 2.0 for the full flow reference.

Recordings

Access call recordings — signed URLs, raw bytes, or streamed.
// Get a signed URL (7-day expiry)
const recording = await client.recordings.getSignedUrl("call_id");
console.log(recording.url, recording.stereo);

// Download to disk (Node.js only)
await client.recordings.download("call_id", "recording.wav");

// Load into memory
const audio = await client.recordings.fetchBytes("call_id");

// Stream to a writable (Node.js only)
import { createWriteStream } from "fs";
await client.recordings.streamTo("call_id", createWriteStream("out.wav"));
getSignedUrl requires calls:read scope. fetchBytes, download, and streamTo require the separate recordings:download scope.
See Recordings API for response details.

Voice agents

Full CRUD for voice agent routing aliases.
// Create
const va = await client.voiceAgents.create({
  name: "Support bot",
  ws_url: "wss://ai.example.com/ws",
  max_concurrent: 50,
});

// List / Get
const all = await client.voiceAgents.list();
const one = await client.voiceAgents.get(va.id);

// Update (PATCH)
await client.voiceAgents.update(va.id, { max_concurrent: 100 });

// Archive (soft delete) or hard delete
await client.voiceAgents.archive(va.id);
await client.voiceAgents.delete(va.id);

// Tenant default
await client.voiceAgents.setAsDefault(va.id);
const defaultId = await client.voiceAgents.getDefault();
See Voice agents CRUD and Voice agent defaults.

AI sessions

Control active voice AI sessions with typed verb methods.
// Transfer call to a human agent
await client.aiSessions.transfer("call_abc", {
  destinationType: "agent",
  destination: "agent_xyz",
  callbackToken: "yt_cb_...",
});

// Hang up
await client.aiSessions.hangup("call_abc", {
  callbackToken: "yt_cb_...",
});

// Set disposition
await client.aiSessions.setDisposition("call_abc", {
  disposition: "interested",
  notes: "Wants premium plan",
  callbackToken: "yt_cb_...",
});

// Get current call state
const state = await client.aiSessions.getCallState("call_abc", {
  callbackToken: "yt_cb_...",
});
All 25 control verbs are available as typed methods: transfer, hangup, log, mute, unmute, hold, unhold, sendDtmf, playAudio, setDisposition, recordingPause, recordingResume, getCallState, conferenceStart, conferenceAdd, conferenceRemove, conferenceLeave, requestSupervisor, whisper, barge, monitorStart, monitorStop, setLeadField, setLeadStatus, scheduleCallback. See AI sessions and the Control API reference for verb parameters.

Additional error types

Beyond the base errors, voice agent and AI session operations can throw:
import {
  NoVoiceAgentConfigured,        // 424 — no agent resolved in hierarchy
  VoiceAgentInUse,               // 409 — active sessions prevent deletion
  MaxConcurrentExceeded,         // 429 — concurrency limit reached
  ConferenceMembershipRequired,  // 403 — whisper/barge needs conference
  AISessionTerminated,           // 410 — call already ended
} from "@yotel/client";
See Error reference for details.

Source

github.com/kamal-zetta/dialer/tree/main/sdks/typescript