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
npm install @dialgem/sdkCreate an organization API key in the dashboard (see Get your API keys) and set it as DIALGEM_API_KEY.
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
| Option | Default | Meaning |
|---|---|---|
apiKey | process.env.DIALGEM_API_KEY | Your organization API key. |
timeout | 60000 | Milliseconds per attempt. |
maxRetries | 2 | See Retries. |
fetch | global fetch | A 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
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
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 nowfrom_ is the wire name: the underscore is part of it.
Start a web call for a browser
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:
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.
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:
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
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.
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
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.
| Status | Class |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | UnprocessableEntityError |
| 429 | RateLimitError |
| 500 and above | InternalServerError |
| No response | APIConnectionError, APIConnectionTimeoutError |
Your signal aborted | APIUserAbortError |
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:
const local = new Dialgem({
apiKey: "zk_local",
fetch: (url, init) => fetch(String(url).replace("https://api.dialgem.com", "http://localhost:8002"), init),
})