@signalhousellc/sdk 1.4.0 → 1.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalhousellc/sdk",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Signal House SDK for use with the Signal House platform",
5
5
  "type": "module",
6
6
  "main": "src/SignalHouseSDK.js",
@@ -17,14 +17,17 @@ description: >-
17
17
  **Deliveries to a webhook endpoint are signed.** Every endpoint created with `POST /webhook` gets a
18
18
  signing secret, returned in plaintext **exactly once**, as `signingSecret` in that create response.
19
19
  Every later read reports only `hasSigningSecret: true|false`. Store the secret at creation; nothing
20
- can hand it back, and a lost secret means creating a new endpoint.
20
+ can hand it back. If it is lost or exposed, rotate it: `POST /webhook/:id/rotate-secret` returns a new
21
+ secret once, and the old one keeps working for a grace window (`graceSeconds`, default 24h, max 7
22
+ days, `0` for none) so the receiver can switch over without dropping deliveries.
21
23
 
22
24
  A signed delivery carries:
23
25
 
24
26
  | Header | Value |
25
27
  |---|---|
26
- | `X-SignalHouse-Signature` | hex HMAC-SHA256, keyed with the signing secret, over `${timestamp}.${rawBody}` |
28
+ | `X-SignalHouse-Signature` | hex HMAC-SHA256, keyed with the signing secret, over `${timestamp}.${rawBody}`. During a rotation grace window it stays on the **previous** secret and switches when the window ends |
27
29
  | `X-SignalHouse-Timestamp` | the `timestamp` in that string: Unix time in **milliseconds** |
30
+ | `X-SignalHouse-Signature-V1` | `t=<ms>,v1=<hex>[,v1=<hex>]`: one signature per currently valid secret, newest first. During a rotation grace window it carries both the new and the previous secret's signature; accept a match on any |
28
31
  | `X-SignalHouse-Delivery-Id` | a UUID for this event to this endpoint, the same on every retry |
29
32
 
30
33
  A correct verifier:
@@ -37,12 +40,18 @@ A correct verifier:
37
40
  header is milliseconds, not seconds.
38
41
  4. **Rejects an unsigned request to an endpoint that has a secret.** Otherwise a forger simply
39
42
  leaves the headers off.
43
+ 5. **Prefers `X-SignalHouse-Signature-V1`**, so a secret rotation never breaks verification.
44
+
45
+ In Node, `verifyWebhookSignature` from `@signalhousellc/sdk/webhooks` does all of this and returns
46
+ `{ valid, reason }` without throwing. Other languages implement the five steps directly; the
47
+ Webhook Signing guide has a Python version.
40
48
 
41
49
  What is still **not** verifiable:
42
50
 
43
- - **Endpoints created before signing shipped (2026-09-03) are sent unsigned.** They report
44
- `hasSigningSecret: false`, and there is no way to add a secret to an existing endpoint. To get
45
- signing, create a new endpoint, store its secret, then delete the old one.
51
+ - **Endpoints created before signing shipped (2026-09-03) are sent unsigned** until they get a
52
+ secret. They report `hasSigningSecret: false`. Call `POST /webhook/:id/rotate-secret` once (or use
53
+ *Create signing secret* in the portal's webhook edit view) to issue their first secret; deliveries
54
+ are signed from then on.
46
55
  - **Per-number inbound URLs are never signed** (see below).
47
56
 
48
57
  For anything unsigned, the old rules still apply: do not treat the request as authenticated, and
@@ -50,9 +59,10 @@ never take an irreversible or financial action on webhook content alone. Read th
50
59
  state back from the API first. Do not put secrets in a webhook URL as a substitute for
51
60
  verification; URLs end up in proxy logs and support screenshots.
52
61
 
53
- The `authType` setting (`NONE`, `BASIC`, `API_KEY`, `OAUTH2`) is a different thing: it
54
- authenticates *Signal House to the receiver's endpoint*. The signature is what proves origin and
55
- integrity. `authType` is a second layer, not a substitute.
62
+ The `authType` setting (`NONE`, `BASIC`, `API_KEY`, `OAUTH2`, `HMAC`) chooses what *else* Signal
63
+ House sends to the receiver. Signing does not depend on it: every endpoint with a secret is signed
64
+ whatever `authType` says. `BASIC`/`API_KEY`/`OAUTH2` add credentials as a second layer; `HMAC` sends
65
+ none and declares that the receiver authenticates by the signature alone.
56
66
 
57
67
  ## Retry behaviour, and when an event is gone
58
68
 
@@ -100,7 +110,7 @@ So a correct receiver:
100
110
 
101
111
  Webhooks are created per group, and numbers can additionally carry their own inbound URL.
102
112
  `authType` picks how Signal House authenticates to the endpoint, and `credentials` is
103
- `{ key, secret }`, required whenever `authType` is not `NONE`.
113
+ `{ key, secret }`, required whenever `authType` is not `NONE` or `HMAC` (and rejected with those two).
104
114
 
105
115
  | authType | What is sent |
106
116
  |---|---|
@@ -108,10 +118,10 @@ Webhooks are created per group, and numbers can additionally carry their own inb
108
118
  | `BASIC` | HTTP Basic; `key` is the username, `secret` is the password |
109
119
  | `API_KEY` | `key` holds the key; `secret` optional |
110
120
  | `OAUTH2` | `secret` is the access token sent as the Authorization value; `key` is required at create but unused in outgoing headers |
121
+ | `HMAC` | no credentials and no Authorization header; the receiver verifies the signature headers. Rejected for an endpoint with no signing secret until it has one |
111
122
 
112
- Verify the signature on every endpoint that has one. On top of that, `NONE` is fine for an
113
- endpoint that is already unreachable from the public internet; for anything else a real `authType`
114
- is cheap defense in depth.
123
+ Verify the signature on every endpoint that has one. `HMAC` (or `NONE`) is fine for a receiver
124
+ that verifies signatures; `BASIC` or `API_KEY` on top is cheap defense in depth.
115
125
 
116
126
  **The table above does not cover per-number inbound URLs.** A number can carry
117
127
  `primaryInboundWebhookUrl` and `secondaryInboundWebhookUrl`, and inbound messages are posted to
@@ -124,7 +134,9 @@ signed group endpoint instead.
124
134
 
125
135
  ## Where to look it up
126
136
 
127
- - **Event types, payload shape and the signing scheme:** the Webhooks reference,
137
+ - **The signing scheme, verification and rotation:** the Webhook Signing guide,
138
+ https://app2.signalhouse.io/docs/webhook-signing.
139
+ - **Event types and payload shape:** the Webhooks reference,
128
140
  https://app2.signalhouse.io/docs/webhooks. The event list changes independently of this skill;
129
141
  read it there rather than reciting it here.
130
142
  - **Whether an event actually happened:** `GET /notification`
@@ -1,5 +1,9 @@
1
1
  import axios from "axios";
2
2
 
3
+ // Bounds every request made by this SDK. Chosen well above typical p99 latency so it only trips on
4
+ // a genuinely hung connection, not a slow-but-completing request.
5
+ const REQUEST_TIMEOUT_MS = 30000;
6
+
3
7
  /**
4
8
  * @typedef {Object} RequestOptions
5
9
  * @property {string} [token] - An optional bearer token to include in the request for authentication
@@ -100,8 +104,10 @@ export class SignalHouseSDK {
100
104
  * @returns {Function} A function to make API requests
101
105
  */
102
106
  _createClient(isMultipart = false) {
103
- // Create an Axios instance with the base URL and default headers
104
- const instance = axios.create({ baseURL: this.baseUrl, headers: isMultipart ? {} : { "Content-Type": "application/json" } });
107
+ // Create an Axios instance with the base URL and default headers. `timeout` bounds every
108
+ // request so a hung connection (server-side lock contention, a stalled upstream call) rejects
109
+ // instead of leaving the caller's promise unsettled forever with no error and no feedback.
110
+ const instance = axios.create({ baseURL: this.baseUrl, timeout: REQUEST_TIMEOUT_MS, headers: isMultipart ? {} : { "Content-Type": "application/json" } });
105
111
 
106
112
  // Add a request interceptor to include the API key in the Authorization header
107
113
  instance.interceptors.request.use((/** @type {import('axios').InternalAxiosRequestConfig & { token?: string, idempotencyKey?: string }} */ config) => {
@@ -122,8 +128,17 @@ export class SignalHouseSDK {
122
128
  return { success: true, data: response.data, status: response.status };
123
129
  }),
124
130
 
125
- // Non-2xx responses
131
+ // Non-2xx responses, plus timeouts and network failures (no `error.response` at all) - those
132
+ // get a clear message here rather than surfacing axios's internal error.message verbatim.
126
133
  (error) => {
134
+ if (!error.response) {
135
+ const isTimeout = error.code === "ECONNABORTED" || /timeout/i.test(error.message || "");
136
+ return {
137
+ success: false,
138
+ error: isTimeout ? "Request timed out. Please try again." : "Network error. Please check your connection and try again.",
139
+ status: null,
140
+ };
141
+ }
127
142
  return {
128
143
  success: false,
129
144
  error: error.response?.data?.message || error.response?.data || error.message || 500,
@@ -143,6 +143,24 @@
143
143
  * @property {boolean} [allowGreetingInterruption] - Whether a caller may talk over the greeting.
144
144
  * @property {string[]} [keyterms] - Words the transcriber is biased toward — names, jargon, unusual spellings.
145
145
  * @property {Object|null} [backgroundSound] - Ambient audio bed as `{ preset, volume }`; null = silence.
146
+ * @property {AgentWorkflow|null} [workflow] - Conversation workflow graph. Voice channel only — sending one on webchat or sms is a 400. null = no workflow.
147
+ */
148
+
149
+ /**
150
+ * A conversation workflow graph, in the voice runtime's own shape. Nodes and edges are keyed
151
+ * maps rather than arrays: an edge names its endpoints by node key, and a node's `edge_order`
152
+ * names its outgoing edges in the order their conditions are evaluated.
153
+ *
154
+ * Node bodies are deliberately not enumerated here. Node types are defined by the runtime and
155
+ * new ones appear without an SDK release, so the SDK passes them through untouched. The API
156
+ * validates the envelope — an entry point under the node key `start_node` typed `start` (the
157
+ * runtime looks it up by that exact key), no second `start` node, at most 200 nodes, and every
158
+ * edge endpoint naming a node that exists — and rejects a graph that fails any of those.
159
+ * @typedef {Object} AgentWorkflow
160
+ * @property {Object<string, Object>} nodes - Nodes keyed by node id. Each carries at least `type` and `position` ({ x, y }).
161
+ * @property {Object<string, Object>} edges - Edges keyed by edge id. Each carries `source` and `target` node ids.
162
+ * @property {Object<string, Object>} [subgraphs] - Nested subgraphs keyed by subgraph id.
163
+ * @property {boolean} [prevent_subagent_loops] - Stop a subagent handing control back to one that already ran.
146
164
  */
147
165
 
148
166
  export class Agents {
@@ -234,14 +252,16 @@ export class Agents {
234
252
  }
235
253
 
236
254
  /**
237
- * Delete (inactivate) an agent profile by its id.
255
+ * Permanently delete an agent profile by its id.
256
+ *
257
+ * Permanently deletes the agent, its channel settings, and any tools and knowledge scoped to it, plus their provider-side counterparts. Tools and knowledge shared across the account are untouched. Conversation records survive — a transcript outlives the agent that produced it.
238
258
  * @async
239
259
  * @roles api, admin, developer
240
260
  * @param {Object} params - The parameters for deleting an agent profile.
241
261
  * @param {string} params.id - The id of the agent profile to delete.
242
262
  * @param {RequestOptions} [params.options] - Additional options for the request.
243
263
  * @throws {Error} Throws an error if the id parameter is missing.
244
- * @returns {Promise<SdkResponse<any>>} - The inactivated agent profile returned from the server.
264
+ * @returns {Promise<Object>} - The deleted agent profile, as it was.
245
265
  */
246
266
  async deleteAgentProfile({ id, options = {} }) {
247
267
  this.client._require({ id });
@@ -36,6 +36,7 @@
36
36
  * @property {string} privacyPolicyLink - A URL linking to the privacy policy for the campaign (required, must be at most 2048 characters)
37
37
  * @property {string} termsAndConditionsLink - A URL linking to the terms and conditions for the campaign (required, must be at most 2048 characters)
38
38
  * @property {string} [embeddedLinkSample] - A sample of the embedded link used in the campaign (optional, must be at most 255 characters)
39
+ * @property {string[]} [sampleMediaIds] - Up to 5 mediaIds from uploadSampleMedia: sample MMS media shown to carrier and TCR reviewers (10DLC only)
39
40
  * @property {Array<string>} phoneNumbers - An array of phone numbers associated with the campaign (required, each number must be at least 10 digits and contain only digits, a maximum of 5000 numbers is allowed)
40
41
  */
41
42
 
@@ -70,6 +71,7 @@
70
71
  * @property {string} [privacyPolicyLink] - A URL linking to the privacy policy for the campaign (optional, must be at most 2048 characters if provided)
71
72
  * @property {string} [termsAndConditionsLink] - A URL linking to the terms and conditions for the campaign (optional, must be at most 2048 characters if provided)
72
73
  * @property {string} [embeddedLinkSample] - A sample of the embedded link used in the campaign (optional, must be at most 255 characters if provided)
74
+ * @property {string[]} [sampleMediaIds] - Up to 5 mediaIds from uploadSampleMedia: sample MMS media shown to carrier and TCR reviewers (10DLC only)
73
75
  * @property {Object} [tollFree] - Toll-Free editable fields, used when updating a Toll-Free campaign. Mirrors the CreateTollFreeCampaignData `tollFree` object with every field optional (useCase, messageVolume, programSummary (40-500), exampleMessage (≤1000), customerCareEmail, optInImageURLs, optIns, multiNumberReason (≤500), useCases, channels, landingId). Phone numbers cannot be changed via update — Toll-Free numbers are locked to their campaign.
74
76
  */
75
77
 
@@ -529,6 +531,43 @@ export class Campaigns {
529
531
  return this.client(`/campaign/short-code/artifact/${safeArtifactId}`, { method: 'GET', ...options });
530
532
  }
531
533
 
534
+ /**
535
+ * Upload a sample media file (JPEG, PNG, GIF, MP4 or MP3, up to 5MB) for a 10DLC campaign. Reference the
536
+ * returned mediaId in sampleMediaIds on campaign create or update; it is attached to the TCR campaign for
537
+ * carrier review
538
+ * @async
539
+ * @roles api, admin, developer, user
540
+ * @param {Object} params - The parameters for uploading the sample media
541
+ * @param {Blob|File} params.file - The media file to upload, provided as a Blob/File
542
+ * @param {string} [params.groupId] - The group the file belongs to. Required with a staff (signalhouse_*) key, which has no active group; otherwise optional and must be your own group
543
+ * @param {RequestOptions} [params.options] - Additional options for the request
544
+ * @throws {Error} Throws an error if the file parameter is missing
545
+ * @returns {Promise<SdkResponse<any>>} The response from the server containing the mediaId, fileName, mimeType and size
546
+ */
547
+ async uploadSampleMedia({ file, groupId, options = {} }) {
548
+ this.client._require({ file });
549
+ const formData = new FormData();
550
+ if (groupId) formData.append('groupId', groupId);
551
+ formData.append('file', file);
552
+ return this.multipartClient(`/campaign/sample-media`, { method: 'POST', body: formData, ...options });
553
+ }
554
+
555
+ /**
556
+ * Download a campaign sample media file your group owns
557
+ * @async
558
+ * @roles api, admin, developer, user
559
+ * @param {Object} params - The parameters for downloading the sample media
560
+ * @param {string} params.mediaId - The mediaId returned by uploadSampleMedia
561
+ * @param {RequestOptions} [params.options] - Additional options for the request
562
+ * @throws {Error} Throws an error if the mediaId parameter is missing
563
+ * @returns {Promise<SdkResponse<any>>} The response from the server containing the file bytes
564
+ */
565
+ async readSampleMedia({ mediaId, options = {} }) {
566
+ this.client._require({ mediaId });
567
+ const safeMediaId = encodeURIComponent(mediaId);
568
+ return this.client(`/campaign/sample-media/${safeMediaId}`, { method: 'GET', ...options });
569
+ }
570
+
532
571
  /**
533
572
  * Upload an opt-in proof image to be hosted on-platform, returning a publicly-fetchable URL suitable for a
534
573
  * Toll-Free campaign's optInImageURLs or a Short Code campaign's shortCode.screenshotUrl
@@ -300,11 +300,12 @@ export class Messages {
300
300
  * @param {("day"|"exact")} [params.dateBounds] - Which contract binds startDate/endDate. "day": each bound is widened to its whole UTC day. "exact": a timestamp is bound as the instant it names and a bare date is its whole UTC day. Omitted: "day".
301
301
  * @param {string|string[]} [params.region] - ISO-2 country filter: US, GB or CA (any case of UK/GB/GBR/United Kingdom is accepted for GB); one value, an array, or a comma-separated string; omitted = every region. Only accepted where the environment serves hourly analytics; elsewhere any value is rejected with a 400.
302
302
  * @param {Object[]|string} [params.regionScopes] - Array of OR-ed branches `{ region, channels, brandId?, campaignId?, phoneNumber? }` (channels among tenDLC, virtualLongCode, tollFree, shortCode, p2p); `[]` matches nothing. Sent as JSON unless already a string. Only accepted where the environment serves hourly analytics; elsewhere any value is rejected with a 400.
303
+ * @param {string} [params.errorCode] - Narrow the breakdown to a single failure reason: a numeric Infobip carrier code (as a string, e.g. "607"), the Signal House campaign-opt-out code "OUT", or a no-code failure class (other, moderation, att_limit, p2p_stop, campaign_opt_out, dnc_blocked, bind_hold_expired). Omitted = every reason.
303
304
  * @param {RequestOptions} [params.options]
304
305
  * @returns {Promise<SdkResponse<any>>} - { rows, totalCount, totalErrors: { sms, mms, p2p, all }, page, limit }
305
306
  */
306
- async getAnalyticsByErrorCode({ groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds, options = {} }) {
307
- const filters = { groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds };
307
+ async getAnalyticsByErrorCode({ groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds, errorCode, options = {} }) {
308
+ const filters = { groupId, subgroupId, brandId, campaignId, registrationId, phoneNumber, carrier, carrierFamily, startDate, endDate, page, limit, channel, region, regionScopes, dateBounds, errorCode };
308
309
  const queryString = this.client._getQueryString(withRegionScopes(filters));
309
310
  return this.client(`/message/analytics/by-error-code${queryString}`, { method: "GET", ...options });
310
311
  }
@@ -31,6 +31,7 @@
31
31
  * @property {string} role - The role of the service user: "api" for a customer API key (required)
32
32
  * @property {string[]} [allowedIps] - Only accept this API key from these IPv4/IPv6 addresses or CIDR ranges (max 100). Omit or pass [] for no IP restriction. Customer API keys only.
33
33
  * @property {string[]} [allowedCountries] - Only accept this API key from these countries, as ISO 3166-1 alpha-2 codes such as "US" (max 250). Omit or pass [] for no country restriction. Customer API keys only.
34
+ * @property {string[]} [allowedDestinationCountries] - Only let this API key send messages to these countries, as ISO 3166-1 alpha-2 codes such as "US" (max 250). Sends to any other country are rejected with a 403 DESTINATION_COUNTRY_NOT_ALLOWED. Omit or pass [] for any country. Customer API keys only.
34
35
  */
35
36
 
36
37
  /**
@@ -51,6 +52,7 @@
51
52
  * @property {string} [status] - The status of the user (e.g., "active", "inactive")
52
53
  * @property {string[]} [allowedIps] - Customer API keys only: replace the key's allowed IPv4/IPv6 addresses or CIDR ranges ([] removes the restriction). Requires an interactive admin or developer session; an API key cannot change its own restrictions.
53
54
  * @property {string[]} [allowedCountries] - Customer API keys only: replace the key's allowed ISO 3166-1 alpha-2 country codes ([] removes the restriction). Requires an interactive admin or developer session.
55
+ * @property {string[]} [allowedDestinationCountries] - Customer API keys only: replace the countries the key may send messages to ([] allows any country). Requires an interactive admin or developer session.
54
56
  */
55
57
 
56
58
  export class Users {
@@ -40,7 +40,9 @@ export class Webhooks {
40
40
  * same event, for receiver-side dedup), plus `X-SignalHouse-Signature-V1` (`t=<ms>,v1=<hex>[,v1=<hex>]`,
41
41
  * every currently valid signature). Store this secret now: it is never returned again by any read
42
42
  * (SHGHL-2944). If it's lost, call `rotateWebhookSecret`. Verify deliveries with
43
- * `verifyWebhookSignature` from `@signalhousellc/sdk/webhooks` (Node only).
43
+ * `verifyWebhookSignature` from `@signalhousellc/sdk/webhooks` (Node only). `webhookData.authType` is one of
44
+ * `NONE` (default), `BASIC`, `API_KEY`, `OAUTH2` or `HMAC`; `HMAC` (SHGHL-3595) sends no credentials, so the
45
+ * receiver authenticates by the signature alone, and must be sent without `credentials` or `apiHeaderPrefix`.
44
46
  * @async
45
47
  * @roles api, admin, developer
46
48
  * @param {Object} params - The parameters for creating a webhook
@@ -58,7 +60,9 @@ export class Webhooks {
58
60
  * Update an existing webhook with the specified data. `updateData` may include `name`, `url`,
59
61
  * `subscribedEvents` and (SHGHL-3486) `authType`, `apiHeaderPrefix` and `credentials` (`{ key, secret? }`)
60
62
  * to change the endpoint's own credentials in place: credentials are required when switching to an auth
61
- * type that needs them, and omitted credentials keep the stored ones. Like every read of a webhook, the
63
+ * type that needs them, and omitted credentials keep the stored ones. Switching to `NONE` or `HMAC` clears
64
+ * the stored credentials; `HMAC` is rejected for an endpoint with no signing secret (call
65
+ * `rotateWebhookSecret` first). Like every read of a webhook, the
62
66
  * response carries `hasSigningSecret` and `hasCredentials` (booleans), never a secret or credential.
63
67
  * @async
64
68
  * @roles api, admin, developer
@@ -9,12 +9,13 @@
9
9
  * @property {string} name - Profile name (required).
10
10
  * @property {string[]} subgroupIds - Subgroups this profile spans (required, at least one).
11
11
  * @property {string} [region] - Region/POP label.
12
- * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"} [routeAction] - Inbound call action (default "FORWARD"). Deliver the call to a PSTN number (FORWARD), ring the profile's subgroup registered softphones (WEBRTC), send to a SIP trunk (SIP_TRUNK), ring a registered SIP endpoint (SIP_PROFILE), or hand the call to your programmable call-control webhook that returns SHML (CALL_CONTROL).
12
+ * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"|"AI"} [routeAction] - Inbound call action (default "FORWARD"). Deliver the call to a PSTN number (FORWARD), ring the profile's subgroup registered softphones (WEBRTC), send to a SIP trunk (SIP_TRUNK), ring a registered SIP endpoint (SIP_PROFILE), hand the call to your programmable call-control webhook that returns SHML (CALL_CONTROL), or answer it with an AI agent (AI).
13
13
  * @property {string} [forwardToE164] - Destination number, required when routeAction is "FORWARD".
14
14
  * @property {number} [forwardAfterSeconds] - Ring seconds before the action fires.
15
15
  * @property {string} [routeSipTrunkId] - SIP trunk id, required when routeAction is "SIP_TRUNK".
16
16
  * @property {string} [routeSipProfileId] - SIP endpoint id, required when routeAction is "SIP_PROFILE".
17
17
  * @property {string} [webhookUrl] - Your inbound voice webhook (SHML program URL), required when routeAction is "CALL_CONTROL". Inbound calls to this profile's numbers are driven by the SHML your webhook returns.
18
+ * @property {string} [routeAgentProfileId] - AI agent profile id (AGP…), required when routeAction is "AI". The agent's prompt, voice, model and tools are read when the call lands, so editing the agent takes effect on the next call with nothing to re-save here.
18
19
  * @property {boolean} [recordingEnabled] - Record calls on this profile.
19
20
  * @property {boolean} [enabled] - Active state (default true).
20
21
  */
@@ -24,12 +25,13 @@
24
25
  * @property {string} [name] - Profile name.
25
26
  * @property {string[]} [subgroupIds] - Subgroups this profile spans.
26
27
  * @property {string|null} [region] - Region/POP label.
27
- * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"} [routeAction] - Inbound call action (FORWARD | WEBRTC | SIP_TRUNK | SIP_PROFILE | CALL_CONTROL).
28
+ * @property {"FORWARD"|"WEBRTC"|"SIP_TRUNK"|"SIP_PROFILE"|"CALL_CONTROL"|"AI"} [routeAction] - Inbound call action (FORWARD | WEBRTC | SIP_TRUNK | SIP_PROFILE | CALL_CONTROL | AI).
28
29
  * @property {string|null} [forwardToE164] - FORWARD destination.
29
30
  * @property {number|null} [forwardAfterSeconds] - Ring seconds before the action fires.
30
31
  * @property {string|null} [routeSipTrunkId] - SIP trunk id (required when routeAction is "SIP_TRUNK").
31
32
  * @property {string|null} [routeSipProfileId] - SIP endpoint id (required when routeAction is "SIP_PROFILE").
32
33
  * @property {string|null} [webhookUrl] - Inbound voice webhook / SHML program URL (required when routeAction is "CALL_CONTROL"). Pass null to clear.
34
+ * @property {string|null} [routeAgentProfileId] - AI agent profile id (AGP…), required when routeAction is "AI". Pass null to clear.
33
35
  * @property {boolean} [recordingEnabled] - Record calls on this profile.
34
36
  * @property {boolean} [enabled] - Active state.
35
37
  */