TypeScript SDK

@dialgem/sdk: a typed client for your backend, with webhook verification.

@dialgem/sdk is for your backend. It has no runtime dependencies and ships ESM and CommonJS builds. It runs on Node.js 20+, Bun, Deno and serverless runtimes that provide fetch and Web Crypto.

Install

bash
npm install @dialgem/sdk

Create an organization API key in the dashboard (see Get your API keys) and set it as DIALGEM_API_KEY.

ts
import { Dialgem } from "@dialgem/sdk"

const dialgem = new Dialgem() // reads DIALGEM_API_KEY

const agent = await dialgem.agents.create({
  name: "Front desk",
  system_prompt: "You are the front desk for {{business_name}}.",
  first_message: "Thanks for calling, how can I help?",
  default_dynamic_variables: { business_name: "Acme Dental" },
})

Options

OptionDefaultMeaning
apiKeyprocess.env.DIALGEM_API_KEYYour organization API key.
timeout60000Milliseconds per attempt.
maxRetries2See Retries.
fetchglobal fetchA custom implementation, such as a proxy agent.
defaultHeaders{}Sent with every request.

Every method takes a final optional argument, { timeout, maxRetries, signal, headers }, which overrides these for that request.

Recipes

Put an agent on a phone number

ts
const number = await dialgem.phoneNumbers.create({ phone_number: "+14155550100" })

await dialgem.phoneNumbers.update(number.id, {
  inbound_agent_id: agent.id,
  set_inbound_agent: true, // inbound_agent_id is ignored without this
})

Place an outbound call

ts
const { call_id } = await dialgem.calls.createPhoneCall({
  from_: "+14155550100", // a number on your organization
  to: "+14155550199",
  agent_id: agent.id,
  dialgem_llm_dynamic_variables: { customer_name: "Sam" },
  metadata: { crm_id: "42" }, // returned on the call and in every webhook
})

await dialgem.calls.stop(call_id) // end it now

from_ is the wire name: the underscore is part of it.

Start a web call for a browser

ts
const { access_token } = await dialgem.webCalls.create({ agent_id: "AGENT_ID" })

Hand access_token to the page. See Web calls.

List calls

calls.list() returns one page when awaited. Iterate to walk every page:

ts
for await (const call of dialgem.calls.list({ start_after: Date.parse("2026-09-01T00:00:00Z"), status: "completed", limit: 50 })) {
  console.log(call.id, call.duration_ms, call.analysis?.call_summary)
}

Filters include agent_id, status, direction, call_type, disconnection_reason, from_number, to_number, user_sentiment, call_successful, start_after, start_before, metadata, search and sort_order. Pages are cursor-based, so walking is stable while new calls arrive.

Ship a change without touching live calls

The agent you edit is the draft. Publishing freezes it as a numbered version that never changes.

ts
const v1 = await dialgem.agents.versions.publish(agent.id, { title: "Refund flow" })

await dialgem.phoneNumbers.assign(number.id, { agent_id: agent.id, agent_version: v1.version })
await dialgem.agents.update(agent.id, { system_prompt: "…work in progress…" })

Use agent_version: "latest_published" to have a number follow the newest publish.

Bring your own Twilio number

Point the number's Voice webhook at your backend, register the call, then connect Twilio's media stream to Dialgem:

ts
app.post("/twilio/voice", express.urlencoded({ extended: false }), async (req, res) => {
  const { stream_url } = await dialgem.calls.register({
    agent_id: AGENT_ID,
    call_sid: req.body.CallSid,
    from_number: req.body.From,
    to_number: req.body.To,
  })
  // stream_url contains `&`: escape it for XML.
  res.type("text/xml").send(`<Response><Connect><Stream url="${escapeXml(stream_url)}"/></Connect></Response>`)
})

Your Twilio credentials are never shared with Dialgem. The stream_url is signed and expires after expires_in seconds.

Clone a voice

ts
const voice = await dialgem.voices.clone({
  name: "Ada",
  files: [new Blob([await readFile("./sample.mp3")], { type: "audio/mpeg" })],
  consent_confirmed: true, // you hold the rights and have the speaker's consent
})

Verify webhooks

Verify against the raw body. A body that has been parsed and written back out, for example by express.json(), will never verify.

ts
import { Dialgem, WebhookVerificationError } from "@dialgem/sdk"

app.post("/webhooks/dialgem", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    const event = await Dialgem.webhooks.constructEvent(req.body, req.header("x-dialgem-signature"), SECRET)
    if (event.event === "call_analyzed") await crm.update(event.call.metadata.crm_id, event.call.analysis?.variables)
    res.sendStatus(204)
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.status(400).send(err.message)
    throw err
  }
})

event is a discriminated union, so switch (event.event) narrows the payload. webhooks.verify returns a boolean if you prefer to handle rejection yourself, and constructFunctionCall verifies requests to your functions. Signatures older than five minutes are rejected. See the event reference.

Live call control

ts
await dialgem.sessions.update(call_id, { dynamic_variables: { plan: "pro" } }) // this call only
await dialgem.sessions.say(call_id, { text: "One moment while I check that." })
await dialgem.sessions.release(call_id)

say and release are off unless live control is enabled for your account, and then raise PermissionDeniedError with errCode live_control_disabled.

Errors

Every error extends DialgemError. HTTP errors extend APIError and are chosen by status.

StatusClass
400BadRequestError
401AuthenticationError
403PermissionDeniedError
404NotFoundError
409ConflictError
422UnprocessableEntityError
429RateLimitError
500 and aboveInternalServerError
No responseAPIConnectionError, APIConnectionTimeoutError
Your signal abortedAPIUserAbortError
ts
try {
  await dialgem.agents.update(id, { flow_config })
} catch (err) {
  if (err instanceof UnprocessableEntityError && err.errCode === "flow_invalid") {
    for (const issue of err.issues) console.log(issue.node_id, issue.message)
  } else if (err instanceof APIError) {
    console.error(err.status, err.errCode, err.message, err.requestId)
  }
}

Branch on errCode to tell errors with the same status apart, and include requestId when you contact support.

Retries

GET, PUT, PATCH and DELETE are retried up to maxRetries times on 408, 429, 5xx and connection failures, with exponential backoff and jitter, honouring Retry-After.

POST is not retried by default, because most POSTs create something. A retried createPhoneCall could dial the same person twice. To retry one anyway: dialgem.agents.create(body, { maxRetries: 2 }).

Redirects are never followed, so your key cannot be sent somewhere else.

Testing against a local stack

The address is fixed. In your tests, pass a fetch that rewrites it:

ts
const local = new Dialgem({
  apiKey: "zk_local",
  fetch: (url, init) => fetch(String(url).replace("https://api.dialgem.com", "http://localhost:8002"), init),
})