@signalhousellc/sdk 1.0.68 → 1.1.0

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
@@ -55,13 +55,3 @@ For full API reference and advanced usage, visit https://api.signalhouse.io
55
55
 
56
56
  License
57
57
  ISC © Signal House
58
-
59
-
60
- ### Geographic availability search
61
-
62
- The availability path supports optional `city` as a case-insensitive prefix within an explicit `country` (`CA` or `US`) and `state` (province/state code). City must be supplied with both country and state. Example: `getAvailablePhoneNumbers({ country: "CA", state: "QC", city: "Charny", npa: "367", nxx: "883", limit: 10 })`. Canadian province, city, NPA and NXX matches are checked before the result limit; unrelated substring matches are excluded. Availability may change before purchase.
63
-
64
-
65
- Canadian message estimates return per-recipient `phoneNumber`, `rate`, `amount` and `fallback`, plus segment count and total in microdollars. They use cached carrier information without exposing carrier names or making a paid lookup. Rates remain 75,500 for standard carriers and 81,000 for Ice Wireless/Iristel or unresolved carriers; MMS/group MMS use one segment per recipient.
66
-
67
- Available-number searches return `{ numbers, numberCount? }`. `numberCount` is omitted when the total is unknown, including Canadian geographic and US city searches; the page length is not a total. These filtered searches have a 20-second discovery deadline and return HTTP 503 when incomplete. Narrow the location/NPA/NXX or retry later. Successful Infobip samples may be cached for 15 seconds across pages; purchase rechecks availability.
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@signalhousellc/sdk",
3
- "version": "1.0.68",
3
+ "version": "1.1.0",
4
4
  "description": "Signal House SDK for use with the Signal House platform",
5
5
  "type": "module",
6
6
  "main": "src/SignalHouseSDK.js",
7
7
  "exports": {
8
8
  ".": "./src/SignalHouseSDK.js",
9
- "./voice-browser": "./src/domains/voice/browser/index.js"
9
+ "./voice-browser": "./src/domains/voice/browser/index.js",
10
+ "./webhooks": "./src/webhooks/verifySignature.js"
10
11
  },
11
12
  "files": [
12
13
  "src/",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "https://github.com/bonzo-signalhouse/agent-skills.git",
3
- "ref": "v0.2.0",
3
+ "ref": "v0.3.0",
4
4
  "skills": [
5
5
  "signalhouse-10dlc",
6
6
  "signalhouse-advisor",
@@ -10,13 +10,13 @@
10
10
  "signalhouse-webhooks"
11
11
  ],
12
12
  "files": {
13
- "signalhouse-10dlc/SKILL.md": "b10ed2f759edcfcc9099c56285341ff0fbc9c131b2fd4b2d47922067128e79c2",
13
+ "signalhouse-10dlc/SKILL.md": "6a46fddf1572b39849c07855c6ece3d3499c711e5ffa6f466d73ab54182fd49f",
14
14
  "signalhouse-10dlc/examples/brightsmile-onboarding.md": "b5c04a551bc918f2563f1d1e59e516cf2e60fa7233490e3312a92dd1d1e9d473",
15
- "signalhouse-10dlc/references/tcr-rejection-causes.md": "8347a1656035626e0fc77dca49a0d06800e077ec8a4dc6e7e89208d90e8aeb2f",
16
- "signalhouse-advisor/SKILL.md": "9e3542f4b8c56474db4167535b2310432456bf935578a160a255b8a7d84d7738",
17
- "signalhouse-numbers/SKILL.md": "47ebcd18cb0bda80ebfefa1165540117c4712691356d602c76ab9e29a69c6626",
18
- "signalhouse-setup/SKILL.md": "0e3743d9a6adf8516d0d194c952090af2875605c82d3a397c58e49e0f4d56cde",
19
- "signalhouse-sms/SKILL.md": "e40c5a9da90c2ba75a34de8f5048b2cde3f202f6eb44067352dd32fce8988ce5",
20
- "signalhouse-webhooks/SKILL.md": "2b47468bcdc581b32b3697fcaab05b1fd16e279e139c915d83ceed1ce1ddd57f"
15
+ "signalhouse-10dlc/references/tcr-rejection-causes.md": "312d754c55dfdf8e21d085724728ae4b2288d7b485fba1c61c78f94a42780e8c",
16
+ "signalhouse-advisor/SKILL.md": "5e5070dc4742a1fe0195989471f809170c30fd7d8f63c33e4778215a2a446146",
17
+ "signalhouse-numbers/SKILL.md": "f996be53809ea2f90e49b32452f162eeff5369056e1f63e422b8fdb46d2896ff",
18
+ "signalhouse-setup/SKILL.md": "ca87a6d71f3f82614b83ad283971385fc55c13c097b76595b1b85def235373ec",
19
+ "signalhouse-sms/SKILL.md": "ec7ed8985ad590b0ab5d40c9ddb003eb8cbc566a1309ac5e330f570e0404ec42",
20
+ "signalhouse-webhooks/SKILL.md": "d16062e9e0a7cdda3c02aee485bd5114659405d00b1d635da1280db0179490a7"
21
21
  }
22
22
  }
@@ -84,17 +84,13 @@ The two with constrained values, which is where callers get stuck:
84
84
  | Field | Values |
85
85
  |---|---|
86
86
  | `entityType` | `PRIVATE_PROFIT`, `PUBLIC_PROFIT`, `NON_PROFIT`, `GOVERNMENT` |
87
- | `vertical` | one of 23 industry values: `PROFESSIONAL`, `REAL_ESTATE`, `HEALTHCARE`, `FINANCIAL`, `TECHNOLOGY`, `RETAIL` and so on. Do not guess; read the list from the registration guide. |
87
+ | `vertical` | one of 23 industry values: `PROFESSIONAL`, `REAL_ESTATE`, `HEALTHCARE`, `FINANCIAL`, `TECHNOLOGY`, `RETAIL` and so on. Do not guess; read the list from the Brands reference. |
88
88
 
89
89
  **`brandRelationship` is not a field you set.** The service overwrites it before validation runs,
90
90
  and every 10DLC brand ends up `MEDIUM_ACCOUNT` regardless of what was sent. Whatever a caller
91
91
  passes is discarded silently. Do not ask a customer to choose one, and do not report it back as
92
92
  something they selected.
93
93
 
94
- > ⚠️ The 10DLC Registration Guide's own required-fields table still lists `brandRelationship` as a
95
- > required, caller-chosen field. It is wrong on this point; `03-api-reference/02-brands.md` and
96
- > the service code agree that it is ignored. Tracked as **SHGHL-2946**.
97
-
98
94
  `PUBLIC_PROFIT` additionally requires `stockSymbol`, `stockExchange`, `website` and
99
95
  `businessContactEmail`.
100
96
 
@@ -166,11 +162,11 @@ Never state a current status, price, or throughput figure from memory. Read it:
166
162
  - **Whether a specific send will be allowed:** attempt it and read the error, or check campaign
167
163
  state first. There is no separate preflight endpoint.
168
164
 
169
- - **The full field lists, verticals, use cases, status meanings and worked examples:** the
170
- internal 10DLC Registration Guide (`documentation/dev-documentation/07-10dlc-registration.md`)
171
- and the published help-centre articles *10DLC Messaging Overview*, *10DLC Campaign Website
165
+ - **The full field lists, verticals, use cases, status meanings and worked examples:** the Brands
166
+ and Campaigns references (https://app2.signalhouse.io/docs/brands,
167
+ https://app2.signalhouse.io/docs/campaigns) and the published help-centre articles *10DLC Messaging Overview*, *10DLC Campaign Website
172
168
  Review* and *10DLC Campaign Approvals*. This skill is a condensed guardrail, not a substitute
173
- for them. Where it and the guide disagree, the guide wins and this file is the bug.
169
+ for them. Where it and the references disagree, the references win and this file is the bug.
174
170
 
175
171
  The platform enforces every constraint on this page. This skill exists so an agent stops
176
172
  *before* a rejected registration or a failed send, not so it can predict the outcome.
@@ -5,8 +5,7 @@ Loaded on demand, and only needed once something has actually been rejected.
5
5
  Every cause below is drawn from a **real Signal House support ticket** or verified against the
6
6
  API, with the source noted. Nothing here is general industry knowledge.
7
7
 
8
- > **Review status:** engineer-verified. Still needs the named business reviewer per
9
- > [CONTRIBUTING.md](../../../CONTRIBUTING.md), since this is compliance-bearing content.
8
+ > **Review status:** engineer-verified against support tickets and the API.
10
9
 
11
10
  ## How to read a rejection
12
11
 
@@ -43,9 +42,10 @@ Two distinct real causes share this code.
43
42
  **`Invalid sub-usecases: SOCIAL`.** `SOCIAL` is not a valid sub-use-case under any parent,
44
43
  neither `LOW_VOLUME` nor `MIXED`. Valid sub-use-cases for `LOW_VOLUME` are
45
44
  `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`, `MARKETING`. Signal House's own
46
- validation now rejects `SOCIAL` as a `subUsecases` value with an immediate 400 before the request
47
- ever reaches TCR (SHGHL-2943), so this specific 501 should no longer be reachable through the
48
- API — `SOCIAL` is still a valid top-level `usecase`, just never a sub-use-case.
45
+ validation now rejects `SOCIAL` as a `subUsecases` value with an immediate 400, before the request
46
+ reaches TCR, so this 501 should no longer be reachable through the API. `SOCIAL` is still a valid
47
+ top-level `usecase`, just never a sub-use-case. If an older campaign carries this rejection,
48
+ resubmit with `SOCIAL` removed from `subUsecases`.
49
49
 
50
50
  **Punctuation in a tag.** A tag of `maria-answers` was rejected for containing a hyphen. The
51
51
  campaign content was fine. Fix the tag (`mariaanswers`), then the campaign has to be
@@ -102,10 +102,3 @@ for `PENDING_REVIEW` / `REJECTED` / `TCR_REGISTER_FAILED` when the campaign is *
102
102
  at the moment it fails. This is deliberate, because those states are appealable and re-registerable,
103
103
  but it means a customer who abandons failed campaigns keeps funds tied up. Deleting the dead
104
104
  campaign frees them.
105
-
106
- ## Open questions for the business reviewer
107
-
108
- 1. **The full valid sub-use-case set per parent use case.** Four are confirmed for `LOW_VOLUME`
109
- from a support ticket (`ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`,
110
- `MARKETING`). The complete mapping should come from TCR's own documentation rather than being
111
- inferred from the tickets we happen to have.
@@ -78,7 +78,7 @@ constraint is nearly always the part that changes the plan.
78
78
 
79
79
  ## Where to look it up
80
80
 
81
- - **The full setup pipeline in one place:** the internal 10DLC Registration guide
82
- (`documentation/dev-documentation/07-10dlc-registration.md`).
83
- - **A first working call:** the Quickstart (`01-quickstart.md`).
81
+ - **The full setup pipeline:** the Brands and Campaigns references
82
+ (https://app2.signalhouse.io/docs/brands, https://app2.signalhouse.io/docs/campaigns).
83
+ - **A first working call:** https://app2.signalhouse.io/docs/getting-started.
84
84
  - **Current fees and balance:** the billing endpoints, read live, per group.
@@ -76,8 +76,7 @@ cost".
76
76
  ## Where to look it up
77
77
 
78
78
  - **Search filters, NPA/NXX lookup, friendly names, per-number inbound webhook URLs, and the
79
- release semantics:** the internal Number Management Guide
80
- (`documentation/dev-documentation/06-number-management.md`).
79
+ release semantics:** the Numbers reference, https://app2.signalhouse.io/docs/numbers.
81
80
  - **Current fees:** `GET /billing/fees/:groupId`.
82
81
  - **Current balance before a bulk buy:** the wallet endpoints under `/billing/wallet`.
83
82
  - **Whether a number can send:** its campaign's state, not the number's. See `signalhouse-10dlc`.
@@ -4,8 +4,9 @@ description: >-
4
4
  Getting a Signal House integration authenticated and wired: login vs service-user tokens, what a
5
5
  token is scoped to, SDK initialization, and why webhooks come before the first write.
6
6
  TRIGGER when the user is starting a Signal House integration, asks how to authenticate, asks for
7
- an API key or token, is choosing a base URL, is installing or initializing the SDK, is getting a
8
- 401 or 403, or asks why their token stopped working.
7
+ an API key or token, is choosing a base URL, is installing or initializing the SDK, is connecting
8
+ Claude, Cursor or another AI client to Signal House (MCP), is getting a 401 or 403, or asks why
9
+ their token stopped working.
9
10
  SKIP for message sending, 10DLC registration, number purchase and webhook event semantics, which
10
11
  have their own skills. This one ends at "the first authenticated call succeeds".
11
12
  ---
@@ -25,8 +26,8 @@ Authentication guide or the environment rather than quoting a number to a custom
25
26
 
26
27
  **A service user cannot be bootstrapped from nothing.** `POST /user/serviceuser` needs an existing
27
28
  group and a token that is already privileged enough to mint one. The first credential in a new
28
- integration comes from a human logging in or from Signal House provisioning it, not from an API call
29
- made with no credential.
29
+ integration comes from a human logging in (including approving an OAuth connection, below) or from
30
+ Signal House provisioning it, not from an API call made with no credential.
30
31
 
31
32
  **`role: "signalhouse_api"` is not available to customers.** The two roles `POST /user/serviceuser`
32
33
  accepts are `api` (customer key, scoped to one `groupId`) and `signalhouse_api` (internal, requires
@@ -62,6 +63,33 @@ on it. See `signalhouse-10dlc`.
62
63
  without webhooks discovers outcomes by polling, burns rate limit doing it, and still finds out
63
64
  late. See `signalhouse-webhooks`.
64
65
 
66
+ ## Connecting an AI client (MCP)
67
+
68
+ Signal House runs a hosted MCP server at `https://mcp.signalhouse.io/mcp`. It holds no credentials
69
+ of its own: every call is forwarded to the API with the caller's own token, so an agent can do
70
+ exactly what that token can do and nothing more.
71
+
72
+ - **Claude (web, Desktop, mobile): connect with OAuth.** Settings > Connectors > Add custom
73
+ connector, paste the URL, leave the client ID and secret blank, then log in to Signal House and
74
+ approve. No API key is copied anywhere. This is the path to recommend to a person.
75
+ - **Clients configured by file or command (Claude Code, Cursor and similar):** send an API key as a
76
+ bearer header, e.g.
77
+ `claude mcp add --transport http signalhouse https://mcp.signalhouse.io/mcp --header "Authorization: Bearer <API key>"`.
78
+ Claude Code can also do the OAuth flow: add it without the header and authenticate from `/mcp`.
79
+
80
+ What an OAuth connection can and cannot do:
81
+
82
+ - **It acts as the user who approved it, with that user's full role.** Scopes are not enforced, so
83
+ do not tell a customer a connection is "read-only" or limited to part of the account.
84
+ - **It cannot mint credentials or manage the login.** Routes under `/user` and `/auth` refuse a
85
+ connector token with a **403**, so an agent on an OAuth connection cannot create service users or
86
+ API keys. A 403 there is by design, not a bug to work around; create keys in the portal.
87
+ - **It is revoked by the user**, in the portal under Settings > Security > Connected Applications.
88
+ After that the client has to reconnect.
89
+
90
+ For an unattended server integration, the rules above still hold: use a service-user API key, not
91
+ an OAuth connection, which exists for a person working through an AI client.
92
+
65
93
  ## Reading the response
66
94
 
67
95
  The SDK returns an **envelope**, and most failures arrive inside it rather than as an exception:
@@ -82,10 +110,12 @@ current shape from the API reference rather than assuming it matches a sibling e
82
110
 
83
111
  ## Where to look it up
84
112
 
85
- - **Auth flows, service users, the full role table, token practice:** the internal Authentication
86
- guide (`documentation/dev-documentation/02-authentication.md`).
87
- - **First working call end to end:** the Quickstart (`01-quickstart.md`).
88
- - **Envelope shapes, error classes and status codes:** the Error Handling guide (`08-error-handling.md`).
89
- - **Backoff, batching and pagination:** the Rate Limits guide (`09-rate-limits.md`).
90
- - **What a role is allowed to do:** the role table in the Authentication guide. Roles are checked
91
- server-side, so a plan that assumes more access than the token has fails at the call, not earlier.
113
+ - **Auth flows, service users, the full role table, token practice:** the Authentication reference,
114
+ https://app2.signalhouse.io/docs/authentication.
115
+ - **The OAuth connection flow:** https://app2.signalhouse.io/docs/oauth.
116
+ - **First working call end to end:** https://app2.signalhouse.io/docs/getting-started.
117
+ - **Envelope shapes, error classes and status codes:** https://app2.signalhouse.io/docs/error-handling.
118
+ - **Backoff, batching and pagination:** https://app2.signalhouse.io/docs/rate-limits.
119
+ - **What a role is allowed to do:** the role table in the Authentication reference. Roles are
120
+ checked server-side, so a plan that assumes more access than the token has fails at the call, not
121
+ earlier.
@@ -73,9 +73,9 @@ is a 400, not an unbounded query. The API refuses rather than scanning.
73
73
 
74
74
  ## Where to look it up
75
75
 
76
- - **Every send parameter, the full status table and delivery receipts:** the internal Sending
77
- Messages guide (`documentation/dev-documentation/05-sending-messages.md`).
76
+ - **Every send parameter, message statuses and delivery receipts:** the Messages reference,
77
+ https://app2.signalhouse.io/docs/messages.
78
78
  - **Current per-group fees:** `GET /billing/fees/:groupId`.
79
- - **Failure codes and what is retryable:** the Error Handling guide (`08-error-handling.md`).
80
- - **Backoff and batching:** the Rate Limits guide (`09-rate-limits.md`).
79
+ - **Failure codes and what is retryable:** https://app2.signalhouse.io/docs/error-handling.
80
+ - **Backoff and batching:** https://app2.signalhouse.io/docs/rate-limits.
81
81
  - **Why a number cannot send yet:** `signalhouse-10dlc`.
@@ -12,71 +12,90 @@ description: >-
12
12
 
13
13
  # Signal House webhooks
14
14
 
15
- ## What cannot happen
15
+ ## What a receiver can verify
16
16
 
17
- **A receiver cannot verify that a webhook came from Signal House.** Payloads are **not signed**.
18
- There is no HMAC, no signature header, and no request timestamp. This is verified in the
19
- backend, not assumed.
17
+ **Deliveries to a webhook endpoint are signed.** Every endpoint created with `POST /webhook` gets a
18
+ signing secret, returned in plaintext **exactly once**, as `signingSecret` in that create response.
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
21
 
21
- The `authType` setting (`NONE`, `BASIC`, `API_KEY`, `OAUTH2`) authenticates *Signal House to the
22
- receiver's endpoint*. It is the opposite direction from a signature and gives the receiver
23
- nothing to check. Even with `BASIC` or `API_KEY`, the credential is a bearer secret sent *to*
24
- the receiver: it proves nothing about payload integrity and any captured request can be replayed.
22
+ A signed delivery carries:
25
23
 
26
- Practical consequences a receiver has to design around:
24
+ | Header | Value |
25
+ |---|---|
26
+ | `X-SignalHouse-Signature` | hex HMAC-SHA256, keyed with the signing secret, over `${timestamp}.${rawBody}` |
27
+ | `X-SignalHouse-Timestamp` | the `timestamp` in that string: Unix time in **milliseconds** |
28
+ | `X-SignalHouse-Delivery-Id` | a UUID for this event to this endpoint, the same on every retry |
29
+
30
+ A correct verifier:
31
+
32
+ 1. **Uses the raw request body**, byte for byte, before any JSON parsing. Re-serializing a parsed
33
+ body changes whitespace and key order and the signature stops matching.
34
+ 2. **Recomputes the HMAC and compares in constant time.** The signature is bare hex, no prefix.
35
+ 3. **Rejects a stale timestamp.** The timestamp is inside the signed string, so a captured request
36
+ cannot be replayed with a fresh one. Pick a window (five minutes is common) and remember the
37
+ header is milliseconds, not seconds.
38
+ 4. **Rejects an unsigned request to an endpoint that has a secret.** Otherwise a forger simply
39
+ leaves the headers off.
40
+
41
+ What is still **not** verifiable:
42
+
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.
46
+ - **Per-number inbound URLs are never signed** (see below).
27
47
 
28
- - **Do not treat an inbound webhook as authenticated.** Anyone who learns the URL can post to it.
29
- With `authType: NONE` there is nothing stopping them at all.
30
- - **Never take a state-changing action on webhook content alone** where the action is
31
- irreversible or financial. Treat the webhook as a *hint that something changed*, then read the
32
- authoritative state back from the API before acting on it.
33
- - **Do not put secrets in the webhook URL** as a substitute for verification. It ends up in
34
- proxy logs, browser history and support screenshots.
48
+ For anything unsigned, the old rules still apply: do not treat the request as authenticated, and
49
+ never take an irreversible or financial action on webhook content alone. Read the authoritative
50
+ state back from the API first. Do not put secrets in a webhook URL as a substitute for
51
+ verification; URLs end up in proxy logs and support screenshots.
35
52
 
36
- > ⚠️ Tracked as **SHGHL-2944**. Delete this section when signing ships. A workaround left in a
37
- > skill becomes permanent.
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.
38
56
 
39
57
  ## Retry behaviour, and when an event is gone
40
58
 
41
- Delivery is a **direct HTTP POST with a 3-second timeout**, retried **3 times, 500 ms apart, with
42
- no backoff and no jitter**. It is not queued, so there is no queue-level redelivery behind it.
43
- Verified against the send path, which takes the retry defaults at every call site.
59
+ Delivery is a **direct HTTP POST with a 3-second timeout**, made up to **3 times in total**. The
60
+ waits between attempts back off exponentially with jitter: about 500 ms, then about 1 second
61
+ (each up to 25% longer). It is not queued, so there is no queue-level redelivery behind it. Each
62
+ attempt is signed afresh with a new timestamp and carries the **same** delivery id.
63
+
64
+ What gets retried:
65
+
66
+ - **Retried:** no response at all (network error, DNS failure, timeout), any **5xx**, and **429**.
67
+ - **Not retried:** every other **4xx**. A receiver that answers 400, 401, 403 or 404 gets the
68
+ event once. So return a 4xx only for a request you will never accept (a bad signature, say), and
69
+ a 5xx or 429 for anything transient.
44
70
 
45
71
  Two numbers matter, and they are different things:
46
72
 
47
73
  - **A receiver has 3 seconds to respond.** Exceed it and the attempt is a failure, even if the
48
74
  work eventually succeeds on your side.
49
- - **The whole delivery window is about 10 seconds** worst case, three attempts that can each
50
- burn the full timeout, plus 500 ms between them.
51
-
52
- - A receiver that is down, restarting, or rate-limited for even a minute **loses the event
53
- permanently**. There is no dead-letter queue and no replay endpoint.
54
- - **4xx responses are retried too.** The HTTP client throws on any non-2xx and the retry
55
- predicate accepts everything, so a receiver returning 400 gets the same doomed request three
56
- times.
75
+ - **The whole delivery window is about 11 seconds** worst case: three attempts that can each burn
76
+ the full timeout, plus the two waits.
77
+
78
+ - A receiver that is down, restarting, or rate-limited for longer than that **loses the push for
79
+ that event**. There is no dead-letter queue and no replay endpoint.
57
80
  - A slow endpoint that exceeds 3 seconds and *then* completes its work has been recorded as a
58
- failure and will be sent the **same event up to three times**, with no delivery id to key
59
- idempotency on. This is the most likely duplicate scenario in practice.
81
+ failure and will be sent the **same event again**. Dedupe on `X-SignalHouse-Delivery-Id`.
60
82
 
61
83
  So a correct receiver:
62
84
 
63
85
  1. **Responds inside 3 seconds, and does the work afterwards.** Acknowledge, enqueue, process out
64
86
  of band. Any processing done before responding is spending a budget you do not control, and
65
87
  blowing it converts a successful handler into a duplicate delivery.
66
- 2. **Is idempotent on the event's own identifiers** (message id, campaign id and status),
67
- because there is no delivery id to dedupe on.
88
+ 2. **Is idempotent on `X-SignalHouse-Delivery-Id`**, which every delivery carries, signed or not,
89
+ including per-number inbound URLs.
68
90
  3. **Reconciles against `GET /notification`.** This is the important one. Every event that would
69
- trigger a webhook also writes a **notification record, before endpoints are even looked up and
70
- regardless of whether delivery succeeds**. It carries the same `{timestamp, event, identifier,
71
- metaData}` shape and is filterable by group, status and event type.
91
+ trigger a group webhook also writes a **notification record, created before endpoints are even
92
+ looked up and independent of whether delivery succeeds**. It carries the same `{timestamp,
93
+ event, identifier, metaData}` shape and is filterable by group, status and event type.
72
94
 
73
95
  That makes it the durable record and the webhook merely the push. A receiver that must not
74
96
  miss anything polls it to catch up after any outage, which is the only reliable recovery,
75
97
  given there is no replay endpoint.
76
98
 
77
- > ⚠️ Also tracked as **SHGHL-2944**. If the retry policy changes, these numbers are wrong and
78
- > this section must be rewritten from the code, not adjusted from memory.
79
-
80
99
  ## Setting one up
81
100
 
82
101
  Webhooks are created per group, and numbers can additionally carry their own inbound URL.
@@ -90,27 +109,28 @@ Webhooks are created per group, and numbers can additionally carry their own inb
90
109
  | `API_KEY` | `key` holds the key; `secret` optional |
91
110
  | `OAUTH2` | `secret` is the access token sent as the Authorization value; `key` is required at create but unused in outgoing headers |
92
111
 
93
- `NONE` is the right default only for an endpoint that is already unreachable from the public
94
- internet. For anything else, pick a real `authType`. It is not verification, but it stops
95
- casual forgery.
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.
96
115
 
97
116
  **The table above does not cover per-number inbound URLs.** A number can carry
98
117
  `primaryInboundWebhookUrl` and `secondaryInboundWebhookUrl`, and inbound messages are posted to
99
- both. That path sends **no auth headers at all**, whatever `authType` is configured elsewhere,
100
- it is always unauthenticated. Do not assume configuring auth on a group webhook protects the
101
- per-number ones; it does not. Treat those URLs as fully public and put nothing behind them that
102
- matters without an independent check.
118
+ both. That path sends **no auth headers and no signature**, whatever is configured elsewhere: it has
119
+ no endpoint record to hold a secret, so it is always unauthenticated. It does carry
120
+ `X-SignalHouse-Delivery-Id`. Do not assume configuring a group webhook protects the per-number
121
+ ones; it does not. Treat those URLs as fully public and put nothing behind them that matters
122
+ without an independent check. If a number's inbound traffic needs verifying, route it through a
123
+ signed group endpoint instead.
103
124
 
104
125
  ## Where to look it up
105
126
 
106
- - **Event types and payload shape:** the internal Webhooks Guide
107
- (`documentation/dev-documentation/04-webhooks-guide.md`). The event list changes independently
108
- of this skill; read it there rather than reciting it here. Note the guide does **not** document
109
- the per-number `primaryInboundWebhookUrl` / `secondaryInboundWebhookUrl` fields described above.
127
+ - **Event types, payload shape and the signing scheme:** the Webhooks reference,
128
+ https://app2.signalhouse.io/docs/webhooks. The event list changes independently of this skill;
129
+ read it there rather than reciting it here.
110
130
  - **Whether an event actually happened:** `GET /notification`
111
- (`documentation/dev-documentation/03-api-reference/13-notifications.md`). The notification
112
- record is written independently of delivery, so it is the record and the webhook is the push.
113
- - **Whether a delivery failed:** you largely cannot, from the outside or the inside. Failed group
114
- and per-number deliveries are swallowed without a server-side log, so support may have no trace
115
- of a delivery you never received. Do not design a process that depends on us being able to
116
- confirm one.
131
+ (https://app2.signalhouse.io/docs/notifications). The notification record is written
132
+ independently of delivery, so it is the record and the webhook is the push.
133
+ - **Whether a delivery failed:** Signal House logs every failed delivery with the group, the
134
+ endpoint, the event and the HTTP status your endpoint returned (or none, for a timeout), so
135
+ support can answer "did you get a 401 from us". There is still no API for a customer to list
136
+ failed deliveries or replay one, so do not design a process that depends on it.
@@ -28,7 +28,9 @@ import { Groups } from "./domains/Groups.js";
28
28
  import { Landings } from "./domains/Landings.js";
29
29
  import { Messages } from "./domains/Messages.js";
30
30
  import { Numbers } from "./domains/Numbers.js";
31
+ import { Registrations } from "./domains/Registrations.js";
31
32
  import { Onboarding } from "./domains/Onboarding.js";
33
+ import { Dnc } from "./domains/Dnc.js";
32
34
  import { Shortlinks } from "./domains/Shortlinks.js";
33
35
  import { Subgroups } from "./domains/Subgroups.js";
34
36
  import { Subscriptions } from "./domains/Subscriptions.js";
@@ -72,7 +74,9 @@ export class SignalHouseSDK {
72
74
  this.landings = new Landings(client, multipartClient, this.enableAdmin);
73
75
  this.messages = new Messages(client, multipartClient, this.enableAdmin);
74
76
  this.numbers = new Numbers(client, multipartClient, this.enableAdmin);
77
+ this.registrations = new Registrations(client, this.enableAdmin);
75
78
  this.onboarding = new Onboarding(client, this.enableAdmin);
79
+ this.dnc = new Dnc(client, this.enableAdmin);
76
80
  this.shortlinks = new Shortlinks(client, this.enableAdmin);
77
81
  this.subgroups = new Subgroups(client, this.enableAdmin);
78
82
  this.subscriptions = new Subscriptions(client, this.enableAdmin);
@@ -13,6 +13,7 @@
13
13
  * @property {string} [systemPrompt] - Base system prompt composed into every conversation.
14
14
  * @property {string} [greeting] - Opening line the agent leads with.
15
15
  * @property {string} [guardrails] - Freeform guardrail instructions appended to the composed prompt.
16
+ * @property {string} [sendAuthority] - Default send authority: "review" or "autopilot" (defaults to "review"). Deployments may override it per subgroup.
16
17
  */
17
18
 
18
19
  /**
@@ -22,6 +23,48 @@
22
23
  * @property {string} [systemPrompt] - Base system prompt composed into every conversation.
23
24
  * @property {string} [greeting] - Opening line the agent leads with.
24
25
  * @property {string} [guardrails] - Freeform guardrail instructions appended to the composed prompt.
26
+ * @property {string} [sendAuthority] - Default send authority: "review" or "autopilot". Deployments may override it per subgroup.
27
+ */
28
+
29
+ /**
30
+ * An agent profile as returned by the server. `publishStatus` and `publishedAt` are read-only:
31
+ * they change only through `publishAgentProfile` and `unpublishAgentProfile`.
32
+ * @typedef {Object} AgentProfile
33
+ * @property {string} agentProfileId - The agent profile id.
34
+ * @property {string} groupId - The owning group id.
35
+ * @property {string|null} subgroupId - The owning subgroup id; null for a group-level agent.
36
+ * @property {string} name - The agent's name.
37
+ * @property {string} status - Lifecycle status: "active" or "inactive".
38
+ * @property {string} [systemPrompt] - Base system prompt composed into every conversation.
39
+ * @property {string} [greeting] - Opening line the agent leads with.
40
+ * @property {string} [guardrails] - Freeform guardrail instructions appended to the composed prompt.
41
+ * @property {string} sendAuthority - Default send authority: "review" or "autopilot".
42
+ * @property {string} publishStatus - "draft" or "published" (read-only).
43
+ * @property {string|null} publishedAt - When the profile was last published; null while never published (read-only).
44
+ * @property {string} createdAt - Creation timestamp.
45
+ * @property {string} updatedAt - Last-update timestamp.
46
+ */
47
+
48
+ /**
49
+ * The fields of a deployment of a group-level agent to a subgroup.
50
+ * @typedef {Object} AgentDeploymentData
51
+ * @property {string|null} [sendAuthorityOverride] - "review" or "autopilot" to override the profile's default for this subgroup; null clears the override so the deployment inherits the profile default.
52
+ * @property {boolean} [enabled] - Whether this deployment is live (defaults to true).
53
+ */
54
+
55
+ /**
56
+ * A deployment of a group-level agent to a subgroup, as returned by the server.
57
+ * @typedef {Object} AgentDeployment
58
+ * @property {string} agentDeploymentId - The deployment id.
59
+ * @property {string} groupId - The group that owns the agent and the subgroup.
60
+ * @property {string} subgroupId - The subgroup the agent is deployed to.
61
+ * @property {string} agentProfileId - The deployed agent.
62
+ * @property {string|null} sendAuthorityOverride - "review", "autopilot", or null (inherit the profile default).
63
+ * @property {string|null} sendAuthorityChangedBy - userId of whoever last changed the override.
64
+ * @property {string|null} sendAuthorityChangedAt - When the override last changed.
65
+ * @property {boolean} enabled - Whether this deployment is live.
66
+ * @property {string} createdAt - Creation timestamp.
67
+ * @property {string} updatedAt - Last-update timestamp.
25
68
  */
26
69
 
27
70
  /**
@@ -361,6 +404,94 @@ export class Agents {
361
404
  return this.client(`/agent/profiles/${safeId}/channels/${safeChannel}`, { method: "DELETE", ...options });
362
405
  }
363
406
 
407
+ /**
408
+ * List a group-level agent's deployments to subgroups.
409
+ * @async
410
+ * @roles api, admin, developer, billing, user
411
+ * @param {Object} params - The parameters for listing deployments.
412
+ * @param {string} params.agentProfileId - The agent whose deployments to list.
413
+ * @param {RequestOptions} [params.options] - Additional options for the request.
414
+ * @throws {Error} Throws an error if the agentProfileId parameter is missing.
415
+ * @returns {Promise<SdkResponse<AgentDeployment[]>>} - The agent's deployments returned from the server.
416
+ */
417
+ async getAgentDeployments({ agentProfileId, options = {} }) {
418
+ this.client._require({ agentProfileId });
419
+ const safeId = encodeURIComponent(agentProfileId);
420
+ return this.client(`/agent/profiles/${safeId}/deployments`, { method: "GET", ...options });
421
+ }
422
+
423
+ /**
424
+ * Deploy a group-level agent to a subgroup, or change that deployment (upsert). One row per
425
+ * (agent, subgroup); the subgroup is taken from the path, the fields from deploymentData.
426
+ * @async
427
+ * @roles api, admin, developer
428
+ * @param {Object} params - The parameters for the upsert.
429
+ * @param {string} params.agentProfileId - The agent to deploy.
430
+ * @param {string} params.subgroupId - The subgroup to deploy to (at least 8 characters, starts with 'S').
431
+ * @param {AgentDeploymentData} [params.deploymentData] - The fields to set.
432
+ * @param {RequestOptions} [params.options] - Additional options for the request.
433
+ * @throws {Error} Throws an error if agentProfileId or subgroupId is missing.
434
+ * @returns {Promise<SdkResponse<AgentDeployment>>} - The created or updated deployment returned from the server.
435
+ */
436
+ async upsertAgentDeployment({ agentProfileId, subgroupId, deploymentData = {}, options = {} }) {
437
+ this.client._require({ agentProfileId, subgroupId });
438
+ const safeId = encodeURIComponent(agentProfileId);
439
+ const safeSubgroupId = encodeURIComponent(subgroupId);
440
+ return this.client(`/agent/profiles/${safeId}/deployments/${safeSubgroupId}`, { method: "PUT", body: deploymentData, ...options });
441
+ }
442
+
443
+ /**
444
+ * Remove an agent's deployment from a subgroup.
445
+ * @async
446
+ * @roles api, admin, developer
447
+ * @param {Object} params - The parameters for the delete.
448
+ * @param {string} params.agentProfileId - The deployed agent.
449
+ * @param {string} params.subgroupId - The subgroup to remove the deployment from.
450
+ * @param {RequestOptions} [params.options] - Additional options for the request.
451
+ * @throws {Error} Throws an error if agentProfileId or subgroupId is missing.
452
+ * @returns {Promise<SdkResponse<AgentDeployment>>} - The removed deployment returned from the server.
453
+ */
454
+ async deleteAgentDeployment({ agentProfileId, subgroupId, options = {} }) {
455
+ this.client._require({ agentProfileId, subgroupId });
456
+ const safeId = encodeURIComponent(agentProfileId);
457
+ const safeSubgroupId = encodeURIComponent(subgroupId);
458
+ return this.client(`/agent/profiles/${safeId}/deployments/${safeSubgroupId}`, { method: "DELETE", ...options });
459
+ }
460
+
461
+ /**
462
+ * Publish an agent profile, setting its `publishStatus` to "published" and stamping
463
+ * `publishedAt`. The server rejects the call with 400 when the agent has no enabled
464
+ * deployment, and with 409 when the agent is inactive.
465
+ * @async
466
+ * @roles api, admin, developer
467
+ * @param {Object} params - The parameters for publishing.
468
+ * @param {string} params.agentProfileId - The agent profile to publish.
469
+ * @param {RequestOptions} [params.options] - Additional options for the request.
470
+ * @throws {Error} Throws an error if the agentProfileId parameter is missing.
471
+ * @returns {Promise<SdkResponse<AgentProfile>>} - The published agent profile returned from the server.
472
+ */
473
+ async publishAgentProfile({ agentProfileId, options = {} }) {
474
+ this.client._require({ agentProfileId });
475
+ const safeId = encodeURIComponent(agentProfileId);
476
+ return this.client(`/agent/profiles/${safeId}/publish`, { method: "POST", ...options });
477
+ }
478
+
479
+ /**
480
+ * Return an agent profile to draft, setting its `publishStatus` to "draft".
481
+ * @async
482
+ * @roles api, admin, developer
483
+ * @param {Object} params - The parameters for unpublishing.
484
+ * @param {string} params.agentProfileId - The agent profile to unpublish.
485
+ * @param {RequestOptions} [params.options] - Additional options for the request.
486
+ * @throws {Error} Throws an error if the agentProfileId parameter is missing.
487
+ * @returns {Promise<SdkResponse<AgentProfile>>} - The unpublished agent profile returned from the server.
488
+ */
489
+ async unpublishAgentProfile({ agentProfileId, options = {} }) {
490
+ this.client._require({ agentProfileId });
491
+ const safeId = encodeURIComponent(agentProfileId);
492
+ return this.client(`/agent/profiles/${safeId}/unpublish`, { method: "POST", ...options });
493
+ }
494
+
364
495
  /**
365
496
  * Read the tenant AI settings for a scope (group, or a subgroup within it) — brand, tone,
366
497
  * timezone, business hours, and after-hours message. Returns the scope's own row only (no