docs

Docs › SDKs

TypeScript SDK

@handset/sdk is generated from the OpenAPI spec — one typed function per endpoint, ESM and CommonJS, zero dependencies beyond a fetch-capable runtime (Node 18+, Bun, Deno, edge workers, browsers).

Install & configure

shell
npm install @handset/sdk

Configure the shared client once at startup — every SDK function uses it. The base URL defaults to production; only the key is required.

setup.ts
import { client } from "@handset/sdk/client";

client.setConfig({
  headers: { Authorization: `Bearer ${process.env.HANDSET_API_KEY}` },
});

Server-side usage (Node, Next.js, workers)

The SDK is isomorphic, but your API key belongs on the server — call Handset from your backend and keep the key out of browser bundles. In Next.js, that means route handlers or server actions. Configure the client once in a server-only module and import SDK functions anywhere server-side:

lib/handset.ts
import { client } from "@handset/sdk/client";

let configured = false;
export function configureHandset() {
  if (configured) return;
  client.setConfig({
    headers: { Authorization: `Bearer ${process.env.HANDSET_API_KEY}` },
  });
  configured = true;
}
app/api/messages/route.ts
import { sendMessage } from "@handset/sdk";
import { configureHandset } from "@/lib/handset";

export async function POST(req: Request) {
  configureHandset();
  const { to, body } = await req.json();
  const res = await sendMessage({ body: { from: process.env.HANDSET_NUMBER_ID!, to, body } });
  if (res.error) return Response.json(res.error, { status: res.response.status });
  return Response.json(res.data);
}

The same pattern works in Express middleware, Bun, Deno, and edge workers — anywhere with fetch. Browser calls to the API are blocked by CORS by design; proxy through your backend as above.

Every call returns an envelope

SDK functions never throw on API errors. Each returns { data, error, response }:

send.ts
import { sendMessage } from "@handset/sdk";

const res = await sendMessage({
  body: { from: "num_01kzv…", to: "+14155550123", body: "On my way!" },
});

if (res.error) {
  console.error(res.error.error.code);   // e.g. "recipient_opted_out"
} else {
  console.log(res.data.id, res.data.status);  // msg_…, "queued"
}

Error codes worth branching on: recipient_opted_out (they sent STOP — do not retry), campaign_not_approved (10DLC in review), rate_limited, and tenant_not_found.

Rate limits (20 req/s per key, bursts to 60) are handled for you: the SDK retries 429s automatically, honoring Retry-After, up to 3 attempts. You'll only see rate_limited if the limit is still exceeded after retries. Tune or disable via client.setConfig({ fetch: withRetry(fetch, { maxAttempts: 1 }) }).

Idempotent sends

idempotent.ts
await sendMessage({
  body: { from: "num_…", to: "+1415…", body: "Appointment confirmed." },
  headers: { "Idempotency-Key": crypto.randomUUID() },
});

The core loop

onboard.ts
import {
  createTenant, searchAvailableNumbers, purchaseNumber,
  sendMessage, listConversations,
} from "@handset/sdk";

// 1. Your customer, as a tenant
const tenant = await createTenant({
  body: { name: "Bayview Dental", timezone: "America/Los_Angeles" },
});

// 2. A number in their area code, live on arrival
const found = await searchAvailableNumbers({ query: { area_code: "415" } });
const number = await purchaseNumber({
  body: { tenant_id: tenant.data!.id, phone_number: found.data!.data[0].phone_number! },
});

// 3. Send — threading, opt-outs, 10DLC all happen server-side
await sendMessage({
  body: { from: number.data!.id, to: "+14155550123", body: "Welcome aboard!" },
});

// 4. Read the thread back for your UI
const threads = await listConversations({ query: { tenant_id: tenant.data!.id } });

Pagination

paginate.ts
let after: string | undefined;
do {
  const page = await listMessages({ query: { limit: 100, after } });
  for (const m of page.data?.data ?? []) handle(m);
  after = page.data?.has_more ? page.data.next_cursor ?? undefined : undefined;
} while (after);

Types

Every request and response shape is exported — the SDK is also your type library:

types.ts
import type { Message, Conversation, Tenant, Voicemail } from "@handset/sdk";

function renderBubble(m: Message) { /* m.direction, m.status, m.body… */ }
Python and Go are official too — same spec, same objects: Python SDK guide · Go SDK guide.