@vuevox/sdk 0.8.0 → 0.9.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/README.md CHANGED
@@ -35,6 +35,8 @@ calls:write
35
35
  leads:read
36
36
  leads:write
37
37
  lead_custom_fields:manage
38
+ webhooks:read
39
+ webhooks:write
38
40
  ```
39
41
 
40
42
  The token request can only request scopes that were granted to that API client.
@@ -117,6 +119,12 @@ vuevox.leads.paginate();
117
119
  vuevox.leadCustomFields.list();
118
120
  vuevox.leadCustomFields.create({ key: "crm_stage", label: "CRM Stage", type: "select", options: ["new", "qualified"] });
119
121
  vuevox.leadCustomFields.update("crm_stage", { label: "CRM Stage" });
122
+ vuevox.webhooks.endpoints.list();
123
+ vuevox.webhooks.endpoints.create({ url: "https://example.com/vuevox/webhooks", events: ["analysis.completed", "analysis.failed"] });
124
+ vuevox.webhooks.endpoints.update("endpoint-id", { isActive: false });
125
+ vuevox.webhooks.endpoints.rotateSecret("endpoint-id");
126
+ vuevox.webhooks.endpoints.test("endpoint-id");
127
+ vuevox.webhooks.endpoints.delete("endpoint-id");
120
128
  vuevox.raw.GET("/v1/hello", { headers: { Authorization: `Bearer ${await vuevox.getAccessToken()}` } });
121
129
  ```
122
130
 
@@ -164,7 +172,7 @@ for await (const call of vuevox.calls.paginate({ limit: 50 })) {
164
172
  }
165
173
  ```
166
174
 
167
- Pagination helpers are available for `spaces`, `agents`, `calls`, and `leads`.
175
+ Pagination helpers are available for `spaces`, `agents`, `calls`, `leads`, and `webhooks.endpoints`.
168
176
 
169
177
  ## Methods
170
178
 
@@ -346,8 +354,9 @@ Options: `UploadCallInput`
346
354
  | `lead.email` | `string \| null` | Optional lead email. |
347
355
  | `lead.phone` | `string \| null` | Optional lead phone. |
348
356
  | `lead.customFields` | `Record<string, unknown>` | Optional organization-defined lead custom fields. |
349
- | `audio.file` | `Blob` | Required audio file blob. In Node, use `new Blob([buffer], { type: "audio/mpeg" })`. |
357
+ | `audio.file` | `Blob \| ArrayBuffer \| Uint8Array` | Required audio file data. In Node, a `Buffer` from `readFile()` can be passed directly. |
350
358
  | `audio.filename` | `string` | Optional filename sent in multipart upload. |
359
+ | `audio.contentType` | `string` | Optional MIME type, for example `audio/mpeg`. |
351
360
 
352
361
  ```ts
353
362
  import { readFile } from "node:fs/promises";
@@ -373,8 +382,9 @@ const response = await vuevox.calls.upload({
373
382
  },
374
383
  },
375
384
  audio: {
376
- file: new Blob([buffer], { type: "audio/mpeg" }),
385
+ file: buffer,
377
386
  filename: "call.mp3",
387
+ contentType: "audio/mpeg",
378
388
  },
379
389
  });
380
390
 
@@ -601,6 +611,126 @@ await vuevox.leadCustomFields.update("crm_stage", {
601
611
 
602
612
  Returns: `Promise<VueVoxApiResponse<LeadCustomFieldResponse>>`.
603
613
 
614
+ ### `vuevox.webhooks.endpoints.list(options?)`
615
+
616
+ Lists webhook endpoints for the authenticated API client.
617
+
618
+ Required scope: `webhooks:read`.
619
+
620
+ Options: `ListWebhookEndpointsOptions`
621
+
622
+ ```ts
623
+ const response = await vuevox.webhooks.endpoints.list({ limit: 50 });
624
+ console.log(response.data.data);
625
+ ```
626
+
627
+ Returns: `Promise<VueVoxApiResponse<WebhookEndpointsListResponse>>`.
628
+
629
+ ### `vuevox.webhooks.endpoints.create(input)`
630
+
631
+ Creates a webhook endpoint. The signing secret is returned only once in this response.
632
+
633
+ Required scope: `webhooks:write`.
634
+
635
+ Input: `WebhookEndpointCreateRequest`
636
+
637
+ ```ts
638
+ const response = await vuevox.webhooks.endpoints.create({
639
+ url: "https://example.com/vuevox/webhooks",
640
+ events: ["analysis.completed", "analysis.failed"],
641
+ });
642
+
643
+ console.log(response.data.data.id, response.data.data.secret);
644
+ ```
645
+
646
+ Returns: `Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>`.
647
+
648
+ ### `vuevox.webhooks.endpoints.update(endpointId, input)`
649
+
650
+ Updates a webhook endpoint URL, event subscriptions, or active state.
651
+
652
+ Required scope: `webhooks:write`.
653
+
654
+ Input: `WebhookEndpointUpdateRequest`
655
+
656
+ ```ts
657
+ const response = await vuevox.webhooks.endpoints.update("endpoint-id", {
658
+ events: ["analysis.completed"],
659
+ isActive: true,
660
+ });
661
+ ```
662
+
663
+ Returns: `Promise<VueVoxApiResponse<WebhookEndpointResponse>>`.
664
+
665
+ ### `vuevox.webhooks.endpoints.delete(endpointId)`
666
+
667
+ Deletes a webhook endpoint.
668
+
669
+ Required scope: `webhooks:write`.
670
+
671
+ ```ts
672
+ await vuevox.webhooks.endpoints.delete("endpoint-id");
673
+ ```
674
+
675
+ Returns: `Promise<VueVoxApiResponse<null>>`.
676
+
677
+ ### `vuevox.webhooks.endpoints.rotateSecret(endpointId)`
678
+
679
+ Rotates the endpoint signing secret. The new secret is returned only once.
680
+
681
+ Required scope: `webhooks:write`.
682
+
683
+ ```ts
684
+ const response = await vuevox.webhooks.endpoints.rotateSecret("endpoint-id");
685
+ console.log(response.data.data.secret);
686
+ ```
687
+
688
+ Returns: `Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>`.
689
+
690
+ ### `vuevox.webhooks.endpoints.test(endpointId)`
691
+
692
+ Queues a `webhook.test` delivery to validate endpoint reachability and signature verification.
693
+
694
+ Required scope: `webhooks:write`.
695
+
696
+ ```ts
697
+ const response = await vuevox.webhooks.endpoints.test("endpoint-id");
698
+ console.log(response.data.data.eventType, response.data.data.status);
699
+ ```
700
+
701
+ Returns: `Promise<VueVoxApiResponse<WebhookTestResponse>>`.
702
+
703
+ ### `vuevox.webhooks.endpoints.paginate(options?)`
704
+
705
+ Iterates webhook endpoints across all pages.
706
+
707
+ Required scope: `webhooks:read`.
708
+
709
+ ```ts
710
+ for await (const endpoint of vuevox.webhooks.endpoints.paginate({ limit: 50 })) {
711
+ console.log(endpoint.id, endpoint.events);
712
+ }
713
+ ```
714
+
715
+ Returns: `AsyncGenerator<WebhookEndpoint>`.
716
+
717
+ ### `verifyVueVoxWebhookSignature(input)`
718
+
719
+ Verifies a webhook HMAC signature. Pass the raw request body string exactly as received.
720
+
721
+ ```ts
722
+ import { verifyVueVoxWebhookSignature } from "@vuevox/sdk";
723
+
724
+ const valid = await verifyVueVoxWebhookSignature({
725
+ body: rawBody,
726
+ secret: process.env.VUEVOX_WEBHOOK_SECRET!,
727
+ signature: request.headers.get("VueVox-Signature") ?? "",
728
+ timestamp: request.headers.get("VueVox-Timestamp") ?? "",
729
+ });
730
+ ```
731
+
732
+ Returns: `Promise<boolean>`.
733
+
604
734
  ## Lower-Level Calls
605
735
 
606
736
  For advanced integrations, `raw` exposes a typed lower-level OpenAPI client. You must attach authorization yourself.
@@ -654,6 +784,7 @@ invalid_scope
654
784
  insufficient_scope
655
785
  rate_limited
656
786
  invalid_request
787
+ internal_error
657
788
  call_not_found
658
789
  call_external_id_conflict
659
790
  analysis_already_queued
@@ -742,15 +873,25 @@ import type {
742
873
  ListLeadCustomFieldsOptions,
743
874
  ListLeadsOptions,
744
875
  ListSpacesOptions,
876
+ ListWebhookEndpointsOptions,
745
877
  Space,
746
878
  SpacesListResponse,
747
879
  UploadCallInput,
880
+ VerifyWebhookSignatureInput,
748
881
  VueVoxApiResponse,
749
882
  VueVoxClientOptions,
750
883
  VueVoxErrorResponse,
751
884
  VueVoxResponseEvent,
752
885
  VueVoxResponseMetadata,
753
886
  WaitForAnalysisOptions,
887
+ WebhookEndpoint,
888
+ WebhookEndpointCreateRequest,
889
+ WebhookEndpointResponse,
890
+ WebhookEndpointSecretResponse,
891
+ WebhookEndpointUpdateRequest,
892
+ WebhookEndpointsListResponse,
893
+ WebhookEventPayload,
894
+ WebhookTestResponse,
754
895
  } from "@vuevox/sdk";
755
896
  ```
756
897
 
package/dist/client.d.ts CHANGED
@@ -12,6 +12,14 @@ export type LeadCustomFieldsListResponse = components["schemas"]["LeadCustomFiel
12
12
  export type LeadCustomFieldResponse = components["schemas"]["LeadCustomFieldResponse"];
13
13
  export type LeadCustomFieldCreateRequest = components["schemas"]["LeadCustomFieldCreateRequest"];
14
14
  export type LeadCustomFieldUpdateRequest = components["schemas"]["LeadCustomFieldUpdateRequest"];
15
+ export type WebhookEndpoint = components["schemas"]["WebhookEndpoint"];
16
+ export type WebhookEndpointCreateRequest = components["schemas"]["WebhookEndpointCreateRequest"];
17
+ export type WebhookEndpointUpdateRequest = components["schemas"]["WebhookEndpointUpdateRequest"];
18
+ export type WebhookEndpointResponse = components["schemas"]["WebhookEndpointResponse"];
19
+ export type WebhookEndpointSecretResponse = components["schemas"]["WebhookEndpointSecretResponse"];
20
+ export type WebhookEndpointsListResponse = components["schemas"]["WebhookEndpointsListResponse"];
21
+ export type WebhookTestResponse = components["schemas"]["WebhookTestResponse"];
22
+ export type WebhookEventPayload = components["schemas"]["WebhookEventPayload"];
15
23
  export type LeadUpsertRequest = components["schemas"]["LeadUpsertRequest"];
16
24
  export type LeadUpdateRequest = components["schemas"]["LeadUpdateRequest"];
17
25
  export type Space = components["schemas"]["Space"];
@@ -45,8 +53,9 @@ export interface ListCallsOptions extends ListSpacesOptions {
45
53
  export interface UploadCallInput extends CallUploadMetadata {
46
54
  idempotencyKey: string;
47
55
  audio: {
48
- file: Blob;
56
+ file: Blob | ArrayBuffer | Uint8Array;
49
57
  filename?: string;
58
+ contentType?: string;
50
59
  };
51
60
  }
52
61
  export interface WaitForAnalysisOptions {
@@ -65,6 +74,16 @@ export interface ListLeadsOptions extends ListSpacesOptions {
65
74
  export interface ListLeadCustomFieldsOptions {
66
75
  includeArchived?: boolean;
67
76
  }
77
+ export interface ListWebhookEndpointsOptions extends ListSpacesOptions {
78
+ }
79
+ export interface VerifyWebhookSignatureInput {
80
+ body: string;
81
+ secret: string;
82
+ signature: string;
83
+ timestamp: string;
84
+ toleranceSeconds?: number;
85
+ now?: number;
86
+ }
68
87
  export interface VueVoxResponseMetadata {
69
88
  requestId?: string;
70
89
  status: number;
@@ -158,5 +177,25 @@ export declare function createVueVoxClient(options: VueVoxClientOptions): {
158
177
  create: (input: LeadCustomFieldCreateRequest) => Promise<VueVoxApiResponse<LeadCustomFieldResponse>>;
159
178
  update: (key: string, input: LeadCustomFieldUpdateRequest) => Promise<VueVoxApiResponse<LeadCustomFieldResponse>>;
160
179
  };
180
+ webhooks: {
181
+ endpoints: {
182
+ list: (listOptions?: ListWebhookEndpointsOptions) => Promise<VueVoxApiResponse<WebhookEndpointsListResponse>>;
183
+ create: (input: WebhookEndpointCreateRequest) => Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>;
184
+ update: (endpointId: string, input: WebhookEndpointUpdateRequest) => Promise<VueVoxApiResponse<WebhookEndpointResponse>>;
185
+ delete: (endpointId: string) => Promise<VueVoxApiResponse<null>>;
186
+ rotateSecret: (endpointId: string) => Promise<VueVoxApiResponse<WebhookEndpointSecretResponse>>;
187
+ test: (endpointId: string) => Promise<VueVoxApiResponse<WebhookTestResponse>>;
188
+ paginate: (listOptions?: ListWebhookEndpointsOptions) => AsyncGenerator<{
189
+ id: string;
190
+ url: string;
191
+ events: components["schemas"]["WebhookEventType"][];
192
+ isActive: boolean;
193
+ lastDeliveredAt: string | null;
194
+ createdAt: string;
195
+ updatedAt: string;
196
+ }, any, any>;
197
+ };
198
+ };
161
199
  raw: import("openapi-fetch").Client<paths, `${string}/${string}`>;
162
200
  };
201
+ export declare function verifyVueVoxWebhookSignature(input: VerifyWebhookSignatureInput): Promise<boolean>;
package/dist/client.js CHANGED
@@ -50,7 +50,7 @@ export function createVueVoxClient(options) {
50
50
  const { audio, idempotencyKey, ...metadata } = input;
51
51
  const formData = new FormData();
52
52
  formData.set("metadata", JSON.stringify(metadata));
53
- formData.set("audioFile", audio.file, audio.filename ?? "call-audio");
53
+ formData.set("audioFile", toUploadBlob(audio.file, audio.contentType), audio.filename ?? "call-audio");
54
54
  return apiMultipart("POST", "/v1/calls", formData, {
55
55
  "Idempotency-Key": idempotencyKey,
56
56
  });
@@ -95,6 +95,24 @@ export function createVueVoxClient(options) {
95
95
  async function updateLeadCustomField(key, input) {
96
96
  return apiJson("PATCH", `/v1/lead-custom-fields/${encodeURIComponent(key)}`, input);
97
97
  }
98
+ async function listWebhookEndpoints(listOptions = {}) {
99
+ return apiGet("/v1/webhooks/endpoints", listOptions);
100
+ }
101
+ async function createWebhookEndpoint(input) {
102
+ return apiJson("POST", "/v1/webhooks/endpoints", input);
103
+ }
104
+ async function updateWebhookEndpoint(endpointId, input) {
105
+ return apiJson("PATCH", `/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}`, input);
106
+ }
107
+ async function deleteWebhookEndpoint(endpointId) {
108
+ return apiDelete(`/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}`);
109
+ }
110
+ async function rotateWebhookEndpointSecret(endpointId) {
111
+ return apiJson("POST", `/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}/rotate-secret`, {});
112
+ }
113
+ async function testWebhookEndpoint(endpointId) {
114
+ return apiJson("POST", `/v1/webhooks/endpoints/${encodeURIComponent(endpointId)}/test`, {});
115
+ }
98
116
  async function apiGet(path, query) {
99
117
  const accessToken = await getAccessToken();
100
118
  const result = await requestJson("GET", path, {
@@ -118,6 +136,24 @@ export function createVueVoxClient(options) {
118
136
  });
119
137
  return withMetadata(result.data, result.response, result.requestId);
120
138
  }
139
+ async function apiDelete(path) {
140
+ const accessToken = await getAccessToken();
141
+ const response = await fetchFn(buildUrl(baseUrl, path, undefined), {
142
+ method: "DELETE",
143
+ headers: {
144
+ Authorization: `Bearer ${accessToken}`,
145
+ },
146
+ });
147
+ const body = await parseJson(response);
148
+ const requestId = getRequestId(response, body);
149
+ const retryAfter = retryAfterSeconds(response);
150
+ notifyResponse(options, "DELETE", path, response, requestId, retryAfter);
151
+ if (response.ok) {
152
+ return withMetadata(null, response, requestId);
153
+ }
154
+ const error = isErrorResponse(body) ? body.error : null;
155
+ throw new VueVoxApiError(response.status, error?.code ?? "api_request_failed", error?.message ?? "VueVox API request failed.", isErrorResponse(body) ? body : undefined, requestId, retryAfter);
156
+ }
121
157
  async function apiMultipart(method, path, body, headers) {
122
158
  const accessToken = await getAccessToken();
123
159
  const result = await requestJson(method, path, {
@@ -190,9 +226,34 @@ export function createVueVoxClient(options) {
190
226
  create: createLeadCustomField,
191
227
  update: updateLeadCustomField,
192
228
  },
229
+ webhooks: {
230
+ endpoints: {
231
+ list: listWebhookEndpoints,
232
+ create: createWebhookEndpoint,
233
+ update: updateWebhookEndpoint,
234
+ delete: deleteWebhookEndpoint,
235
+ rotateSecret: rotateWebhookEndpointSecret,
236
+ test: testWebhookEndpoint,
237
+ paginate: (listOptions = {}) => paginate(listWebhookEndpoints, listOptions),
238
+ },
239
+ },
193
240
  raw,
194
241
  };
195
242
  }
243
+ export async function verifyVueVoxWebhookSignature(input) {
244
+ const toleranceSeconds = input.toleranceSeconds ?? 300;
245
+ const now = input.now ?? Date.now();
246
+ const timestampSeconds = Number(input.timestamp);
247
+ if (!Number.isFinite(timestampSeconds)) {
248
+ return false;
249
+ }
250
+ if (Math.abs(Math.floor(now / 1000) - timestampSeconds) > toleranceSeconds) {
251
+ return false;
252
+ }
253
+ const expected = await hmacSha256Hex(input.secret, `${input.timestamp}.${input.body}`);
254
+ const provided = input.signature.startsWith("v1=") ? input.signature.slice(3) : input.signature;
255
+ return constantTimeEqual(expected, provided);
256
+ }
196
257
  function formatScope(scope) {
197
258
  if (Array.isArray(scope)) {
198
259
  return scope.join(" ");
@@ -256,6 +317,12 @@ function withMetadata(data, response, requestId) {
256
317
  status: response.status,
257
318
  };
258
319
  }
320
+ function toUploadBlob(file, contentType) {
321
+ if (file instanceof Blob) {
322
+ return contentType && file.type !== contentType ? new Blob([file], { type: contentType }) : file;
323
+ }
324
+ return new Blob([file], { type: contentType });
325
+ }
259
326
  function notifyResponse(options, method, path, response, requestId, retryAfter) {
260
327
  options.onResponse?.({
261
328
  method,
@@ -293,3 +360,18 @@ function retryAfterSeconds(response) {
293
360
  function sleep(ms) {
294
361
  return new Promise((resolve) => setTimeout(resolve, ms));
295
362
  }
363
+ async function hmacSha256Hex(secret, value) {
364
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
365
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value));
366
+ return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
367
+ }
368
+ function constantTimeEqual(left, right) {
369
+ if (left.length !== right.length) {
370
+ return false;
371
+ }
372
+ let result = 0;
373
+ for (let index = 0; index < left.length; index++) {
374
+ result |= left.charCodeAt(index) ^ right.charCodeAt(index);
375
+ }
376
+ return result === 0;
377
+ }