@trysaperly/sdk
v3.0.0
Published
TypeScript SDK for the Saperly v2 API — autogenerated from the OpenAPI contract.
Readme
@trysaperly/sdk
TypeScript SDK for the Saperly v2 API — the camelCase data plane (numbers, connections, messaging, voice, usage, consent, pricing, keys).
Autogenerated from the OpenAPI contract. This is the current Saperly SDK — the legacy snake_case
/api/v1surface has been retired.
Install
npm install @trysaperly/sdkRequires Node.js 18+ (uses global fetch).
Usage
import { configure, numbers, voice } from '@trysaperly/sdk'
configure({ apiKey: process.env.SAPERLY_API_KEY! })
// Provision a number and place a call.
const { data: provisioned } = await numbers.provision({ body: { areaCode: '415' } })
const { data: call } = await voice.place({
body: { fromNumberId: provisioned!.id, to: '+14155550123' }
})Every method returns { data, error, request, response } — data on success,
error (the typed error body) on a non-2xx status. Nothing throws by default;
pass { throwOnError: true } to opt into exceptions.
Multiple keys
configure() sets one shared client. To talk to several workspaces from one
process, create isolated clients and pass them per call:
import { createSaperlyClient, numbers } from '@trysaperly/sdk'
const client = createSaperlyClient({ apiKey: 'sap_sk_live_...' })
const { data } = await numbers.list({ client })Resources
numbers, connections, messaging, voice, usage, consent, pricing,
keys, assistant, health. Request/response and typed-error
types are exported from the package root (e.g. NumbersProvisionData).
Retries
configure() / createSaperlyClient() retry idempotent requests
(GET/HEAD/OPTIONS/DELETE) once on 5xx + network errors. POST/PATCH are never
retried. Tune or disable with retries:
configure({ apiKey, retries: 3 }) // or retries: 0 to disableWebhooks
Verify the signature on inbound Saperly webhooks. Saperly signs each delivery
x-saperly-signature: v1=<hex> (HMAC-SHA256 over ${timestamp}.${rawBody}) with
x-saperly-timestamp; verifyWebhook checks the signature constant-time, then
the timestamp window (default 5 min).
import { verifyWebhook } from '@trysaperly/sdk'
const raw = await req.text() // the EXACT bytes — do not re-serialize
const result = await verifyWebhook(raw, process.env.SAPERLY_WEBHOOK_SECRET!, req.headers)
if (!result.valid) return new Response(`invalid: ${result.reason}`, { status: 400 })
// Dedup result.deliveryId for ≥ the tolerance window to defeat replays.Agent brain (manual mode)
In manual mode a connection's "brain" drives a live phone call: Saperly POSTs a
signed { event } to the connection's webhook per call event and reads back exactly
ONE directive. @trysaperly/sdk/agent turns that into a typed handler surface — you
write per-event handlers, the framework owns signature verification (401 on a bad
signature), parsing (400 on a malformed event), dispatch, and the fail-safe rule
(past the signature gate it always returns 200 with a graceful directive, since
a 5xx degrades a live call). The /agent entry is dependency-light (Web Crypto +
plain TS, no generated client, no effect), so it imports cleanly in Node 18+, Bun,
Deno (npm:@trysaperly/sdk/agent), and Cloudflare Workers.
import { agentBrain } from '@trysaperly/sdk/agent'
// Mount on any Web-`Request` runtime (Workers, Deno, Bun.serve, Next.js, Hono…).
export default {
fetch: agentBrain({
secret: env.SAPERLY_MANUAL_SECRET, // the connection's manualSecret
onInboundCall: ({ event }) => `Hi, you've reached ${event.to}. How can I help?`,
onTurn: ({ event }) => `You said: ${event.userText}`, // a string → speak(...)
onCallEnded: () => {} // terminal — the directive is ignored
})
}A handler returns a Directive (speak, reject, transfer, …, built with the
exported builders), a string (shorthand for speak), or nothing (a safe default).
createAgentBrain({ secret }).on('turn', …) is the same engine with a chainable API.
Auth
Scoped Saperly API key (sap_sk_live_…), sent as
Authorization: Bearer <key>. Mint keys in the dashboard.
Development
This package is generated — do not edit src/generated/. The only
hand-written file is src/index.ts (the configure() facade + re-exports).
bun install
bun run generate # re-emit ../openapi.v2.json + regenerate src/generated
bun run build # bundle to dist/ (tsdown → ESM + d.ts)
bun test # smoke tests (auth + base-URL wiring)The generator is @hey-api/openapi-ts; config in
openapi-ts.config.ts. The contract source of truth is the SaperlyApi Effect
HttpApi in packages/api.
