@dialgem/sdk 0.1.0-beta.0 → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.1 (unreleased)
4
+
5
+ ### Breaking changes
6
+ - **The API address is fixed at `https://api.dialgem.com`.** The `baseURL` option and the `DIALGEM_BASE_URL` environment variable are gone. Passing `baseURL` now throws rather than being ignored, so a leftover staging or local URL cannot quietly send your key to production. `DEFAULT_BASE_URL` is renamed `BASE_URL`.
7
+ - **Webhook signatures are timestamped.** `x-dialgem-signature` is now `v=<timestamp_ms>,d=<hex>`, where the HMAC covers the raw body plus the timestamp. `verify` and `constructEvent` also reject signatures more than 5 minutes old; set `toleranceMs` or `now` to change that. Signatures from 0.1.0-beta.0 no longer verify.
8
+ - **Webhook payloads carry the full call record.** Every event is `{ event, call_id, agent_id, call }`. The flat fields (`status`, `duration_s`, `analysis`) now live under `call`.
9
+ - **`calls.list` pages are cursor-based.** A page is `{ data, has_more, pagination_key }`. `page`, `page_size` and `total` are gone; use `limit`.
10
+
11
+ ### Added
12
+ - `calls.update` (replace metadata), `calls.delete` and `calls.stop`.
13
+ - `metadata` on `createPhoneCall` and `register`.
14
+ - New call record fields: `metadata`, `dynamic_variables`, `start_timestamp`, `end_timestamp`, `ended_at`, `duration_ms`, `transcript_text`, `in_voicemail` and `latency` (per-stage p50/p90/p95/p99). Post-call analysis gains `call_summary` and `in_voicemail`.
15
+ - `calls.list` filters: `agent_id`, `status` (one or several), `direction`, `call_type`, `disconnection_reason`, `from_number`, `to_number`, `user_sentiment`, `call_successful`, `start_after`, `start_before`, `metadata`, `sort_order`.
16
+ - `webhooks.constructFunctionCall` verifies and parses signed requests to your custom functions (`{ name, call, args }`).
17
+ - `AgentFunction.args_at_root`.
18
+ - **Voice cloning.** `voices.clone({ name, files, consent_confirmed: true })` uploads audio and returns a voice private to your organization; `voices.delete(voiceId)` removes it at the provider and here. `Voice` gains `is_custom` and `created_by`. The upload is never retried, whatever `maxRetries` says.
19
+ - `phoneNumbers.retrieve(phoneNumber)` looks a number up by its E.164 value.
20
+ - **`agent_version: "latest_published"`** on a phone number and on `createPhoneCall` / `register`: a pointer that follows the newest publish rather than naming a version. A call still reports the concrete version it ran.
21
+ - **Agent versions.** `agents.versions.publish` freezes an agent — its configuration, functions and MCP servers — as an immutable numbered snapshot; `list` and `retrieve` read them back. Pin one with `inbound_agent_version` / `outbound_agent_version` on a phone number, or `agent_version` on `createPhoneCall` and `register`. The version a call ran is reported as `agent_version` on the call record and in every webhook. The agent itself is the draft, version 0, and stays what every unpinned call runs.
22
+
3
23
  ## 0.1.0-beta.0 (unreleased)
4
24
 
5
25
  The first release.
package/README.md CHANGED
@@ -37,7 +37,6 @@ const agent = await dialgem.agents.create({
37
37
  | Option | Default | |
38
38
  |---|---|---|
39
39
  | `apiKey` | `process.env.DIALGEM_API_KEY` | Organization API key. |
40
- | `baseURL` | `process.env.DIALGEM_BASE_URL`, then `https://api.dialgem.com` | Point at a self-hosted or local stack. |
41
40
  | `timeout` | `60000` | Milliseconds per attempt. |
42
41
  | `maxRetries` | `2` | See [Retries](#retries). |
43
42
  | `fetch` | global `fetch` | Custom implementation (proxy agent, instrumentation). |
@@ -94,9 +93,18 @@ const { call_id } = await dialgem.calls.createPhoneCall({
94
93
  to: "+14155550199",
95
94
  agent_id: agent.id,
96
95
  dialgem_llm_dynamic_variables: { customer_name: "Sam" },
96
+ metadata: { crm_id: "42" }, // your own data; returned on the call and in every webhook
97
97
  })
98
98
  ```
99
99
 
100
+ Manage it afterwards:
101
+
102
+ ```ts
103
+ await dialgem.calls.update(call_id, { metadata: { crm_id: "42", campaign: "renewals" } })
104
+ await dialgem.calls.stop(call_id) // end it now; disconnection_reason becomes "manual_stopped"
105
+ await dialgem.calls.delete(call_id) // a finished call: record, transcript and recording
106
+ ```
107
+
100
108
  ### 3. Bring your own Twilio number
101
109
 
102
110
  Point your Twilio number's Voice webhook at your backend. Register the call there, then return TwiML that connects Twilio's media stream to Dialgem:
@@ -118,7 +126,13 @@ Your Twilio credentials are never shared with Dialgem. `stream_url` is signed an
118
126
 
119
127
  ### 4. Verify webhooks
120
128
 
121
- Set `webhook_url`, `webhook_events` and `webhook_signing_secret` on the agent. Dialgem then POSTs `call_started`, `call_ended` and `call_analyzed` events, each signed with HMAC-SHA256 in the `x-dialgem-signature` header.
129
+ Set `webhook_url`, `webhook_events` and `webhook_signing_secret` on the agent. Dialgem then POSTs `call_started`, `call_ended` and `call_analyzed` events. Each body is `{ event, call_id, agent_id, call }`, where `call` is the full call record, and each request is signed:
130
+
131
+ ```
132
+ x-dialgem-signature: v=<timestamp_ms>,d=<hex> hex = HMAC-SHA256(secret, raw_body + timestamp_ms)
133
+ ```
134
+
135
+ `constructEvent` and `verify` check the HMAC and reject signatures more than 5 minutes old, so a captured request cannot be replayed. `call_ended` arrives as soon as the call ends; `call_analyzed` follows once the summary, sentiment and your extracted variables are ready.
122
136
 
123
137
  > **Verify the raw body.** The signature covers the exact bytes Dialgem sent. A body that was parsed and re-serialized, for example by `express.json()`, will never verify, even when the JSON is identical.
124
138
 
@@ -130,7 +144,7 @@ import { Dialgem, WebhookVerificationError } from "@dialgem/sdk"
130
144
  app.post("/webhooks/dialgem", express.raw({ type: "application/json" }), async (req, res) => {
131
145
  try {
132
146
  const event = await Dialgem.webhooks.constructEvent(req.body, req.header("x-dialgem-signature"), SECRET)
133
- if (event.event === "call_analyzed") await crm.update(event.call_id, event.analysis.variables)
147
+ if (event.event === "call_analyzed") await crm.update(event.call.metadata.crm_id, event.call.analysis?.variables)
134
148
  res.sendStatus(204)
135
149
  } catch (err) {
136
150
  if (err instanceof WebhookVerificationError) return res.status(400).send(err.message)
@@ -155,26 +169,91 @@ export async function POST(request: Request) {
155
169
 
156
170
  **Fastify**: register a buffer parser for `application/json` on the webhook route. See `examples/webhook-fastify.ts`.
157
171
 
158
- `event` is a discriminated union, so `switch (event.event)` narrows the payload type. A failed delivery is retried once, so make your handler idempotent on `(event, call_id)`. Respond within 10 seconds and queue any slow work. `webhooks.verify(body, signature, secret)` returns a boolean if you prefer to handle rejection yourself.
172
+ `event` is a discriminated union, so `switch (event.event)` narrows the payload type. A failed delivery is retried up to three times; every attempt carries the same `x-dialgem-delivery` header, so drop an id you have already processed. Respond within 10 seconds and queue any slow work. `webhooks.verify(body, signature, secret)` returns a boolean if you prefer to handle rejection yourself.
173
+
174
+ **Verifying your custom functions.** When the agent calls one of your functions, the request is signed the same way, and its body is `{ name, call, args }`:
175
+
176
+ ```ts
177
+ app.post("/functions/lookup-order", express.raw({ type: "application/json" }), async (req, res) => {
178
+ const { args, call } = await Dialgem.webhooks.constructFunctionCall<{ order_id: string }>(
179
+ req.body, req.header("x-dialgem-signature"), SECRET,
180
+ )
181
+ res.json(await orders.find(args.order_id, call.metadata.crm_id))
182
+ })
183
+ ```
184
+
185
+ A function created with `args_at_root: true` receives the arguments object alone instead.
159
186
 
160
187
  ### 5. Export calls and transcripts
161
188
 
162
189
  `calls.list()` returns one page when you `await` it. Iterate over it with `for await` to walk every page:
163
190
 
164
191
  ```ts
165
- const cutoff = "2026-09-01T00:00:00"
166
- for await (const call of dialgem.calls.list({ page_size: 50 })) {
167
- if (call.started_at < cutoff) break // newest first: the rest are older
168
- console.log(call.id, call.duration_s, call.analysis?.call_successful, call.transcript)
192
+ const since = Date.parse("2026-09-01T00:00:00Z")
193
+ for await (const call of dialgem.calls.list({ start_after: since, status: "completed", limit: 50 })) {
194
+ console.log(call.id, call.duration_ms, call.analysis?.call_summary, call.transcript_text)
169
195
  }
170
196
 
171
- const page = await dialgem.calls.list({ page_size: 50 })
197
+ const page = await dialgem.calls.list({ agent_id: agent.id, metadata: { crm_id: "42" }, limit: 50 })
172
198
  page.data // Call[]
173
- page.total // number
174
199
  if (page.hasNextPage()) await page.getNextPage()
175
200
  ```
176
201
 
177
- Pages are offset-based and ordered newest first. A call that starts mid-export shifts later pages by one, so de-duplicate on `id` if exactness matters.
202
+ Filters: `agent_id`, `status`, `direction`, `call_type`, `disconnection_reason`, `from_number`, `to_number`, `user_sentiment`, `call_successful`, `start_after` / `start_before` (epoch ms), `metadata`, `search` and `sort_order`. Pages are cursor-based, so walking them is stable while new calls arrive.
203
+
204
+ ### 6. Ship a change without touching live calls
205
+
206
+ The agent you edit is the **draft**. Publishing freezes it — configuration, functions and MCP servers — as a numbered version that never changes afterwards:
207
+
208
+ ```ts
209
+ const v1 = await dialgem.agents.versions.publish(agent.id, { title: "Refund flow" })
210
+ v1.version // 1
211
+
212
+ // Point production at the frozen copy, then keep editing the draft freely.
213
+ await dialgem.phoneNumbers.assign(number.id, { agent_id: agent.id, agent_version: v1.version })
214
+ await dialgem.agents.update(agent.id, { system_prompt: "…work in progress…" })
215
+
216
+ await dialgem.agents.versions.list(agent.id) // one page, newest first
217
+ await dialgem.agents.versions.retrieve(agent.id, 1) // with its snapshot
218
+
219
+ // Nothing prunes versions, so the list is paged. `listPage` keeps `has_more`.
220
+ const { versions, has_more } = await dialgem.agents.versions.listPage(agent.id, { limit: 20 })
221
+ ```
222
+
223
+ A number can also follow the newest publish instead of naming a version, so shipping moves it with nothing to update:
224
+
225
+ ```ts
226
+ await dialgem.phoneNumbers.assign(number.id, {
227
+ agent_id: agent.id,
228
+ agent_version: "latest_published",
229
+ })
230
+ ```
231
+
232
+ A version is also pinnable per call — `createPhoneCall({ …, agent_id, agent_version: 1 })` and `register({ …, agent_version: 1 })` — and the version a call ran comes back on the call record as `agent_version`, so a recording, its transcript and its analysis all name the configuration that produced them. Version `0` everywhere means the draft, which is what an unpinned call runs.
233
+
234
+ ### 7. Give an agent a cloned voice
235
+
236
+ ```ts
237
+ import { readFile } from "node:fs/promises"
238
+
239
+ const voice = await dialgem.voices.clone({
240
+ name: "Ada",
241
+ files: [new Blob([await readFile("./sample.mp3")], { type: "audio/mpeg" })],
242
+ // You are asserting that you hold the rights to this audio and have the
243
+ // speaker's consent. It is recorded against your API identity.
244
+ consent_confirmed: true,
245
+ })
246
+
247
+ await dialgem.agents.update(agent.id, { voice_id: voice.provider_voice_id })
248
+ ```
249
+
250
+ The new voice is private to your organization — it never appears in another
251
+ one's catalog, and `voices.list()` puts yours first. ElevenLabs (the default)
252
+ takes up to 25 files; **Cartesia takes exactly one** and refuses more rather
253
+ than quietly picking one.
254
+
255
+ `voices.delete(voice.id)` removes it at the provider and here. It refuses with a
256
+ 409 while an agent still uses it, rather than leaving that agent without a voice.
178
257
 
179
258
  ## Errors
180
259
 
@@ -223,11 +302,12 @@ The SDK never follows redirects. The API does not issue them, and following one
223
302
  | `agents.functions` | `list`, `create`, `update`, `delete`, `reorder` |
224
303
  | `agents.knowledge` | `list`, `create`, `delete` |
225
304
  | `agents.mcpServers` | `list`, `create`, `update`, `delete`, `test` |
226
- | `calls` | `list`, `retrieve`, `createPhoneCall`, `register` |
227
- | `phoneNumbers` | `create`, `list`, `update`, `assign`, `delete` |
228
- | `voices` | `list` |
305
+ | `agents.versions` | `publish`, `list`, `listPage`, `retrieve` |
306
+ | `calls` | `list`, `retrieve`, `update`, `delete`, `stop`, `createPhoneCall`, `register` |
307
+ | `phoneNumbers` | `create`, `list`, `retrieve`, `update`, `assign`, `delete` |
308
+ | `voices` | `list`, `clone`, `delete` |
229
309
  | `flowTemplates`, `promptTemplates` | `list`, `retrieve` |
230
- | `webhooks` | `verify`, `constructEvent` |
310
+ | `webhooks` | `verify`, `constructEvent`, `constructFunctionCall` |
231
311
 
232
312
  In-browser web calls are not part of this package.
233
313
 
@@ -245,7 +325,7 @@ pnpm sync:openapi --from https://api.dialgem.com/openapi.json
245
325
  pnpm gen:types
246
326
 
247
327
  # Against a running API, with an organization key:
248
- DIALGEM_API_KEY=zk_… DIALGEM_BASE_URL=http://localhost:8002 pnpm test:live
328
+ DIALGEM_API_KEY=zk_… pnpm test:live # runs against api.dialgem.com — use a test org's key
249
329
  ```
250
330
 
251
331
  Request-body types are generated from `openapi/openapi.json`. Response types in `src/types/` are written by hand, so a change to a response shape needs a matching edit there.