@signalhousellc/sdk 1.0.65 → 1.0.66

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.
@@ -0,0 +1,114 @@
1
+ # Why 10DLC registrations get rejected
2
+
3
+ Loaded on demand, and only needed once something has actually been rejected.
4
+
5
+ Every cause below is drawn from a **real Signal House support ticket** or verified against the
6
+ API, with the source noted. Nothing here is general industry knowledge.
7
+
8
+ > **Review status:** engineer-verified. Still needs the named business reviewer per
9
+ > [CONTRIBUTING.md](../../../CONTRIBUTING.md), since this is compliance-bearing content.
10
+
11
+ ## How to read a rejection
12
+
13
+ `rejectionReason` is **free text, 10 to 256 characters** (verified: `campaign.validation.js`). It
14
+ is not an enum and cannot be reliably pattern-matched.
15
+
16
+ Registry errors arrive with a numeric code and a description, and the code is the reliable part.
17
+ **These are TCR's own error codes, not HTTP status codes**. They arrive *inside* the response
18
+ body of what is typically an HTTP 400, in the shape
19
+ `External service error: TCR - Request failed with status code 400 - [{ "code": 503, "description": "..." }]`.
20
+ A `503` here means "reference id already in use", not "service unavailable". Do not reason about
21
+ them as HTTP semantics.
22
+
23
+ ## Registry error codes seen in production
24
+
25
+ ### 509: "Brand does not qualify for submitted campaign use-case"
26
+
27
+ The brand is not eligible for the use case, on carrier-side vetting criteria. Two real cases:
28
+ a generic technology company rejected for `UCAAS_HIGH` (which expects a recognized UCaaS
29
+ provider), and a brand with `brandScore: 0` rejected for `DELIVERY_NOTIFICATION`.
30
+
31
+ **Not retryable.** Resubmitting the same campaign cannot fix it. The two real options are to
32
+ switch to a use case the brand qualifies for (`LOW_VOLUME` unblocks immediately), or to re-vet
33
+ the brand to earn a qualifying score.
34
+
35
+ Related: a brand with `brandScore: null` has not been scored by the registry yet. Campaigns
36
+ depending on a score will keep failing until scoring runs, and that is not something the
37
+ platform can force.
38
+
39
+ ### 501: invalid sub-use-case
40
+
41
+ Two distinct real causes share this code.
42
+
43
+ **`Invalid sub-usecases: SOCIAL`.** `SOCIAL` is not a valid sub-use-case under any parent,
44
+ neither `LOW_VOLUME` nor `MIXED`. Valid sub-use-cases for `LOW_VOLUME` are
45
+ `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`, `MARKETING`. Resubmit with
46
+ `SOCIAL` removed.
47
+
48
+ ⚠️ **Known platform bug, tracked as SHGHL-2943.** Signal House validates `subUsecases` against
49
+ the *use-case* enum, so `SOCIAL` is selectable as a sub-use-case even though the registry always
50
+ rejects it. **Delete this subsection when SHGHL-2943 ships.** It documents a defect, and a
51
+ workaround left in a skill becomes permanent.
52
+
53
+ **Punctuation in a tag.** A tag of `maria-answers` was rejected for containing a hyphen. The
54
+ campaign content was fine. Fix the tag (`mariaanswers`), then the campaign has to be
55
+ re-approved internally before it re-registers.
56
+
57
+ ### 503: "Reference ID already in use"
58
+
59
+ A duplicate `referenceId` on brand creation. The brand cannot be repaired: create a **new**
60
+ brand with the same details and **omit `referenceId` entirely**.
61
+
62
+ This is specific to a brand that landed in `TCR_REJECTED`. It is **not** the general rule: a
63
+ brand in `UNVERIFIED` can be fixed and re-submitted with an ordinary update (`PUT /brand/:brandId`
64
+ calls the registry and writes back its fresh status) or with `PUT /brand/revet/:brandId`. Only
65
+ `TCR_REJECTED` refuses both and forces recreation.
66
+
67
+ ## Platform-side rejections (before the registry ever sees it)
68
+
69
+ **No numbers on the campaign.** Verified in `campaign.service.js`: registration throws
70
+ `No phone numbers assigned to campaign with ID <campaignId>`. At least one number must be
71
+ attached to the campaign. Confirmed by a real ticket where a campaign was rejected for exactly
72
+ this.
73
+
74
+ **Campaign not internally approved.** Campaigns wait on Signal House approval before they are
75
+ upstreamed to the provider. "Not approved in our system yet" is a distinct state from any
76
+ registry rejection.
77
+
78
+ ## Content and use-case rejections
79
+
80
+ **Missing content attribute for the declared activity.** Real example: *"Campaign is for direct
81
+ lending or loan arrangement and is missing content attribute indicating direct lending."* The
82
+ campaign's declared attributes have to match what the traffic and landing page actually show.
83
+ A landing page saying "no direct lending" while the campaign reads as lending is a rejection.
84
+
85
+ ## Adjacent gotchas
86
+
87
+ **Brand deletion is blocked by campaigns.** The actual error is
88
+ `Cannot delete brand <brandId> because it has active campaigns`, and the gate is broader than
89
+ "active" suggests: it blocks on any campaign not yet `EXPIRED`. Campaigns must be cleared first.
90
+ A brand can also show `EXPIRED` in Signal House while still `ACTIVE` at the registry, and the two
91
+ are not automatically in sync.
92
+
93
+ **Brand edits and fees, settled from the code.** Ordinary brand updates are **free**:
94
+ `brandUpdate` exists in the fee schedule but is never charged anywhere. Updating landing URLs is
95
+ its own path and charges nothing. The $4.50 people ask about is `brandRevet`, and it is only
96
+ charged by an explicit re-vet (`PUT /brand/revet/:brandId`), never as a side effect of an edit.
97
+ `brandCreate` is the same $4.50 and applies to creating a new brand, which is why a
98
+ registry-rejected brand that must be recreated does cost again.
99
+
100
+ What an edit *can* cost you is time, not money: changing verification-relevant data moves the
101
+ brand `VERIFIED → UNVERIFIED` and it re-verifies.
102
+
103
+ **A failed campaign holds its reserved funds.** Funds reserved at campaign creation are released
104
+ for `PENDING_REVIEW` / `REJECTED` / `TCR_REGISTER_FAILED` when the campaign is **deleted**, not
105
+ at the moment it fails. This is deliberate, because those states are appealable and re-registerable,
106
+ but it means a customer who abandons failed campaigns keeps funds tied up. Deleting the dead
107
+ campaign frees them.
108
+
109
+ ## Open questions for the business reviewer
110
+
111
+ 1. **The full valid sub-use-case set per parent use case.** Four are confirmed for `LOW_VOLUME`
112
+ from a support ticket (`ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`,
113
+ `MARKETING`). The complete mapping should come from TCR's own documentation rather than being
114
+ inferred from the tickets we happen to have.
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: signalhouse-advisor
3
+ description: >-
4
+ Planning a Signal House integration: the order the platform actually enforces, which work is
5
+ asynchronous and how long it really takes, what the caller's role permits, and which Signal House
6
+ skill answers which question.
7
+ TRIGGER when the user asks how to build on Signal House, wants an integration plan or timeline,
8
+ asks "what do I need to send my first message", is scoping or estimating work, or is deciding
9
+ which endpoint or SDK namespace to reach for.
10
+ SKIP once the plan is settled and the work is one specific area. Hand off to signalhouse-setup,
11
+ signalhouse-10dlc, signalhouse-numbers, signalhouse-sms or signalhouse-webhooks and follow that.
12
+ ---
13
+
14
+ # Planning a Signal House integration
15
+
16
+ ## What cannot happen
17
+
18
+ **There is no path from zero to a sent message in one sitting.** Brand and campaign registration go
19
+ to external registries and carriers. Approval is measured in **days**, not minutes, and it can be
20
+ rejected. Any plan, estimate or demo script that ends "and then we send a test message" on day one
21
+ is wrong, and stating it optimistically wastes the customer's launch date rather than saving it.
22
+
23
+ **The order is enforced by the platform, not by convention.** Brand before campaign; numbers
24
+ attached at campaign creation; campaign `ACTIVE` before any send. A plan that discovers this at step
25
+ nine has to be rebuilt from step two.
26
+
27
+ **A plan is only executable if the token's role permits it.** Roles are checked server-side and
28
+ differ sharply: a customer `api` service user is bound to a single group, `developer` reaches
29
+ messaging, numbers, keys and webhooks, `billing` reaches billing only, and the `signalhouse_*` roles
30
+ are staff. Confirm what the authenticated caller actually is before planning work it cannot perform.
31
+ When the role is unknown, say so and plan against the least privilege rather than assuming `admin`.
32
+
33
+ **Nothing about cost may be stated from memory.** Fees are per group, configurable and writable.
34
+ `GET /billing/fees/:groupId` is the only correct answer to what something costs, and a number
35
+ written into a plan reads as authoritative long after it stops being true.
36
+
37
+ ## The pipeline, in the order it must happen
38
+
39
+ 1. **Credential and group.** A token, and the `groupId` it is scoped to. `signalhouse-setup`.
40
+ 2. **Webhooks.** Before the first write, not after the first surprise. Most of what follows resolves
41
+ out of band. `signalhouse-webhooks`.
42
+ 3. **Funded wallet.** Registration and number purchase are charged. An unfunded account fails at the
43
+ first billable step, which is usually the brand.
44
+ 4. **Brand.** Submitted for verification. Days. Can be rejected, and some rejections require
45
+ recreating the brand rather than editing it. `signalhouse-10dlc`.
46
+ 5. **Numbers.** These can be bought while the brand is still verifying. Buying is not gated; sending is.
47
+ `signalhouse-numbers`.
48
+ 6. **Campaign.** Created **with its numbers attached**, then registered downstream. More days.
49
+ `signalhouse-10dlc`.
50
+ 7. **Send.** Only once the campaign is `ACTIVE` and the number is `READY`. `signalhouse-sms`.
51
+
52
+ Steps 4 and 5 overlap. Steps 4 and 6 do not.
53
+
54
+ ## Asynchronous by design
55
+
56
+ Mutating operations across brand, campaign and number return **202** and complete out of band. The
57
+ HTTP response means accepted, not done. This shapes the whole integration:
58
+
59
+ - **Subscribe, do not poll.** The platform's own guidance says so, and a polling loop hits rate
60
+ limits while still finding out late.
61
+ - **Every long-running step needs a persisted state machine on the caller's side.** An integration
62
+ that holds "waiting for brand approval" in memory loses it on the next deploy.
63
+ - **Design for rejection, not just delay.** Brand and campaign both have terminal failure states. A
64
+ flow with no path for "rejected, here is why" is unfinished.
65
+
66
+ ## Which skill answers which question
67
+
68
+ | The question | Skill |
69
+ |---|---|
70
+ | How do I authenticate, what token, why a 401 | `signalhouse-setup` |
71
+ | How do I know when something finished | `signalhouse-webhooks` |
72
+ | Why is my campaign rejected or stuck, what does TCR need | `signalhouse-10dlc` |
73
+ | How do I find, buy, or release numbers, what do they cost | `signalhouse-numbers` |
74
+ | How do I send, why did a send fail, what does a message cost | `signalhouse-sms` |
75
+
76
+ If a question spans two of these, answer with the constraint first and the mechanics second. The
77
+ constraint is nearly always the part that changes the plan.
78
+
79
+ ## Where to look it up
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`).
84
+ - **Current fees and balance:** the billing endpoints, read live, per group.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: signalhouse-numbers
3
+ description: >-
4
+ Signal House phone numbers: the search, purchase and list flow, and what a purchase commits
5
+ the account to financially.
6
+ TRIGGER when the user is searching for, buying, listing or releasing Signal House numbers;
7
+ when they are scripting or bulk-buying numbers; when a purchase fails; or when they ask what
8
+ numbers cost or why their balance dropped.
9
+ SKIP for toll-free verification and short-code provisioning, for porting numbers in, and for
10
+ assigning an existing number to a campaign, which belongs to 10DLC registration.
11
+ ---
12
+
13
+ # Signal House phone numbers
14
+
15
+ ## What cannot happen
16
+
17
+ **Releasing a number is irreversible.** `DELETE /number` sets the number to `RELEASED`, and
18
+ **nothing transitions a number out of `RELEASED`**. It cannot be reactivated (reactivation
19
+ requires `DEACTIVATED`), cannot be assigned to a campaign, and cannot be re-deleted. The number
20
+ management guide says it plainly: released numbers cannot be recovered. Treat a release request
21
+ as destructive and confirm it explicitly, every time.
22
+
23
+ **A bulk local purchase has no ceiling.** `POST /number` accepts a `phoneNumbers` array with a
24
+ minimum of 1 and **no maximum**, unlike campaign assignment, which caps at 5000. Passing a whole
25
+ search page buys a whole search page. If a caller asks for "some numbers", establish the count
26
+ before purchasing, not after.
27
+
28
+ Toll-free is different: those purchases are capped at 10 per order and additionally gated by a
29
+ per-group toll-free flag. The unbounded risk above is specific to local numbers.
30
+
31
+ **A number is not sendable just because it was purchased.** It has to be attached to a campaign,
32
+ and that campaign has to be `ACTIVE`. See the `signalhouse-10dlc` skill.
33
+
34
+ ## Order of operations
35
+
36
+ 1. **Search** for available numbers, filtered to what is actually needed (state, area code,
37
+ SMS/MMS capability). Searching and NPA/NXX lookup are **free**, with no billing call on those
38
+ paths, so filter properly rather than buying to find out.
39
+ 2. **Confirm the count and the cost** before purchasing. Read current fees from
40
+ `GET /billing/fees/:groupId`. Do not assume a price or carry one over from a previous run.
41
+ 3. **Purchase** the specific numbers chosen. `subgroupId` is required, and the subgroup must
42
+ exist and be active.
43
+ 4. **List** to confirm what the account now owns and is paying for.
44
+
45
+ Numbers can be purchased while a brand is still verifying. There is no brand or campaign gate on
46
+ the purchase path. What must wait is sending, not buying.
47
+
48
+ ## Spend awareness
49
+
50
+ The rule to hold: **the account is billed for what it owns, not for what it uses.**
51
+
52
+ **Purchase is charged immediately**, per number, on both the local and toll-free paths.
53
+
54
+ **Renewal is recurring, but it is environment-gated.** There is a renewal scheduler that charges a
55
+ per-number renewal fee on a billing date. It only runs when `ENABLE_NUMBER_BILLING_RENEWALS` is
56
+ enabled, and that flag is **off by default** with the real value set per environment. So:
57
+
58
+ - Never tell a customer that numbers have no ongoing cost. The mechanism exists and is the
59
+ intended behaviour.
60
+ - Never quote the renewal as definitely active either. If it matters to the answer, check the
61
+ environment rather than asserting.
62
+ - The renewal fee is applied to toll-free numbers too, despite being named for local ones.
63
+
64
+ **Repeated renewal failures delete the number.** After a configured number of failed renewal
65
+ attempts the number is queued for deletion, and deletion is the irreversible release above. A
66
+ customer whose balance is empty does not simply go unbilled; they can lose the number.
67
+
68
+ Before any purchase, be able to state how many numbers are being bought and the current
69
+ per-number fee, read live. If either is unknown, ask rather than buy.
70
+
71
+ Never quote a price from memory or from this file. Fees are per-group and configurable,
72
+ and writable via `PUT /billing/fees/:groupId`, so a number here would be both stale and
73
+ authoritative-looking. `GET /billing/fees/:groupId` is the only correct answer to "what does this
74
+ cost".
75
+
76
+ ## Where to look it up
77
+
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`).
81
+ - **Current fees:** `GET /billing/fees/:groupId`.
82
+ - **Current balance before a bulk buy:** the wallet endpoints under `/billing/wallet`.
83
+ - **Whether a number can send:** its campaign's state, not the number's. See `signalhouse-10dlc`.
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: signalhouse-setup
3
+ description: >-
4
+ Getting a Signal House integration authenticated and wired: login vs service-user tokens, what a
5
+ token is scoped to, SDK initialization, and why webhooks come before the first write.
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.
9
+ SKIP for message sending, 10DLC registration, number purchase and webhook event semantics, which
10
+ have their own skills. This one ends at "the first authenticated call succeeds".
11
+ ---
12
+
13
+ # Signal House setup
14
+
15
+ ## What cannot happen
16
+
17
+ **A login token cannot carry a server integration.** `POST /auth` returns a **short-lived session
18
+ JWT**, measured in hours. It is the right token for a user acting in a session and the wrong token
19
+ for anything that runs unattended. A cron job built on a login token works all afternoon and fails
20
+ overnight.
21
+
22
+ Both lifetimes are set per environment (`JWT_USER_EXPIRES_IN`, `JWT_SYSTEM_EXPIRES_IN`), so treat
23
+ the session token as short and the service token as long, and read the exact values from the
24
+ Authentication guide or the environment rather than quoting a number to a customer.
25
+
26
+ **A service user cannot be bootstrapped from nothing.** `POST /user/serviceuser` needs an existing
27
+ 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.
30
+
31
+ **`role: "signalhouse_api"` is not available to customers.** The two roles `POST /user/serviceuser`
32
+ accepts are `api` (customer key, scoped to one `groupId`) and `signalhouse_api` (internal, requires
33
+ `signalhouse_admin` credentials). A customer caller asking for the internal role gets a **403**. If
34
+ an integration seems to need it, the requirement is wrong.
35
+
36
+ **The API key is shown once.** The token is signed and returned at creation and never stored on the
37
+ user record, so no endpoint can hand it back. Losing it means creating another service user, not
38
+ recovering the old key.
39
+
40
+ **A group under compliance rejection cannot create API keys at all.** Service-user creation is gated
41
+ on the group's compliance state and fails with a group-rejected error rather than a validation error.
42
+ Staff roles are exempt. When key creation fails for one group and works for another, check the
43
+ account's standing before debugging the request.
44
+
45
+ **Being authenticated does not mean being able to send.** Setup ends with a working credential.
46
+ Sending additionally requires a verified brand, an `ACTIVE` campaign, and a number in `READY` status
47
+ on it. See `signalhouse-10dlc`.
48
+
49
+ ## Order of operations
50
+
51
+ 1. **Get a token.** Human session → `POST /auth`, short-lived. Server integration → a service user,
52
+ long-lived. Do not build a server integration on a login token and plan to "refresh it later".
53
+ 2. **Know the group.** A customer service user (`role: "api"`) is bound to one `groupId`, an
54
+ 8-character id beginning with `G`. That scoping is what limits the damage of a leaked
55
+ key, so one key per group is the design, not an inconvenience to work around.
56
+ 3. **Initialize the SDK** with `apiKey` and `baseUrl`. The `apiKey` is the default credential on
57
+ every request; `options: { token }` overrides it per call, which is how an app acting for many
58
+ users should work rather than reinitializing the SDK per user.
59
+ 4. **Make one read call** and check the envelope before writing anything.
60
+ 5. **Register webhooks before the first write.** Brand registration, campaign registration, number
61
+ purchase and message delivery all resolve out of band, minutes to days later. An integration
62
+ without webhooks discovers outcomes by polling, burns rate limit doing it, and still finds out
63
+ late. See `signalhouse-webhooks`.
64
+
65
+ ## Reading the response
66
+
67
+ The SDK returns an **envelope**, and most failures arrive inside it rather than as an exception:
68
+
69
+ ```
70
+ { success: true|false, data: <payload>, error: <body>, status: <http status> }
71
+ ```
72
+
73
+ - Branch on `response.success` and `response.status`, not on a try/catch alone.
74
+ - **Rate limiting is a `429` in the envelope**, not a throw. Detect it as `response.status === 429`
75
+ and back off; matching on the error string is brittle.
76
+ - Client-side validation failures DO throw (`SignalHouseValidationError`). Those are not retryable,
77
+ and retrying an invalid payload just spends the retry budget.
78
+ - A `401` means the token expired or was revoked. Re-authenticate; do not retry the same token.
79
+
80
+ Response shapes are nested differently per endpoint and the nesting is not guessable. Read the
81
+ current shape from the API reference rather than assuming it matches a sibling endpoint.
82
+
83
+ ## Where to look it up
84
+
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.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: signalhouse-sms
3
+ description: >-
4
+ Sending SMS and MMS on Signal House: what a successful-looking response does not prove, the
5
+ parameter name that differs between the SDK and the raw API, how segments drive cost, and the
6
+ carrier and opt-out gates that stop a send.
7
+ TRIGGER when the user is sending or scripting SMS, MMS or group MMS; when a message came back
8
+ successful but never arrived; when a send fails or is rejected; when they ask about delivery
9
+ status, delivery receipts, message cost, segments, emoji, or STOP and opt-outs.
10
+ SKIP for 10DLC brand and campaign registration, for buying numbers, and for webhook endpoint
11
+ setup, which have their own skills. This one assumes a number that is already able to send.
12
+ ---
13
+
14
+ # Sending on Signal House
15
+
16
+ ## What cannot happen
17
+
18
+ **`success: true` does not prove the message went out.** When the Moderation API blocks content, the
19
+ send does **not** raise an API error. The recipients are written to the message log as **accepted but
20
+ `FAILED`** and returned like a normal send, so the caller sees a normal success envelope. Any code
21
+ that treats a 2xx as "delivered" reports a blocked campaign as a working one. Confirm outcomes from
22
+ message status or a delivery webhook, never from the send response alone.
23
+
24
+ **A number cannot send just because the account owns it.** It must be in `READY` status on an
25
+ `ACTIVE` campaign, under a brand that is `VERIFIED` or `VETTED_VERIFIED`. A brand-new number is not a
26
+ sendable number. See `signalhouse-10dlc`.
27
+
28
+ **The recipient field is named differently in the SDK and the API.** The raw API takes
29
+ `recipientPhoneNumber` (**singular**, still an array). The JS SDK takes `recipientPhoneNumbers`
30
+ (**plural**) and maps it internally. A direct HTTP caller who copies the SDK's plural name gets a
31
+ validation failure, and it reads as a mysterious 400 rather than a typo.
32
+
33
+ **Opt-out suppression is environment-gated, so a STOP is not automatically a block.**
34
+ `DNC_ENFORCEMENT_MODE` has three settings, `off`, `log` and `enforce`, and the default is **`log`**.
35
+ In `log` mode the platform queries suppressions and records what it *would* have blocked, then lets
36
+ every recipient through. So:
37
+
38
+ - Never tell a customer that STOP replies are being enforced without checking the environment.
39
+ - Never tell them suppression does not exist either. The list is real and is being recorded.
40
+ - STOP and HELP confirmations deliberately bypass suppression. They are addressed to someone who is
41
+ on the list by design, so seeing one go out is not evidence that suppression is broken.
42
+
43
+ **Sending is billed per segment, not per message.** A single non-GSM character, such as an emoji or a
44
+ curly quote pasted from a document, flips the whole body to a different encoding and
45
+ multiplies the segment count, and the cost, silently. Message body length is a billing decision.
46
+
47
+ **Carrier throughput is capped above the API.** There is a T-Mobile daily segment cap tracked per
48
+ brand EIN and an AT&T per-minute segment limit. Hitting either fails messages with a recorded reason
49
+ rather than raising an exception at the call site. A burst where most messages went out and some did not
50
+ is what this looks like in practice.
51
+
52
+ ## Order of operations
53
+
54
+ 1. **Confirm the sender can send.** Check the number's status is `READY` rather than assuming.
55
+ 2. **Know the segment count and current fee** before a bulk send. Fees are per group and read from
56
+ `GET /billing/fees/:groupId`. Never quote a price from memory or from this file.
57
+ 3. **Send.** `POST /message/sms` (JSON), `POST /message/mms` (multipart, max 5 attachments),
58
+ `POST /message/groupMessage` (multipart, shared thread). The send is queued and returns **201**
59
+ with an `insertedMessages` array holding one document per recipient.
60
+ 4. **Read the outcome asynchronously.** `statusCallbackUrl` on the send, or `MESSAGE_DELIVERED` /
61
+ `MESSAGE_FAILED` webhooks. Polling `GET /message` in a loop is the wrong shape and the rate-limit
62
+ guidance says so explicitly.
63
+
64
+ Message status runs `ENQUEUED → DEQUEUED → SENT → DELIVERED`, with `FAILED` reachable from the
65
+ middle. `SENT` means handed to the carrier, not received by the handset. Only `DELIVERED` means
66
+ delivered, and not every carrier returns one.
67
+
68
+ ## Querying messages
69
+
70
+ `GET /message` requires `groupId`, `startDate` and `endDate` **unless** you filter by `id`, which
71
+ routes to a single-record lookup and skips the date-range requirement. Omitting them without an `id`
72
+ is a 400, not an unbounded query. The API refuses rather than scanning.
73
+
74
+ ## Where to look it up
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`).
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`).
81
+ - **Why a number cannot send yet:** `signalhouse-10dlc`.
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: signalhouse-webhooks
3
+ description: >-
4
+ Signal House webhooks: how delivery actually behaves, what a receiver can and cannot trust,
5
+ and the retry window before an event is lost for good.
6
+ TRIGGER when the user is setting up, debugging, or writing a receiver for Signal House
7
+ webhooks; when webhook events appear to be missing, duplicated, or arriving late; when they
8
+ ask how to verify a webhook is genuine; or when they are choosing an authType.
9
+ SKIP for inbound message handling logic itself, for the SMS send path, and for questions about
10
+ which events exist. That list is in the webhooks guide and changes independently of this.
11
+ ---
12
+
13
+ # Signal House webhooks
14
+
15
+ ## What cannot happen
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.
20
+
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.
25
+
26
+ Practical consequences a receiver has to design around:
27
+
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.
35
+
36
+ > ⚠️ Tracked as **SHGHL-2944**. Delete this section when signing ships. A workaround left in a
37
+ > skill becomes permanent.
38
+
39
+ ## Retry behaviour, and when an event is gone
40
+
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.
44
+
45
+ Two numbers matter, and they are different things:
46
+
47
+ - **A receiver has 3 seconds to respond.** Exceed it and the attempt is a failure, even if the
48
+ 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.
57
+ - 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.
60
+
61
+ So a correct receiver:
62
+
63
+ 1. **Responds inside 3 seconds, and does the work afterwards.** Acknowledge, enqueue, process out
64
+ of band. Any processing done before responding is spending a budget you do not control, and
65
+ 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.
68
+ 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.
72
+
73
+ That makes it the durable record and the webhook merely the push. A receiver that must not
74
+ miss anything polls it to catch up after any outage, which is the only reliable recovery,
75
+ given there is no replay endpoint.
76
+
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
+ ## Setting one up
81
+
82
+ Webhooks are created per group, and numbers can additionally carry their own inbound URL.
83
+ `authType` picks how Signal House authenticates to the endpoint, and `credentials` is
84
+ `{ key, secret }`, required whenever `authType` is not `NONE`.
85
+
86
+ | authType | What is sent |
87
+ |---|---|
88
+ | `NONE` | nothing. The endpoint is open to anyone who knows the URL |
89
+ | `BASIC` | HTTP Basic; `key` is the username, `secret` is the password |
90
+ | `API_KEY` | `key` holds the key; `secret` optional |
91
+ | `OAUTH2` | `secret` is the access token sent as the Authorization value; `key` is required at create but unused in outgoing headers |
92
+
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.
96
+
97
+ **The table above does not cover per-number inbound URLs.** A number can carry
98
+ `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.
103
+
104
+ ## Where to look it up
105
+
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.
110
+ - **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.
@@ -64,7 +64,7 @@
64
64
  * @property {string} [privacyPolicyLink] - A URL linking to the privacy policy for the campaign (optional, must be at most 2048 characters if provided)
65
65
  * @property {string} [termsAndConditionsLink] - A URL linking to the terms and conditions for the campaign (optional, must be at most 2048 characters if provided)
66
66
  * @property {string} [embeddedLinkSample] - A sample of the embedded link used in the campaign (optional, must be at most 255 characters if provided)
67
- * @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, customerCareEmail, optInImageURLs, optIns, multiNumberReason (≤500)). Phone numbers cannot be changed via update — Toll-Free numbers are locked to their campaign.
67
+ * @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.
68
68
  */
69
69
 
70
70
  /**
@@ -79,7 +79,7 @@
79
79
  * @property {string} useCase - The Toll-Free use case (required, e.g. TWO_FA, GENERAL_MARKETING, HEALTHCARE, MIXED — one of the Toll-Free use-case values)
80
80
  * @property {string} messageVolume - The estimated monthly message volume tier (required, one of TEN, HUNDRED, THOUSAND, TEN_THOUSAND, HUNDRED_THOUSAND, TWO_HUNDRED_FIFTY_THOUSAND, FIVE_HUNDRED_THOUSAND, SEVEN_HUNDRED_FIFTY_THOUSAND, ONE_MILLION, FIVE_MILLION, TEN_MILLION_PLUS)
81
81
  * @property {string} programSummary - A summary of the messaging program (required, 40-500 characters)
82
- * @property {string} exampleMessage - An example message (required, at most 4096 characters; stored verbatim, no STOP/HELP appended by the server)
82
+ * @property {string} exampleMessage - An example message (required, at most 1000 characters; holds one sample per use case, stored verbatim, no STOP/HELP appended by the server)
83
83
  * @property {string} customerCareEmail - A customer-care contact email (required, valid email)
84
84
  * @property {Array<string>} optInImageURLs - URLs to opt-in proof images (required, at least one, each at most 2048 characters)
85
85
  * @property {Object} [optIns] - Opt-in method details. Each channel is optional.
@@ -88,6 +88,9 @@
88
88
  * @property {TollFreeOptIn} [optIns.keyword] - Keyword opt-in details
89
89
  * @property {TollFreeOptIn} [optIns.interactiveVoiceResponse] - IVR opt-in details
90
90
  * @property {string} [multiNumberReason] - Internally-stored reason the customer requested more than one Toll-Free number on the campaign (optional, at most 500 characters). Required when more than one phone number is assigned; captured for Signal House review only and never sent to the carrier.
91
+ * @property {string[]} [useCases] - The sender's real use-case picks (optional). `useCase` above carries the single value the carrier accepts (MIXED when there is more than one); these are what a Signal House landing page renders a consent checkbox per. Never forwarded to the carrier.
92
+ * @property {string[]} [channels] - The channels consent covers ("SMS", "MMS", "VOICE") (optional). Never forwarded to the carrier.
93
+ * @property {string} [landingId] - The Signal House landing page whose consent boxes this campaign's call-to-action describes (optional). Must be an ACTIVE page owned by the same brand and group.
91
94
  */
92
95
 
93
96
  /**
@@ -261,6 +264,15 @@ export class Campaigns {
261
264
  * @param {string} [params.sortOrder] - Sort direction ("asc" or "desc")
262
265
  * @param {string} [params.registrationType] - Filter by registration type ("TEN_DLC", "TOLL_FREE", or "SHORT_CODE")
263
266
  * @param {import('../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request
267
+ *
268
+ * Every record carries a `sentiment` object -- trailing 7-day and 30-day inbound reply sentiment,
269
+ * refreshed periodically from analytics rather than computed per request: `{ sevenDay, thirtyDay,
270
+ * updatedAt }`, each window being `{ score, label, scoredCount, positive, neutral, negative }`. The
271
+ * object is always present and fully populated, so it can be read without a presence check.
272
+ * `score` is a volume-weighted average in -100..100 and is null when nothing was scored in the
273
+ * window, which is not the same as a score of 0 -- null means nobody replied, 0 means replies
274
+ * averaged neutral. `updatedAt` is null until the first rollup writes the record, and it tracks when
275
+ * the figures last CHANGED, not when the job last ran.
264
276
  * @returns {Promise<Array|Object>} The response from the server - array when unpaginated, or { data, total, page, limit, totalPages } when paginated
265
277
  */
266
278
  async getCampaigns({
@@ -441,13 +453,18 @@ export class Campaigns {
441
453
  * @roles api, admin, developer, user
442
454
  * @param {Object} params - The parameters for capturing the opt-in image
443
455
  * @param {string} params.brandId - The ID of the brand whose generated landing page should be captured
456
+ * @param {string} [params.landingId] - The specific page to capture. Required for Toll-Free brands, which have
457
+ * one page per campaign — omitted, the request is refused rather than guessing which page to shoot. A 10DLC
458
+ * brand has a single page, so it may be omitted there
444
459
  * @param {import('../SignalHouseSDK').RequestOptions} [params.options] - Additional options for the request
445
460
  * @throws {Error} Throws an error if the brandId parameter is missing
446
461
  * @returns {Promise<Object>} The response from the server containing the hosted image's id and URL
447
462
  */
448
- async captureOptInImageFromLanding({ brandId, options = {} }) {
463
+ async captureOptInImageFromLanding({ brandId, landingId, options = {} }) {
449
464
  this.client._require({ brandId });
450
- return this.client(`/campaign/opt-in-image/from-landing`, { method: 'POST', body: { brandId }, ...options });
465
+ const body = { brandId };
466
+ if (landingId) body.landingId = landingId;
467
+ return this.client(`/campaign/opt-in-image/from-landing`, { method: 'POST', body, ...options });
451
468
  }
452
469
 
453
470
  /**