@signalhousellc/sdk 1.0.67 → 1.0.69
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 +0 -10
- package/package.json +11 -3
- package/skills/VENDORED.json +8 -8
- package/skills/signalhouse-10dlc/SKILL.md +5 -9
- package/skills/signalhouse-10dlc/references/tcr-rejection-causes.md +5 -12
- package/skills/signalhouse-advisor/SKILL.md +3 -3
- package/skills/signalhouse-numbers/SKILL.md +1 -2
- package/skills/signalhouse-setup/SKILL.md +41 -11
- package/skills/signalhouse-sms/SKILL.md +4 -4
- package/skills/signalhouse-webhooks/SKILL.md +76 -56
- package/src/SignalHouseSDK.js +26 -13
- package/src/domains/Agents.js +204 -55
- package/src/domains/Auth.js +30 -24
- package/src/domains/Billing.js +32 -26
- package/src/domains/Brands.js +45 -34
- package/src/domains/Campaigns.js +106 -47
- package/src/domains/Dnc.js +168 -0
- package/src/domains/Groups.js +53 -16
- package/src/domains/Landings.js +36 -30
- package/src/domains/Messages.js +110 -65
- package/src/domains/Notifications.js +12 -6
- package/src/domains/Numbers.js +58 -52
- package/src/domains/Onboarding.js +14 -8
- package/src/domains/Shortlinks.js +10 -4
- package/src/domains/Subgroups.js +16 -10
- package/src/domains/Subscriptions.js +22 -16
- package/src/domains/Tickets.js +18 -12
- package/src/domains/Users.js +32 -22
- package/src/domains/Webhooks.js +45 -13
- package/src/domains/voice/Analytics.js +8 -2
- package/src/domains/voice/CallLogs.js +14 -8
- package/src/domains/voice/Calls.js +14 -8
- package/src/domains/voice/GlobalVoiceSettings.js +10 -4
- package/src/domains/voice/ProgrammableVoiceProfiles.js +22 -16
- package/src/domains/voice/SipProfiles.js +24 -18
- package/src/domains/voice/SipTrunks.js +26 -20
- package/src/domains/voice/Tokens.js +10 -4
- package/src/domains/voice/browser/Call.js +2 -0
- package/src/domains/voice/browser/Device.js +23 -11
- package/src/webhooks/verifySignature.js +66 -0
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.
|
|
3
|
+
"version": "1.0.69",
|
|
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/",
|
|
@@ -19,7 +20,9 @@
|
|
|
19
20
|
},
|
|
20
21
|
"scripts": {
|
|
21
22
|
"postinstall": "node scripts/install-skills.mjs",
|
|
22
|
-
"sync-skills": "node scripts/sync-skills.mjs"
|
|
23
|
+
"sync-skills": "node scripts/sync-skills.mjs",
|
|
24
|
+
"build:types": "tsc -p tsconfig.json",
|
|
25
|
+
"check:types": "tsc -p tsconfig.json --noEmit"
|
|
23
26
|
},
|
|
24
27
|
"agents": {
|
|
25
28
|
"skills": "./skills"
|
|
@@ -36,5 +39,10 @@
|
|
|
36
39
|
"jssip": {
|
|
37
40
|
"optional": true
|
|
38
41
|
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.20.3",
|
|
45
|
+
"jssip": "^3.13.8",
|
|
46
|
+
"typescript": "^5.9.3"
|
|
39
47
|
}
|
|
40
48
|
}
|
package/skills/VENDORED.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "https://github.com/bonzo-signalhouse/agent-skills.git",
|
|
3
|
-
"ref": "v0.
|
|
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": "
|
|
13
|
+
"signalhouse-10dlc/SKILL.md": "6a46fddf1572b39849c07855c6ece3d3499c711e5ffa6f466d73ab54182fd49f",
|
|
14
14
|
"signalhouse-10dlc/examples/brightsmile-onboarding.md": "b5c04a551bc918f2563f1d1e59e516cf2e60fa7233490e3312a92dd1d1e9d473",
|
|
15
|
-
"signalhouse-10dlc/references/tcr-rejection-causes.md": "
|
|
16
|
-
"signalhouse-advisor/SKILL.md": "
|
|
17
|
-
"signalhouse-numbers/SKILL.md": "
|
|
18
|
-
"signalhouse-setup/SKILL.md": "
|
|
19
|
-
"signalhouse-sms/SKILL.md": "
|
|
20
|
-
"signalhouse-webhooks/SKILL.md": "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
82
|
-
(
|
|
83
|
-
- **A first working call:**
|
|
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
|
|
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
|
|
8
|
-
401 or 403, or asks why
|
|
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
|
|
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
|
|
86
|
-
|
|
87
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
- **
|
|
90
|
-
- **
|
|
91
|
-
|
|
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,
|
|
77
|
-
|
|
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:**
|
|
80
|
-
- **Backoff and batching:**
|
|
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
|
|
15
|
+
## What a receiver can verify
|
|
16
16
|
|
|
17
|
-
**
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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**,
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- A receiver that is down, restarting, or rate-limited for
|
|
53
|
-
|
|
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
|
|
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
|
|
67
|
-
|
|
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
|
|
70
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
(
|
|
112
|
-
|
|
113
|
-
- **Whether a delivery failed:**
|
|
114
|
-
and
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
package/src/SignalHouseSDK.js
CHANGED
|
@@ -6,6 +6,19 @@ import axios from "axios";
|
|
|
6
6
|
* @property {object} [headers] - Additional headers to include in the request
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SignalHouseSDKConfig
|
|
11
|
+
* @property {string} apiKey - The API key for authenticating requests to the SignalHouse API
|
|
12
|
+
* @property {string} baseUrl - The base URL for the SignalHouse API (e.g., "https://api.signalhouse.com")
|
|
13
|
+
* @property {boolean} [enableAdmin=false] - Enable admin-scoped behaviors on domains that support them
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The envelope every domain method resolves to. Non-2xx responses never reject; check `success` first.
|
|
18
|
+
* @template T
|
|
19
|
+
* @typedef {{ success: true, data: T, status: number } | { success: false, error: string|Object, status: number|null }} SdkResponse
|
|
20
|
+
*/
|
|
21
|
+
|
|
9
22
|
// Main SDK Class (SignalHouseSDK.js)
|
|
10
23
|
import { Auth } from "./domains/Auth.js";
|
|
11
24
|
import { Billing } from "./domains/Billing.js";
|
|
@@ -16,6 +29,7 @@ import { Landings } from "./domains/Landings.js";
|
|
|
16
29
|
import { Messages } from "./domains/Messages.js";
|
|
17
30
|
import { Numbers } from "./domains/Numbers.js";
|
|
18
31
|
import { Onboarding } from "./domains/Onboarding.js";
|
|
32
|
+
import { Dnc } from "./domains/Dnc.js";
|
|
19
33
|
import { Shortlinks } from "./domains/Shortlinks.js";
|
|
20
34
|
import { Subgroups } from "./domains/Subgroups.js";
|
|
21
35
|
import { Subscriptions } from "./domains/Subscriptions.js";
|
|
@@ -29,19 +43,15 @@ import { Agents } from "./domains/Agents.js";
|
|
|
29
43
|
export class SignalHouseSDK {
|
|
30
44
|
/**
|
|
31
45
|
* Initialize the SignalHouseSDK with the required configuration
|
|
32
|
-
* @param {
|
|
33
|
-
* @param {string} config.apiKey - The API key for authenticating requests to the SignalHouse API
|
|
34
|
-
* @param {string} config.baseUrl - The base URL for the SignalHouse API (e.g., "https://api.signalhouse.com")
|
|
35
|
-
* @param {boolean} [config.enableAdmin=false] - Enable admin-scoped behaviors on domains that support them.
|
|
46
|
+
* @param {SignalHouseSDKConfig} config - The configuration object for initializing the SDK
|
|
36
47
|
* @throws {Error} Throws an error if the API key or base URL is missing from the configuration
|
|
37
|
-
* @returns {SignalHouseSDK} An instance of the SignalHouseSDK
|
|
38
48
|
*/
|
|
39
|
-
constructor(config
|
|
49
|
+
constructor(config) {
|
|
40
50
|
// Validate required config parameters
|
|
41
|
-
if (!config
|
|
51
|
+
if (!config?.apiKey) {
|
|
42
52
|
throw new Error("API key is required to initialize SignalHouseSDK");
|
|
43
53
|
}
|
|
44
|
-
if (!config
|
|
54
|
+
if (!config?.baseUrl) {
|
|
45
55
|
throw new Error("Base URL is required to initialize SignalHouseSDK");
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -64,6 +74,7 @@ export class SignalHouseSDK {
|
|
|
64
74
|
this.messages = new Messages(client, multipartClient, this.enableAdmin);
|
|
65
75
|
this.numbers = new Numbers(client, multipartClient, this.enableAdmin);
|
|
66
76
|
this.onboarding = new Onboarding(client, this.enableAdmin);
|
|
77
|
+
this.dnc = new Dnc(client, this.enableAdmin);
|
|
67
78
|
this.shortlinks = new Shortlinks(client, this.enableAdmin);
|
|
68
79
|
this.subgroups = new Subgroups(client, this.enableAdmin);
|
|
69
80
|
this.subscriptions = new Subscriptions(client, this.enableAdmin);
|
|
@@ -90,19 +101,21 @@ export class SignalHouseSDK {
|
|
|
90
101
|
const instance = axios.create({ baseURL: this.baseUrl, headers: isMultipart ? {} : { "Content-Type": "application/json" } });
|
|
91
102
|
|
|
92
103
|
// Add a request interceptor to include the API key in the Authorization header
|
|
93
|
-
instance.interceptors.request.use((config) => {
|
|
104
|
+
instance.interceptors.request.use((/** @type {import('axios').InternalAxiosRequestConfig & { token?: string }} */ config) => {
|
|
94
105
|
const token = config.token || this.apiKey;
|
|
95
106
|
config.headers.Authorization = `Bearer ${token}`;
|
|
96
107
|
delete config.token; // Remove token from config to avoid sending it as a header
|
|
97
108
|
return config;
|
|
98
109
|
});
|
|
99
110
|
|
|
100
|
-
// Standardize responses
|
|
111
|
+
// Standardize responses. The fulfilled handler deliberately replaces the AxiosResponse with the
|
|
112
|
+
// SdkResponse envelope, which axios's interceptor typing does not model; the cast keeps that intent visible.
|
|
101
113
|
instance.interceptors.response.use(
|
|
102
114
|
// Standard 2xx responses
|
|
103
|
-
(response) =>
|
|
115
|
+
/** @type {(response: import('axios').AxiosResponse) => any} */
|
|
116
|
+
((response) => {
|
|
104
117
|
return { success: true, data: response.data, status: response.status };
|
|
105
|
-
},
|
|
118
|
+
}),
|
|
106
119
|
|
|
107
120
|
// Non-2xx responses
|
|
108
121
|
(error) => {
|
|
@@ -156,7 +169,7 @@ export class SignalHouseSDK {
|
|
|
156
169
|
_require(fields) {
|
|
157
170
|
for (const [name, value] of Object.entries(fields)) {
|
|
158
171
|
if (value === undefined || value === null || value === "") {
|
|
159
|
-
const error = new Error(`Missing required parameter: ${name}`);
|
|
172
|
+
const error = /** @type {Error & { status?: number }} */ (new Error(`Missing required parameter: ${name}`));
|
|
160
173
|
error.name = "SignalHouseValidationError";
|
|
161
174
|
error.status = 400; // Match API validation code
|
|
162
175
|
throw error;
|