@polymorfa/sdk 0.1.0-dev.20260922093854
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/LICENSE +21 -0
- package/README.md +1881 -0
- package/dist/bridge.d.ts +31 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +39 -0
- package/dist/bridge.js.map +1 -0
- package/dist/calls/api.d.ts +109 -0
- package/dist/calls/api.d.ts.map +1 -0
- package/dist/calls/api.js +179 -0
- package/dist/calls/api.js.map +1 -0
- package/dist/calls/call.d.ts +286 -0
- package/dist/calls/call.d.ts.map +1 -0
- package/dist/calls/call.js +860 -0
- package/dist/calls/call.js.map +1 -0
- package/dist/calls/client.d.ts +124 -0
- package/dist/calls/client.d.ts.map +1 -0
- package/dist/calls/client.js +447 -0
- package/dist/calls/client.js.map +1 -0
- package/dist/calls/diagnostics.d.ts +74 -0
- package/dist/calls/diagnostics.d.ts.map +1 -0
- package/dist/calls/diagnostics.js +133 -0
- package/dist/calls/diagnostics.js.map +1 -0
- package/dist/calls/errors.d.ts +37 -0
- package/dist/calls/errors.d.ts.map +1 -0
- package/dist/calls/errors.js +52 -0
- package/dist/calls/errors.js.map +1 -0
- package/dist/calls/events.d.ts +15 -0
- package/dist/calls/events.d.ts.map +1 -0
- package/dist/calls/events.js +34 -0
- package/dist/calls/events.js.map +1 -0
- package/dist/calls/index.d.ts +6 -0
- package/dist/calls/index.d.ts.map +1 -0
- package/dist/calls/index.js +7 -0
- package/dist/calls/index.js.map +1 -0
- package/dist/calls/internal.d.ts +18 -0
- package/dist/calls/internal.d.ts.map +1 -0
- package/dist/calls/internal.js +18 -0
- package/dist/calls/internal.js.map +1 -0
- package/dist/calls/lifecycle.d.ts +111 -0
- package/dist/calls/lifecycle.d.ts.map +1 -0
- package/dist/calls/lifecycle.js +490 -0
- package/dist/calls/lifecycle.js.map +1 -0
- package/dist/calls/media.d.ts +102 -0
- package/dist/calls/media.d.ts.map +1 -0
- package/dist/calls/media.js +414 -0
- package/dist/calls/media.js.map +1 -0
- package/dist/calls/protocol.d.ts +216 -0
- package/dist/calls/protocol.d.ts.map +1 -0
- package/dist/calls/protocol.js +268 -0
- package/dist/calls/protocol.js.map +1 -0
- package/dist/calls/token.d.ts +43 -0
- package/dist/calls/token.d.ts.map +1 -0
- package/dist/calls/token.js +146 -0
- package/dist/calls/token.js.map +1 -0
- package/dist/client.d.ts +65 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +119 -0
- package/dist/client.js.map +1 -0
- package/dist/credentials.d.ts +50 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +73 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +71 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +129 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/media/whatsapp.d.ts +105 -0
- package/dist/media/whatsapp.d.ts.map +1 -0
- package/dist/media/whatsapp.js +624 -0
- package/dist/media/whatsapp.js.map +1 -0
- package/dist/messaging/bansafe.d.ts +30 -0
- package/dist/messaging/bansafe.d.ts.map +1 -0
- package/dist/messaging/bansafe.js +86 -0
- package/dist/messaging/bansafe.js.map +1 -0
- package/dist/messaging/business.d.ts +35 -0
- package/dist/messaging/business.d.ts.map +1 -0
- package/dist/messaging/business.js +145 -0
- package/dist/messaging/business.js.map +1 -0
- package/dist/messaging/calls.d.ts +9 -0
- package/dist/messaging/calls.d.ts.map +1 -0
- package/dist/messaging/calls.js +16 -0
- package/dist/messaging/calls.js.map +1 -0
- package/dist/messaging/campaigns.d.ts +19 -0
- package/dist/messaging/campaigns.d.ts.map +1 -0
- package/dist/messaging/campaigns.js +77 -0
- package/dist/messaging/campaigns.js.map +1 -0
- package/dist/messaging/channels.d.ts +23 -0
- package/dist/messaging/channels.d.ts.map +1 -0
- package/dist/messaging/channels.js +104 -0
- package/dist/messaging/channels.js.map +1 -0
- package/dist/messaging/chats.d.ts +14 -0
- package/dist/messaging/chats.d.ts.map +1 -0
- package/dist/messaging/chats.js +51 -0
- package/dist/messaging/chats.js.map +1 -0
- package/dist/messaging/client-tokens.d.ts +15 -0
- package/dist/messaging/client-tokens.d.ts.map +1 -0
- package/dist/messaging/client-tokens.js +64 -0
- package/dist/messaging/client-tokens.js.map +1 -0
- package/dist/messaging/client.d.ts +58 -0
- package/dist/messaging/client.d.ts.map +1 -0
- package/dist/messaging/client.js +100 -0
- package/dist/messaging/client.js.map +1 -0
- package/dist/messaging/contacts.d.ts +20 -0
- package/dist/messaging/contacts.d.ts.map +1 -0
- package/dist/messaging/contacts.js +75 -0
- package/dist/messaging/contacts.js.map +1 -0
- package/dist/messaging/groups.d.ts +34 -0
- package/dist/messaging/groups.d.ts.map +1 -0
- package/dist/messaging/groups.js +97 -0
- package/dist/messaging/groups.js.map +1 -0
- package/dist/messaging/identities.d.ts +9 -0
- package/dist/messaging/identities.d.ts.map +1 -0
- package/dist/messaging/identities.js +21 -0
- package/dist/messaging/identities.js.map +1 -0
- package/dist/messaging/labels.d.ts +16 -0
- package/dist/messaging/labels.d.ts.map +1 -0
- package/dist/messaging/labels.js +65 -0
- package/dist/messaging/labels.js.map +1 -0
- package/dist/messaging/media.d.ts +72 -0
- package/dist/messaging/media.d.ts.map +1 -0
- package/dist/messaging/media.js +142 -0
- package/dist/messaging/media.js.map +1 -0
- package/dist/messaging/messages.d.ts +18 -0
- package/dist/messaging/messages.d.ts.map +1 -0
- package/dist/messaging/messages.js +32 -0
- package/dist/messaging/messages.js.map +1 -0
- package/dist/messaging/observation-policies.d.ts +10 -0
- package/dist/messaging/observation-policies.d.ts.map +1 -0
- package/dist/messaging/observation-policies.js +28 -0
- package/dist/messaging/observation-policies.js.map +1 -0
- package/dist/messaging/onboarding.d.ts +53 -0
- package/dist/messaging/onboarding.d.ts.map +1 -0
- package/dist/messaging/onboarding.js +69 -0
- package/dist/messaging/onboarding.js.map +1 -0
- package/dist/messaging/presence.d.ts +12 -0
- package/dist/messaging/presence.d.ts.map +1 -0
- package/dist/messaging/presence.js +43 -0
- package/dist/messaging/presence.js.map +1 -0
- package/dist/messaging/privacy.d.ts +11 -0
- package/dist/messaging/privacy.d.ts.map +1 -0
- package/dist/messaging/privacy.js +34 -0
- package/dist/messaging/privacy.js.map +1 -0
- package/dist/messaging/profile.d.ts +13 -0
- package/dist/messaging/profile.d.ts.map +1 -0
- package/dist/messaging/profile.js +49 -0
- package/dist/messaging/profile.js.map +1 -0
- package/dist/messaging/quick-replies.d.ts +13 -0
- package/dist/messaging/quick-replies.d.ts.map +1 -0
- package/dist/messaging/quick-replies.js +45 -0
- package/dist/messaging/quick-replies.js.map +1 -0
- package/dist/messaging/quicklinks.d.ts +69 -0
- package/dist/messaging/quicklinks.d.ts.map +1 -0
- package/dist/messaging/quicklinks.js +45 -0
- package/dist/messaging/quicklinks.js.map +1 -0
- package/dist/messaging/session-configuration.d.ts +67 -0
- package/dist/messaging/session-configuration.d.ts.map +1 -0
- package/dist/messaging/session-configuration.js +2 -0
- package/dist/messaging/session-configuration.js.map +1 -0
- package/dist/messaging/sessions.d.ts +26 -0
- package/dist/messaging/sessions.d.ts.map +1 -0
- package/dist/messaging/sessions.js +108 -0
- package/dist/messaging/sessions.js.map +1 -0
- package/dist/messaging/templates.d.ts +15 -0
- package/dist/messaging/templates.d.ts.map +1 -0
- package/dist/messaging/templates.js +67 -0
- package/dist/messaging/templates.js.map +1 -0
- package/dist/messaging/testing-configuration.d.ts +89 -0
- package/dist/messaging/testing-configuration.d.ts.map +1 -0
- package/dist/messaging/testing-configuration.js +14 -0
- package/dist/messaging/testing-configuration.js.map +1 -0
- package/dist/messaging/types.d.ts +1793 -0
- package/dist/messaging/types.d.ts.map +1 -0
- package/dist/messaging/types.js +25 -0
- package/dist/messaging/types.js.map +1 -0
- package/dist/messaging/users.d.ts +9 -0
- package/dist/messaging/users.d.ts.map +1 -0
- package/dist/messaging/users.js +15 -0
- package/dist/messaging/users.js.map +1 -0
- package/dist/messaging/voip.d.ts +51 -0
- package/dist/messaging/voip.d.ts.map +1 -0
- package/dist/messaging/voip.js +293 -0
- package/dist/messaging/voip.js.map +1 -0
- package/dist/messaging/webhooks.d.ts +13 -0
- package/dist/messaging/webhooks.d.ts.map +1 -0
- package/dist/messaging/webhooks.js +48 -0
- package/dist/messaging/webhooks.js.map +1 -0
- package/dist/node.d.ts +55 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +237 -0
- package/dist/node.js.map +1 -0
- package/dist/pagination.d.ts +24 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +28 -0
- package/dist/pagination.js.map +1 -0
- package/dist/platform/api-keys.d.ts +10 -0
- package/dist/platform/api-keys.d.ts.map +1 -0
- package/dist/platform/api-keys.js +22 -0
- package/dist/platform/api-keys.js.map +1 -0
- package/dist/platform/audiences.d.ts +16 -0
- package/dist/platform/audiences.d.ts.map +1 -0
- package/dist/platform/audiences.js +46 -0
- package/dist/platform/audiences.js.map +1 -0
- package/dist/platform/audit-logs.d.ts +9 -0
- package/dist/platform/audit-logs.d.ts.map +1 -0
- package/dist/platform/audit-logs.js +20 -0
- package/dist/platform/audit-logs.js.map +1 -0
- package/dist/platform/bansafe.d.ts +24 -0
- package/dist/platform/bansafe.d.ts.map +1 -0
- package/dist/platform/bansafe.js +70 -0
- package/dist/platform/bansafe.js.map +1 -0
- package/dist/platform/billing.d.ts +13 -0
- package/dist/platform/billing.d.ts.map +1 -0
- package/dist/platform/billing.js +27 -0
- package/dist/platform/billing.js.map +1 -0
- package/dist/platform/campaigns.d.ts +28 -0
- package/dist/platform/campaigns.d.ts.map +1 -0
- package/dist/platform/campaigns.js +92 -0
- package/dist/platform/campaigns.js.map +1 -0
- package/dist/platform/customers.d.ts +23 -0
- package/dist/platform/customers.d.ts.map +1 -0
- package/dist/platform/customers.js +140 -0
- package/dist/platform/customers.js.map +1 -0
- package/dist/platform/developer-resources.d.ts +101 -0
- package/dist/platform/developer-resources.d.ts.map +1 -0
- package/dist/platform/developer-resources.js +250 -0
- package/dist/platform/developer-resources.js.map +1 -0
- package/dist/platform/developer-types.d.ts +378 -0
- package/dist/platform/developer-types.d.ts.map +1 -0
- package/dist/platform/developer-types.js +2 -0
- package/dist/platform/developer-types.js.map +1 -0
- package/dist/platform/event-stream.d.ts +114 -0
- package/dist/platform/event-stream.d.ts.map +1 -0
- package/dist/platform/event-stream.js +308 -0
- package/dist/platform/event-stream.js.map +1 -0
- package/dist/platform/media.d.ts +13 -0
- package/dist/platform/media.d.ts.map +1 -0
- package/dist/platform/media.js +33 -0
- package/dist/platform/media.js.map +1 -0
- package/dist/platform/members.d.ts +9 -0
- package/dist/platform/members.d.ts.map +1 -0
- package/dist/platform/members.js +15 -0
- package/dist/platform/members.js.map +1 -0
- package/dist/platform/opt-outs.d.ts +15 -0
- package/dist/platform/opt-outs.d.ts.map +1 -0
- package/dist/platform/opt-outs.js +36 -0
- package/dist/platform/opt-outs.js.map +1 -0
- package/dist/platform/organizations.d.ts +9 -0
- package/dist/platform/organizations.d.ts.map +1 -0
- package/dist/platform/organizations.js +15 -0
- package/dist/platform/organizations.js.map +1 -0
- package/dist/platform/project-tokens.d.ts +9 -0
- package/dist/platform/project-tokens.d.ts.map +1 -0
- package/dist/platform/project-tokens.js +16 -0
- package/dist/platform/project-tokens.js.map +1 -0
- package/dist/platform/projects.d.ts +24 -0
- package/dist/platform/projects.d.ts.map +1 -0
- package/dist/platform/projects.js +83 -0
- package/dist/platform/projects.js.map +1 -0
- package/dist/platform/quicklink-settings.d.ts +85 -0
- package/dist/platform/quicklink-settings.d.ts.map +1 -0
- package/dist/platform/quicklink-settings.js +35 -0
- package/dist/platform/quicklink-settings.js.map +1 -0
- package/dist/platform/response.d.ts +10 -0
- package/dist/platform/response.d.ts.map +1 -0
- package/dist/platform/response.js +41 -0
- package/dist/platform/response.js.map +1 -0
- package/dist/platform/security-incidents.d.ts +10 -0
- package/dist/platform/security-incidents.d.ts.map +1 -0
- package/dist/platform/security-incidents.js +22 -0
- package/dist/platform/security-incidents.js.map +1 -0
- package/dist/platform/session-bans.d.ts +13 -0
- package/dist/platform/session-bans.d.ts.map +1 -0
- package/dist/platform/session-bans.js +21 -0
- package/dist/platform/session-bans.js.map +1 -0
- package/dist/platform/session-configuration.d.ts +15 -0
- package/dist/platform/session-configuration.d.ts.map +1 -0
- package/dist/platform/session-configuration.js +36 -0
- package/dist/platform/session-configuration.js.map +1 -0
- package/dist/platform/sessions.d.ts +26 -0
- package/dist/platform/sessions.d.ts.map +1 -0
- package/dist/platform/sessions.js +121 -0
- package/dist/platform/sessions.js.map +1 -0
- package/dist/platform/sip-trunks.d.ts +141 -0
- package/dist/platform/sip-trunks.d.ts.map +1 -0
- package/dist/platform/sip-trunks.js +140 -0
- package/dist/platform/sip-trunks.js.map +1 -0
- package/dist/platform/types.d.ts +897 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/platform/types.js +2 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/raw.d.ts +26 -0
- package/dist/raw.d.ts.map +1 -0
- package/dist/raw.js +79 -0
- package/dist/raw.js.map +1 -0
- package/dist/system.d.ts +37 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +45 -0
- package/dist/system.js.map +1 -0
- package/dist/transport/body.d.ts +6 -0
- package/dist/transport/body.d.ts.map +1 -0
- package/dist/transport/body.js +33 -0
- package/dist/transport/body.js.map +1 -0
- package/dist/transport/content-disposition.d.ts +9 -0
- package/dist/transport/content-disposition.d.ts.map +1 -0
- package/dist/transport/content-disposition.js +107 -0
- package/dist/transport/content-disposition.js.map +1 -0
- package/dist/transport/http.d.ts +35 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +660 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/idempotency.d.ts +10 -0
- package/dist/transport/idempotency.d.ts.map +1 -0
- package/dist/transport/idempotency.js +13 -0
- package/dist/transport/idempotency.js.map +1 -0
- package/dist/transport/retry.d.ts +11 -0
- package/dist/transport/retry.d.ts.map +1 -0
- package/dist/transport/retry.js +45 -0
- package/dist/transport/retry.js.map +1 -0
- package/dist/transport/types.d.ts +58 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +2 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/dist/webhooks/events.d.ts +741 -0
- package/dist/webhooks/events.d.ts.map +1 -0
- package/dist/webhooks/events.js +80 -0
- package/dist/webhooks/events.js.map +1 -0
- package/dist/webhooks/index.d.ts +4 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +4 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/utilities.d.ts +33 -0
- package/dist/webhooks/utilities.d.ts.map +1 -0
- package/dist/webhooks/utilities.js +67 -0
- package/dist/webhooks/utilities.js.map +1 -0
- package/dist/webhooks/verify.d.ts +9 -0
- package/dist/webhooks/verify.d.ts.map +1 -0
- package/dist/webhooks/verify.js +77 -0
- package/dist/webhooks/verify.js.map +1 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,1881 @@
|
|
|
1
|
+
# `@polymorfa/sdk`
|
|
2
|
+
|
|
3
|
+
The handwritten Polymorfa server SDK for TypeScript and Node.js.
|
|
4
|
+
|
|
5
|
+
This package has not been published to npm. Build it from a clone of the
|
|
6
|
+
development branch and install the packed tarball:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm ci
|
|
10
|
+
npm run build:workspaces
|
|
11
|
+
npm pack -w @polymorfa/sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The Calls client is part of this package as `@polymorfa/sdk/calls`.
|
|
15
|
+
`@polymorfa/sdk/calls/internal` exists for the Polymorfa browser package;
|
|
16
|
+
applications must not import it.
|
|
17
|
+
|
|
18
|
+
Import the management and Messaging clients, errors, response metadata,
|
|
19
|
+
request options, pagination, webhook utilities, and public request/response
|
|
20
|
+
types from the package root:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
BridgeClient,
|
|
25
|
+
Client,
|
|
26
|
+
MessagingClient,
|
|
27
|
+
PolymorfaError,
|
|
28
|
+
SystemClient,
|
|
29
|
+
webhooks,
|
|
30
|
+
type RequestOptions,
|
|
31
|
+
} from "@polymorfa/sdk";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See the repository README for the complete development contract and current
|
|
35
|
+
typed-resource coverage. This package has no runtime dependencies and requires
|
|
36
|
+
Node.js 20 or newer.
|
|
37
|
+
|
|
38
|
+
## Management client and project views
|
|
39
|
+
|
|
40
|
+
`Client` has one ownership context for its lifetime. Construct an organization
|
|
41
|
+
client with an organization API key, then derive immutable project views with
|
|
42
|
+
`project(projectId)`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const platform = new Client({
|
|
46
|
+
credential: {
|
|
47
|
+
type: "organizationApiKey",
|
|
48
|
+
value: process.env.POLYMORFA_PLATFORM_API_KEY!,
|
|
49
|
+
},
|
|
50
|
+
apiVersion: "1.0.0",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const project = platform.project("project_123");
|
|
54
|
+
const events = await project.events.list({ limit: 25 });
|
|
55
|
+
console.log(events.items, events.response.metadata.requestId);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A project token can construct only a project view and requires `projectId`:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const project = new Client({
|
|
62
|
+
credential: {
|
|
63
|
+
type: "projectToken",
|
|
64
|
+
value: process.env.POLYMORFA_PROJECT_TOKEN!,
|
|
65
|
+
},
|
|
66
|
+
projectId: "project_123",
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The server verifies that initial binding. Rebinding the same project-token
|
|
71
|
+
client to a different project fails before transport. A project view exposes
|
|
72
|
+
only owner-bound management resources; organization resources such as
|
|
73
|
+
`projects`, `members`, and `billing` stay on the organization client.
|
|
74
|
+
`MessagingClient` remains separate because its session APIs and credentials
|
|
75
|
+
have a different authorization boundary.
|
|
76
|
+
|
|
77
|
+
Messaging credentials are explicit: `apiKey` for an organization server key,
|
|
78
|
+
`projectToken` for the single opaque project-token format, and `clientToken`
|
|
79
|
+
for the browser action allowlist. Server credentials fail in browser runtimes.
|
|
80
|
+
An organization key is exactly `pmfa_` plus 72 unpadded base64url characters;
|
|
81
|
+
a project token is exactly `pmfa_pt_` plus 94. The SDK checks only this public
|
|
82
|
+
v1 grammar and never decodes or decrypts the credential.
|
|
83
|
+
|
|
84
|
+
The SDK rejects `pmfa_ct_` browser tokens and CLI-only `pmfa_ls_` listener
|
|
85
|
+
credentials before a management request. It also rejects retired call-agent
|
|
86
|
+
`pmfa_at_` and socket `pmfa_wst_` tickets and simulated-device `pmfa_sd_`
|
|
87
|
+
capabilities as server API keys. It does not expose a listener,
|
|
88
|
+
`AsyncIterable`, event emitter, or forwarding API. Live forwarding belongs to
|
|
89
|
+
`polymorfa listen`.
|
|
90
|
+
|
|
91
|
+
## System and Bridge clients
|
|
92
|
+
|
|
93
|
+
`SystemClient` is credential-free. Its four methods preserve the normal
|
|
94
|
+
`ApiResponse<T>` metadata while calling public service probes:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const system = new SystemClient();
|
|
98
|
+
const [status, version, health, ping] = await Promise.all([
|
|
99
|
+
system.status(),
|
|
100
|
+
system.version(),
|
|
101
|
+
system.health(),
|
|
102
|
+
system.ping(),
|
|
103
|
+
]);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`BridgeClient` accepts only `{ type: "projectToken", value }`. It exposes
|
|
107
|
+
`routes.resolve()` for regional Bridge route discovery:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const bridge = new BridgeClient({
|
|
111
|
+
credential: {
|
|
112
|
+
type: "projectToken",
|
|
113
|
+
value: process.env.POLYMORFA_PROJECT_TOKEN!,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const route = await bridge.routes.resolve();
|
|
118
|
+
console.log(route.data.wsUrl, route.metadata.requestId);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`BridgeClient` does not open the returned WebSocket or manage its lifecycle.
|
|
122
|
+
It is also unrelated to the CLI-only SSE listener protocol. Project tokens and
|
|
123
|
+
listener credentials are not interchangeable; `pmfa_ls_` fails before a
|
|
124
|
+
Bridge request.
|
|
125
|
+
|
|
126
|
+
## Customers
|
|
127
|
+
|
|
128
|
+
Use `Client.customers` to manage project-owned Customers and their
|
|
129
|
+
Numbers. The resource covers the complete Customers contract, including
|
|
130
|
+
enablement, profile lifecycle, pairing links, recent events, and Number
|
|
131
|
+
transfers.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const customer = await platform.customers.create(
|
|
135
|
+
{
|
|
136
|
+
projectId: "project_123",
|
|
137
|
+
name: "Ada",
|
|
138
|
+
externalCustomerId: "crm_456",
|
|
139
|
+
},
|
|
140
|
+
{ idempotencyKey: crypto.randomUUID() },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const pairing = await platform.customers.createPairingLink(
|
|
144
|
+
customer.data.data.id,
|
|
145
|
+
{
|
|
146
|
+
projectId: "project_123",
|
|
147
|
+
methods: ["qr", "phone"],
|
|
148
|
+
},
|
|
149
|
+
{ idempotencyKey: crypto.randomUUID() },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
console.log(pairing.data.data.url);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The pairing URL is returned once. An idempotent replay returns the same link
|
|
156
|
+
record with `url: null`. `customers.list()` preserves both the Customer array
|
|
157
|
+
and the cursor metadata from the API response.
|
|
158
|
+
|
|
159
|
+
## BanSafe Health and telemetry
|
|
160
|
+
|
|
161
|
+
`Client.banSafe` reads Health, telemetry collection status, the fixed
|
|
162
|
+
signal catalogue, findings, restrictions, incidents, claims, and Health action
|
|
163
|
+
history. Paged methods preserve the API's `data` array and `page` metadata.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const health = await platform.banSafe.getHealth("support");
|
|
167
|
+
const telemetry = await platform.banSafe.getTelemetry("support");
|
|
168
|
+
const actions = await platform.banSafe.listHealthActions({
|
|
169
|
+
projectId: "project_123",
|
|
170
|
+
session: "support",
|
|
171
|
+
status: "succeeded",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
console.log(
|
|
175
|
+
health.data.data.health,
|
|
176
|
+
telemetry.data.data.collection.state,
|
|
177
|
+
actions.data.page.hasMore,
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Use `platform.projects` for project Safe Mode, warm-up, Ban Insurance evidence,
|
|
182
|
+
and Health policy settings. Use `platform.sessions` for one number's Safe Mode
|
|
183
|
+
override. `MessagingClient.banSafe` exposes the same settings on the Messaging
|
|
184
|
+
API for organization API keys and project tokens; its responses carry
|
|
185
|
+
`success: true` beside `data`. Browser client tokens fail before any request.
|
|
186
|
+
|
|
187
|
+
Claim `measuredCents`, `capCents`, and `amountCents` are credit quantities with
|
|
188
|
+
up to six decimal places, not integer cents. Finding acknowledgement and
|
|
189
|
+
enforcement appeals require a signed-in dashboard session and are not SDK
|
|
190
|
+
methods.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const policy = await platform.projects.getHealthPolicy("project_123");
|
|
194
|
+
await platform.projects.updateHealthPolicy("project_123", {
|
|
195
|
+
version: policy.data.data.version,
|
|
196
|
+
enabled: true,
|
|
197
|
+
threshold: 50,
|
|
198
|
+
sessionAction: "slow_down",
|
|
199
|
+
slowDownMps: 0.5,
|
|
200
|
+
emailNotification: true,
|
|
201
|
+
webhookNotification: true,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const messaging = new MessagingClient({
|
|
205
|
+
credential: {
|
|
206
|
+
type: "projectToken",
|
|
207
|
+
value: process.env.POLYMORFA_PROJECT_TOKEN!,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const safeMode = await messaging.banSafe.getSessionSafeMode("support");
|
|
211
|
+
console.log(safeMode.data.data.effective.presence);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Finding acknowledgement and restriction appeals require a signed-in dashboard
|
|
215
|
+
user. The organization-key SDK does not expose those two mutations.
|
|
216
|
+
|
|
217
|
+
## Browser client tokens
|
|
218
|
+
|
|
219
|
+
`MessagingClient.clientTokens` mints short-lived tokens and manages the live
|
|
220
|
+
session rules that authorize them. Use it only on the server. The browser-safe
|
|
221
|
+
transport and allowed-action resources live in `@polymorfa/browser`; the
|
|
222
|
+
Next.js-compatible route adapter lives in `@polymorfa/nextjs`.
|
|
223
|
+
|
|
224
|
+
Minting a client token and updating its session rules require all six client
|
|
225
|
+
delegation scopes: `sessions:manage`, `messages:write`, `contacts:read`,
|
|
226
|
+
`presence:read`, `presence:observe`, and `mcp`. The issuing key must cover
|
|
227
|
+
every action that the session rules can delegate to the browser token.
|
|
228
|
+
`clientTokens.mint` (`POST /platform/client-tokens`) is the only token issuer,
|
|
229
|
+
including for Calls; there are no call-specific tokens or tickets.
|
|
230
|
+
|
|
231
|
+
### Customer-scoped tokens (beta)
|
|
232
|
+
|
|
233
|
+
Pass a Polymorfa Customer ID in `customer` instead of `session` to mint one
|
|
234
|
+
token for the numbers a Customer owns. The issuing key also needs
|
|
235
|
+
`customers:read`, and the team must be enrolled in the Customer-scoped client
|
|
236
|
+
tokens beta.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const { data } = await messaging.clientTokens.mint({
|
|
240
|
+
customer: "0190f0b6-7c1e-7a55-9d1a-2f0c6b1e4a10",
|
|
241
|
+
ephemeralId: "user_42",
|
|
242
|
+
allow: ["send_message", "read_presence"],
|
|
243
|
+
ttlSeconds: 900,
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The token covers the numbers the Customer owns at mint time. A number moved
|
|
248
|
+
to another Customer stops working with the token on the next request; a
|
|
249
|
+
number moved to this Customer needs a new token. Each request is still
|
|
250
|
+
limited by that session's client rules, and `allow` (typed as
|
|
251
|
+
`CustomerClientTokenAction`) narrows it further. Customer-scoped tokens can't
|
|
252
|
+
use Calls or MCP. Never pass your own external ID as `customer`; look up the
|
|
253
|
+
Customer on your server first. The SDK throws `PolymorfaConfigurationError`
|
|
254
|
+
before sending if both or neither of `session` and `customer` are set, or if
|
|
255
|
+
`allow` is set without `customer`.
|
|
256
|
+
|
|
257
|
+
## Session connection lifecycle
|
|
258
|
+
|
|
259
|
+
Session administration uses Platform routes and requires a server credential.
|
|
260
|
+
The existing `MessagingClient.sessions` method names remain available.
|
|
261
|
+
Start an existing Linked Device session, then retrieve its connection status with
|
|
262
|
+
`sessions.retrieve`. The standard pairing flow is QuickLink. Direct JSON QR and phone
|
|
263
|
+
pairing-code routes require `sessions:manage` plus an explicit organization
|
|
264
|
+
entitlement; without it, the API returns `403` and the application must create
|
|
265
|
+
a QuickLink. Follow a returned operation ID with `Client.operations.get` or
|
|
266
|
+
`Client.operations.wait`.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
const started = await messaging.sessions.start("support", {
|
|
270
|
+
idempotencyKey: "start-support",
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
console.log(started.data.data.started);
|
|
274
|
+
console.log((await messaging.sessions.retrieve("support")).data);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`sessions.retrieve` is the typed source of session connection status. The
|
|
278
|
+
pinned API contract does not expose session logs or a separate
|
|
279
|
+
connection-status endpoint. The API no longer emits `session.qr` webhook
|
|
280
|
+
events. Applications must observe QuickLink state through the QuickLink flow;
|
|
281
|
+
the entitlement-gated `sessions.qr` and `sessions.requestPairingCode` methods
|
|
282
|
+
remain available only for organizations that have direct pairing enabled.
|
|
283
|
+
|
|
284
|
+
## Project templates
|
|
285
|
+
|
|
286
|
+
`MessagingClient.templates` provides the seven canonical project-template
|
|
287
|
+
operations: list, create, retrieve, update, delete, preview, and submit to Meta.
|
|
288
|
+
Definitions use the exported `TemplateDefinition` model instead of generic
|
|
289
|
+
component objects.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const created = await messaging.templates.create("support", {
|
|
293
|
+
name: "order_ready",
|
|
294
|
+
definition: {
|
|
295
|
+
version: 1,
|
|
296
|
+
kind: "standard",
|
|
297
|
+
category: "UTILITY",
|
|
298
|
+
language: "en_US",
|
|
299
|
+
body: "Hello {{name}}, your order is ready.",
|
|
300
|
+
variables: [{ name: "name", type: "text", example: "Ada" }],
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await messaging.templates.preview("support", created.data.data.id, {
|
|
305
|
+
values: { name: "Grace" },
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Keep this client on the server. Browser builders use an application-owned
|
|
310
|
+
route, such as `createTemplateBuilderRoute` from `@polymorfa/nextjs`.
|
|
311
|
+
|
|
312
|
+
## Contacts
|
|
313
|
+
|
|
314
|
+
`MessagingClient.contacts` exposes the complete Linked Device contact surface.
|
|
315
|
+
Read operations require `contacts:read`; blocking and unblocking require
|
|
316
|
+
`contacts:manage`.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const contacts = await messaging.contacts.list("support");
|
|
320
|
+
const registrations = await messaging.contacts.check("support", [
|
|
321
|
+
"+15551234567",
|
|
322
|
+
"+15557654321",
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const firstRegistration = registrations.data.data[0];
|
|
326
|
+
if (firstRegistration?.exists && firstRegistration.id) {
|
|
327
|
+
await messaging.contacts.block("support", firstRegistration.id, {
|
|
328
|
+
idempotencyKey: "block-abusive-contact",
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log(contacts.data.data, contacts.metadata.requestId);
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The resource also provides `retrieve`, `picture`, `info`, `devices`,
|
|
336
|
+
`businessProfile`, `blocklist`, and `unblock`. Contact operations are not
|
|
337
|
+
available for Cloud API sessions.
|
|
338
|
+
|
|
339
|
+
## Polymorfa Calls
|
|
340
|
+
|
|
341
|
+
`MessagingClient.voip` controls calls from a server. Every incoming call rings
|
|
342
|
+
until a participant accepts or rejects it; nothing answers automatically.
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
const placed = await messaging.voip.place(
|
|
346
|
+
{ session: "support", to: "+15551234567", participant: "agent-7" },
|
|
347
|
+
{ idempotencyKey: "place-order-1042" },
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const accepted = await messaging.voip.accept(incomingCallId, {
|
|
351
|
+
exclusive: true,
|
|
352
|
+
participant: "agent-7",
|
|
353
|
+
});
|
|
354
|
+
console.log(accepted.data.data.answeredBy); // "server:agent-7"
|
|
355
|
+
|
|
356
|
+
await messaging.voip.addParticipant(placed.data.data.callId, {
|
|
357
|
+
to: "+15557654321",
|
|
358
|
+
});
|
|
359
|
+
await messaging.voip.leave(incomingCallId, { connectionId: "conn_desk_1" });
|
|
360
|
+
await messaging.voip.end(placed.data.data.callId);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
- `place` requires `session` with a server credential and accepts `video`,
|
|
364
|
+
`exclusive`, and `participant`. Send an idempotency key to retry safely.
|
|
365
|
+
- `accept` answers a ringing call. Later accepts from other participants join
|
|
366
|
+
the call unless a participant claimed it with `exclusive: true`; those
|
|
367
|
+
requests fail with `409 call_claimed` (`PolymorfaConflictError`). Repeating
|
|
368
|
+
an accept as the same participant has no further effect.
|
|
369
|
+
- `reject` declines a ringing call and fails with `409 call_not_ringing`
|
|
370
|
+
otherwise.
|
|
371
|
+
- `leave` closes one media connection. `end` ends the call for everyone.
|
|
372
|
+
- `addParticipant` invites another WhatsApp user and returns a
|
|
373
|
+
`VoipParticipant`.
|
|
374
|
+
|
|
375
|
+
A server credential acts as `server:<participant>`; `participant` matches
|
|
376
|
+
`[A-Za-z0-9._:@-]{1,128}` and defaults to `default`. A client token acts as its
|
|
377
|
+
own participant, so the SDK rejects `participant` for client tokens. The SDK
|
|
378
|
+
checks `participant` and `connectionId` (`[A-Za-z0-9_-]{8,64}`) before sending.
|
|
379
|
+
|
|
380
|
+
`voip.retrieveCallSettings(session)` and `voip.updateCallSettings(session,
|
|
381
|
+
{ conferenceMode, inboundRoute, sipTrunkId, sipClaim, hostCloudApiCalls })` read and change the
|
|
382
|
+
session's call settings through `/platform/sessions/{session}/call-settings`.
|
|
383
|
+
`callsEnabled: false` turns calling off for the session: placing, answering,
|
|
384
|
+
joining, inviting and media fail with `PolymorfaAuthorizationError`
|
|
385
|
+
(`calls_disabled`), incoming calls are declined, and calls in progress
|
|
386
|
+
continue. `conferenceMode` (default `true`) lets every participant you connect
|
|
387
|
+
to a call (browser, app and server connections, and SIP trunk callers) hear
|
|
388
|
+
the WhatsApp party and each other; with `false`, each hears only the WhatsApp
|
|
389
|
+
party. The WhatsApp party always hears all of your participants, and nobody
|
|
390
|
+
hears their own audio in either mode. `inboundRoute` is `clients` (the default) or
|
|
391
|
+
`sip_trunk`, which also sends incoming calls to `sipTrunkId`; `sipClaim`
|
|
392
|
+
(default `true`) makes the trunk's answer claim the call. On a Cloud API
|
|
393
|
+
session, `hostCloudApiCalls: true` has Polymorfa Calls answer incoming calls;
|
|
394
|
+
with the default `false`, your Graph API integration answers them. An update changes
|
|
395
|
+
only the settings you send. Pass the `revision` you read as
|
|
396
|
+
`expectedRevision` to fail with `PolymorfaConflictError` (`state_conflict`) if
|
|
397
|
+
the settings changed meanwhile.
|
|
398
|
+
These methods require a server credential.
|
|
399
|
+
|
|
400
|
+
`voip.report(callId, report)` sends diagnostics your app measured for one of
|
|
401
|
+
its media connections: `{ kind: "quality", connectionId, quality }` with at
|
|
402
|
+
least one of `rttMs`, `jitterMs`, `packetsLost`, `packetsReceived`,
|
|
403
|
+
`audioCodec`, `videoCodec`, `candidateType` and `reconnects`, or
|
|
404
|
+
`{ kind: "error", connectionId, error: { code } }`. `client` optionally names
|
|
405
|
+
the SDK (`sdk`, `version`, `platform`). The SDK rejects fields the platform
|
|
406
|
+
does not accept before sending. The platform accepts one quality report per
|
|
407
|
+
connection every 5 seconds and 20 error reports per minute, while the call is
|
|
408
|
+
live and for 10 minutes after it ends. Treat reports as best-effort: do not
|
|
409
|
+
retry a `4xx`, and drop reports refused with `429` or `503`. Client tokens
|
|
410
|
+
need the `voip_signal` action and cannot send `participant`. The browser and
|
|
411
|
+
Calls clients send these reports for you.
|
|
412
|
+
|
|
413
|
+
## SIP trunks
|
|
414
|
+
|
|
415
|
+
`Client.sipTrunks` manages the SIP trunks that connect a PBX to a project's
|
|
416
|
+
calls. SIP trunks are part of Calls and need no enrollment.
|
|
417
|
+
Team clients name the project on `list` and `create`; project clients use their
|
|
418
|
+
own project.
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
const project = platform.project("018f0000-0000-7000-8000-000000000002");
|
|
422
|
+
const { data } = await project.sipTrunks.create({
|
|
423
|
+
name: "Head office PBX",
|
|
424
|
+
direction: "both",
|
|
425
|
+
outbound: { targetUri: "sips:pbx.example.com", transport: "tls" },
|
|
426
|
+
inbound: { session: "support", allowedAddresses: ["203.0.113.10"] },
|
|
427
|
+
});
|
|
428
|
+
// Store data.inboundCredentials now; the password is not returned again.
|
|
429
|
+
await project.sipTrunks.update(data.trunk.id, {
|
|
430
|
+
enabled: false,
|
|
431
|
+
expectedRevision: data.trunk.revision,
|
|
432
|
+
});
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
`retrieve`, `update`, `delete`, and `rotateCredentials` take a trunk ID. A
|
|
436
|
+
project client built from a team key reads the trunk first and refuses a trunk
|
|
437
|
+
of another project with `PolymorfaNotFoundError`. Conflicts raise
|
|
438
|
+
`PolymorfaConflictError` with `code` `sip_trunk_in_use`,
|
|
439
|
+
`sip_trunk_revision_conflict`, `sip_trunk_limit`, or `state_conflict`.
|
|
440
|
+
|
|
441
|
+
## Calls and stable user identity
|
|
442
|
+
|
|
443
|
+
The `calls`, `identities`, and `users` resources use public Polymorfa user IDs.
|
|
444
|
+
Identity resolution accepts an ID, phone number, or BSUID. Calling and security
|
|
445
|
+
code checks require a connected Linked Device Number; identity resolution also
|
|
446
|
+
supports Cloud Numbers when their business portfolio is configured.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
await messaging.calls.reject(
|
|
450
|
+
"support",
|
|
451
|
+
incomingCallId,
|
|
452
|
+
{ from: callerId },
|
|
453
|
+
{ idempotencyKey: incomingCallId },
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const identity = await messaging.identities.resolve("support", {
|
|
457
|
+
phoneNumber: "+15551234567",
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (identity.data.data.id) {
|
|
461
|
+
const code = await messaging.users.getSecurityCode(
|
|
462
|
+
"support",
|
|
463
|
+
identity.data.data.id,
|
|
464
|
+
);
|
|
465
|
+
console.log(code.data.data.numericCode, code.data.data.qrCode);
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
`calls.reject` requires `chats:manage`. Its call ID must contain 1 through 128
|
|
470
|
+
characters and the JSON `from` field must contain the incoming caller's
|
|
471
|
+
public Polymorfa user ID or E.164 phone number. Path identifiers are URL-encoded.
|
|
472
|
+
The SDK sends an idempotency key
|
|
473
|
+
when supplied and only permits automatic retries of this POST when that key is
|
|
474
|
+
nonempty. This session-scoped route is separate from the Polymorfa Calls
|
|
475
|
+
routes on `MessagingClient.voip`.
|
|
476
|
+
|
|
477
|
+
The pinned OpenAPI declares a generic synchronous `SuccessResponse` for call
|
|
478
|
+
rejection. The live runner returns
|
|
479
|
+
`{ success: true, data: { status: "REJECTED" } }`. A caller can explicitly send
|
|
480
|
+
`Prefer: respond-async` through `RequestOptions.headers`, in which case the live
|
|
481
|
+
RPC returns HTTP 202 with `{ success: true, data: { requestId } }`.
|
|
482
|
+
`RejectCallResponse` represents all three source-observable shapes.
|
|
483
|
+
|
|
484
|
+
`identities.resolve` requires `contacts:read`. `ResolveIdentityParams` is a discriminated
|
|
485
|
+
union that permits exactly one of these inputs:
|
|
486
|
+
|
|
487
|
+
- `phoneNumber`: digits with an optional leading `+`; the runner trims
|
|
488
|
+
surrounding whitespace and returns a normalized leading `+` when known
|
|
489
|
+
- `id`: a decimal Polymorfa user ID
|
|
490
|
+
- `username`: 3 through 35 characters, with an optional four-digit
|
|
491
|
+
`usernameKey`
|
|
492
|
+
|
|
493
|
+
`usernameKey` is invalid without `username`, and competing identity inputs are
|
|
494
|
+
rejected before runner dispatch. The response can contain the stable `id`, a
|
|
495
|
+
phone number, a BSUID, a username, and `keyRequired` when
|
|
496
|
+
WhatsApp needs the username's four-digit key. The source exposes no bulk
|
|
497
|
+
resolution, search, list, pagination, or retained identity history.
|
|
498
|
+
|
|
499
|
+
`users.getSecurityCode` also requires `contacts:read` and accepts only a stable
|
|
500
|
+
decimal Polymorfa user ID. The result contains that ID,
|
|
501
|
+
optional known aliases, a 60-digit `numericCode`, and a base64-encoded display
|
|
502
|
+
`qrCode`. The runner deliberately excludes WhatsApp's private verification QR
|
|
503
|
+
payload, and the API schema rejects an upstream response that does not match
|
|
504
|
+
the public shape. The API marks successful and failed responses
|
|
505
|
+
`Cache-Control: private, no-store`; callers can inspect that header through
|
|
506
|
+
`ApiResponse.metadata.headers`. This GET is always synchronous. The source
|
|
507
|
+
exposes no security-code list, cache, history, refresh, or verification-submit
|
|
508
|
+
operation.
|
|
509
|
+
|
|
510
|
+
All three methods preserve request IDs and response metadata and accept the
|
|
511
|
+
standard timeout, cancellation, API-version, custom-header, and retry options.
|
|
512
|
+
|
|
513
|
+
## Groups
|
|
514
|
+
|
|
515
|
+
`MessagingClient.groups` exposes all 21 operations in the pinned Groups tag.
|
|
516
|
+
Reads require `groups:read`; mutations require `groups:manage`.
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
const groups = await messaging.groups.list("support");
|
|
520
|
+
const group = groups.data.data[0];
|
|
521
|
+
|
|
522
|
+
if (group) {
|
|
523
|
+
const participants = await messaging.groups.listParticipants(
|
|
524
|
+
"support",
|
|
525
|
+
group.id,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
await messaging.groups.addParticipants(
|
|
529
|
+
"support",
|
|
530
|
+
group.id,
|
|
531
|
+
{ participants: ["15551234567"] },
|
|
532
|
+
{ idempotencyKey: "add-support-participant" },
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
console.log(participants.data.data, participants.metadata.requestId);
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
The resource includes create and retrieve, invite-code lookup and revocation,
|
|
540
|
+
join-info lookup, join and leave, participant add/remove/promote/demote,
|
|
541
|
+
subject and description updates, profile pictures, and all four group
|
|
542
|
+
permission settings. `delete` maps the API's DELETE leave alias; it does not
|
|
543
|
+
delete the remote group for every participant. Group identifiers and session
|
|
544
|
+
names are encoded as path segments, and every mutation accepts idempotency,
|
|
545
|
+
timeout, cancellation, and API-version request options.
|
|
546
|
+
|
|
547
|
+
The source exposes no pagination for group or participant lists. Browser client
|
|
548
|
+
tokens cannot access Groups routes because no Groups action exists in the
|
|
549
|
+
client-token allowlist; use a server API key with the required scope.
|
|
550
|
+
|
|
551
|
+
## Messages
|
|
552
|
+
|
|
553
|
+
`MessagingClient.messages` maps the complete five-operation Messages tag:
|
|
554
|
+
`send`, `markSeen`, `setTyping`, `react`, and `star`. All five require
|
|
555
|
+
`messages:write` when called with a server API key and accept the standard
|
|
556
|
+
`RequestOptions`, including idempotency, cancellation, timeouts, custom
|
|
557
|
+
headers, and API-version overrides.
|
|
558
|
+
|
|
559
|
+
The source has one send route rather than separate routes for each message
|
|
560
|
+
kind. `SendMessageRequest` is therefore a union of the exact typed payloads for
|
|
561
|
+
text, image/file/voice/video media, polls, locations, contacts, phone-number
|
|
562
|
+
requests, products, product lists, orders, lists, buttons, address messages,
|
|
563
|
+
and flows. Template sends use `SendTemplateMessageRequest`. Select exactly one
|
|
564
|
+
message kind inside `content`; `conversation` selects its destination.
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
await messaging.messages.send(
|
|
568
|
+
"support",
|
|
569
|
+
{
|
|
570
|
+
conversation: { phoneNumber: "+15551234567" },
|
|
571
|
+
content: {
|
|
572
|
+
buttons: {
|
|
573
|
+
body: "Continue with this request?",
|
|
574
|
+
buttons: [
|
|
575
|
+
{ type: "reply", text: "Continue", id: "continue" },
|
|
576
|
+
{ type: "reply", text: "Cancel", id: "cancel" },
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
quotedMessage: {
|
|
581
|
+
id: "739182640518204",
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
{ idempotencyKey: "reply-to-message-id" },
|
|
585
|
+
);
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Reply context uses `quotedMessage`; forwarding is represented by
|
|
589
|
+
`isForwarded`. Neither is a separate endpoint. The pinned contract exposes no
|
|
590
|
+
message history, list, search, or standalone forward/reply route.
|
|
591
|
+
|
|
592
|
+
Client tokens can call all five Messages operations only when the corresponding
|
|
593
|
+
live rule is enabled: `send_message` for send and star, `send_reaction` for
|
|
594
|
+
react, `send_typing` for typing, and `send_seen` for seen markers. Send and
|
|
595
|
+
reaction are also subject to recipient rules and send limits. Edit and delete
|
|
596
|
+
are not Messages routes: they remain `MessagingClient.chats.editMessage` and
|
|
597
|
+
`deleteMessage`, require `chats:manage` with a server key, and are not in the
|
|
598
|
+
client-token allowlist.
|
|
599
|
+
|
|
600
|
+
### Idempotent sends
|
|
601
|
+
|
|
602
|
+
These methods send an `Idempotency-Key` on every call:
|
|
603
|
+
|
|
604
|
+
- `messages.send` and `messages.react`
|
|
605
|
+
- `chats.editMessage` and `chats.deleteMessage`
|
|
606
|
+
- `channels.reactToMessage`
|
|
607
|
+
- `campaigns.create` and `campaigns.launch`
|
|
608
|
+
|
|
609
|
+
If you don't pass `idempotencyKey`, the SDK generates a random UUID for the
|
|
610
|
+
call. Every automatic retry of that call reuses the key, so the API never
|
|
611
|
+
runs the write twice. If the first attempt succeeded but its response was lost,
|
|
612
|
+
the retry fails with `PolymorfaConflictError` (`idempotency_completed`) instead
|
|
613
|
+
of sending the message again. Pass your own key, such as
|
|
614
|
+
an order event ID, to deduplicate across processes or restarts:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
await messaging.messages.send(
|
|
618
|
+
"support",
|
|
619
|
+
{
|
|
620
|
+
conversation: { phoneNumber: "+15551234567" },
|
|
621
|
+
content: { text: "Shipped" },
|
|
622
|
+
},
|
|
623
|
+
{ idempotencyKey: `order-${orderId}-shipped` },
|
|
624
|
+
);
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
The API keeps each key for 24 hours per credential. Reusing a key for a
|
|
628
|
+
different request fails with `PolymorfaConflictError` (`idempotency_conflict`).
|
|
629
|
+
A retry that arrives while the first request is still running receives
|
|
630
|
+
`idempotency_in_progress`, and the SDK retries it after `Retry-After`. When a
|
|
631
|
+
response carries `Idempotent-Replayed: true`, the SDK treats it as final and
|
|
632
|
+
does not retry. A replayed `result_unknown`, or `idempotency_outcome_unknown`,
|
|
633
|
+
means the first attempt's outcome is unknown, so check message events before
|
|
634
|
+
you send again with a new key.
|
|
635
|
+
|
|
636
|
+
`BrowserMessagingClient.messages.send` and `react` generate keys the same way.
|
|
637
|
+
|
|
638
|
+
## Messaging media
|
|
639
|
+
|
|
640
|
+
`MessagingClient.media` is distinct from `Client.media`. It covers the
|
|
641
|
+
Messaging Media tag for Linked Device sessions. Every download method requires
|
|
642
|
+
a server credential with `media:read`; client tokens cannot call media routes.
|
|
643
|
+
|
|
644
|
+
| Method | Result |
|
|
645
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
646
|
+
| `downloadStream(mediaId, options)` | `{ body: ReadableStream<Uint8Array>, contentType?, contentLength?, filename?, requestId?, redirected, metadata }` |
|
|
647
|
+
| `downloadBlob(mediaId, options)` | `{ blob, filename?, requestId? }`, with `blob.type` set from `Content-Type` |
|
|
648
|
+
| `downloadUrl(mediaId, options)` | `{ streamed: false, url, expiresAt? }` or `{ streamed: true, url: undefined }` |
|
|
649
|
+
| `download(mediaId, options)` | `ApiResponse<ArrayBuffer>` (buffers the whole file) |
|
|
650
|
+
| `retrieve(mediaId)` | `MessagingMediaInfo` |
|
|
651
|
+
| `persist(mediaId)` | Saves the object to the tenant's object storage; requires `media:manage` |
|
|
652
|
+
|
|
653
|
+
The API either streams the file or answers `302` with a fresh signed storage
|
|
654
|
+
URL. `downloadStream`, `downloadBlob` and `downloadUrl` send requests with
|
|
655
|
+
`redirect: "manual"`. When the SDK follows a redirect, it requests the storage
|
|
656
|
+
URL without the `Authorization` header, custom headers or cookies.
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
import { Readable } from "node:stream";
|
|
660
|
+
|
|
661
|
+
const download = await messaging.media.downloadStream("media-id", {
|
|
662
|
+
signal: request.signal,
|
|
663
|
+
});
|
|
664
|
+
console.log(download.contentType, download.filename, download.requestId);
|
|
665
|
+
Readable.fromWeb(download.body).pipe(response);
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
- `downloadStream` retries only before it returns the body. After that, a
|
|
669
|
+
failed read errors the stream with `PolymorfaConnectionError`, or with
|
|
670
|
+
`PolymorfaCancelledError` if the caller aborted it. `timeoutMs` applies until
|
|
671
|
+
response headers arrive. `signal` also cancels a body that is still being
|
|
672
|
+
read.
|
|
673
|
+
- `filename` comes from `Content-Disposition`. The SDK prefers the RFC 6266
|
|
674
|
+
`filename*` value and keeps only the last path segment. Treat it as a
|
|
675
|
+
display name, not as a path.
|
|
676
|
+
- `downloadUrl` does not follow the redirect. It returns the signed URL, so
|
|
677
|
+
your app can hand a browser a direct link instead of proxying the bytes.
|
|
678
|
+
The URL is short-lived and acts as a bearer credential: never log or store
|
|
679
|
+
it, and give it only to a user you have already authorized for this media.
|
|
680
|
+
`expiresAt` is derived from SigV4 or `Expires` query parameters when they
|
|
681
|
+
are present. When the API streams the file instead, `downloadUrl` cancels
|
|
682
|
+
the body and returns `{ streamed: true }`.
|
|
683
|
+
- API errors keep the existing error classes, such as
|
|
684
|
+
`PolymorfaNotFoundError` for 404 and `PolymorfaAuthorizationError` for 403.
|
|
685
|
+
A failed storage request raises `PolymorfaError` with code
|
|
686
|
+
`media_storage_error`. A redirect to anything other than HTTPS raises code
|
|
687
|
+
`invalid_redirect`.
|
|
688
|
+
|
|
689
|
+
`download()` buffers the response into an `ArrayBuffer` and keeps its existing
|
|
690
|
+
behavior. Timeout and cancellation stay active while the body is buffered.
|
|
691
|
+
`response.metadata.headers` keeps `Content-Type`. Credentialed clients send
|
|
692
|
+
`download()` requests with `redirect: "error"`, so `download()` fails when the
|
|
693
|
+
API answers with a storage redirect. Use `downloadStream` or `downloadBlob`
|
|
694
|
+
when media may be served from object storage.
|
|
695
|
+
|
|
696
|
+
### Write media to a file (Node.js)
|
|
697
|
+
|
|
698
|
+
`@polymorfa/sdk/node` contains the Node-only helpers, which import `node:fs`
|
|
699
|
+
and `node:crypto`. The main entry does not import `node:fs`.
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
import { downloadMediaToFile } from "@polymorfa/sdk/node";
|
|
703
|
+
|
|
704
|
+
await downloadMediaToFile(messaging.media, "media-id", "./attachment.bin", {
|
|
705
|
+
signal,
|
|
706
|
+
});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
The helper writes to a sibling temporary file (`.<name>.<uuid>.partial`,
|
|
710
|
+
mode `0600`) and renames it into place when the download finishes. If the
|
|
711
|
+
download fails or is aborted, the helper deletes the temporary file and leaves
|
|
712
|
+
any existing file at the destination unchanged. With `overwrite: false`, the
|
|
713
|
+
final step is an atomic link that fails with `PolymorfaConflictError` (code
|
|
714
|
+
`file_exists`) when the destination exists. `maxBytes` stops the download with
|
|
715
|
+
`media_too_large`, either from `Content-Length` before any bytes are read or
|
|
716
|
+
once too many bytes arrive. `writeStreamToFile(body, path, options)` applies
|
|
717
|
+
the same steps to any web stream.
|
|
718
|
+
|
|
719
|
+
### Download media directly from WhatsApp
|
|
720
|
+
|
|
721
|
+
When a project does not persist media, image, video, audio, document and
|
|
722
|
+
sticker message webhooks include `media`. This field is a base64 protobuf of
|
|
723
|
+
the WhatsApp attachment, including its CDN URL, `directPath`, hashes and
|
|
724
|
+
`mediaKey`. The SDK can fetch the encrypted file directly from the WhatsApp
|
|
725
|
+
CDN and decrypt it locally. This makes no Polymorfa API call and sends no
|
|
726
|
+
Polymorfa credential.
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
import { constructWebhookEvent, isEvent } from "@polymorfa/sdk";
|
|
730
|
+
import {
|
|
731
|
+
downloadWhatsAppMediaToFile,
|
|
732
|
+
nodeMediaCrypto,
|
|
733
|
+
} from "@polymorfa/sdk/node";
|
|
734
|
+
|
|
735
|
+
const event = await constructWebhookEvent(rawBody, signature, secret);
|
|
736
|
+
if (
|
|
737
|
+
isEvent(event, "message.received") &&
|
|
738
|
+
typeof event.payload.media === "string"
|
|
739
|
+
) {
|
|
740
|
+
// Buffered, verified before any byte is released (default).
|
|
741
|
+
const media = await messaging.media.downloadFromWhatsApp(event.payload, {
|
|
742
|
+
maxBytes: 50 * 1024 * 1024,
|
|
743
|
+
});
|
|
744
|
+
const blob = await media.blob(); // typed with media.mimetype
|
|
745
|
+
|
|
746
|
+
// Streamed to disk; renamed into place only after verification.
|
|
747
|
+
await downloadWhatsAppMediaToFile(event.payload, "./incoming.bin");
|
|
748
|
+
|
|
749
|
+
// Streamed to a consumer that can discard partial output on error.
|
|
750
|
+
const live = await messaging.media.downloadFromWhatsApp(event.payload, {
|
|
751
|
+
crypto: nodeMediaCrypto,
|
|
752
|
+
verify: "streaming",
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
`downloadWhatsAppMedia(input, options)` is the standalone form.
|
|
758
|
+
`decodeWhatsAppMedia(base64, messageType)` returns the decoded descriptor.
|
|
759
|
+
`deriveWhatsAppMediaKeys` and `decryptWhatsAppMedia(bytesOrStream, keys,
|
|
760
|
+
options)` cover apps that fetch the encrypted bytes themselves.
|
|
761
|
+
|
|
762
|
+
Verification follows the WhatsApp client order:
|
|
763
|
+
|
|
764
|
+
1. HKDF-SHA256 expands `mediaKey` to 112 bytes with the per-type info string.
|
|
765
|
+
Stickers use the image string. The first 80 bytes give the IV, cipher key
|
|
766
|
+
and MAC key.
|
|
767
|
+
2. The encrypted file is `ciphertext || mac10`. The SDK checks its SHA-256
|
|
768
|
+
against `fileEncSha256` when that hash is present.
|
|
769
|
+
3. The SDK compares the HMAC-SHA256 of `iv || ciphertext`, truncated to 10
|
|
770
|
+
bytes, in constant time.
|
|
771
|
+
4. The SDK decrypts with AES-256-CBC and strict PKCS#7 unpadding, then checks
|
|
772
|
+
the plaintext SHA-256 against `fileSha256`. A descriptor without
|
|
773
|
+
`fileSha256` is rejected.
|
|
774
|
+
|
|
775
|
+
A failure raises `PolymorfaMediaIntegrityError`. Its `code` is one of
|
|
776
|
+
`media_invalid_descriptor`, `media_too_short`, `media_too_large`,
|
|
777
|
+
`media_invalid_ciphertext`, `media_enc_hash_mismatch`, `media_mac_mismatch`,
|
|
778
|
+
`media_invalid_padding` or `media_hash_mismatch`.
|
|
779
|
+
|
|
780
|
+
- **Verification modes.** The default WebCrypto backend buffers the file and
|
|
781
|
+
releases plaintext only after every check passes. `nodeMediaCrypto`
|
|
782
|
+
decrypts incrementally and supports `verify: "streaming"`, which emits
|
|
783
|
+
plaintext before the MAC is verified. If verification then fails, the
|
|
784
|
+
stream errors and consumers must discard everything they received. Asking
|
|
785
|
+
for `streaming` without an incremental backend raises
|
|
786
|
+
`PolymorfaConfigurationError`.
|
|
787
|
+
- **Limits.** `maxBytes` defaults to 256 MiB and applies to the plaintext.
|
|
788
|
+
The SDK also caps the encrypted size at the padded `fileLength` from the
|
|
789
|
+
descriptor. It rejects an oversized `Content-Length` before reading the
|
|
790
|
+
body. The descriptor input is limited to 1 MiB of base64. The decoder
|
|
791
|
+
interprets only varint and length-delimited fields, and rejects wrong key
|
|
792
|
+
or hash lengths.
|
|
793
|
+
- **Hosts.** The SDK tries the descriptor `url` first. It then tries
|
|
794
|
+
`https://mmg.whatsapp.net` with `directPath` and the `hash`, `mms-type` and
|
|
795
|
+
`__wa-mms` parameters. Every URL, including redirect targets, must be HTTPS
|
|
796
|
+
on `*.whatsapp.net` with the default port.
|
|
797
|
+
- **Browsers.** `mmg.whatsapp.net` returned `access-control-allow-origin: *`
|
|
798
|
+
to an unauthenticated probe on 2026-09-17. This was checked only on error
|
|
799
|
+
responses, not on a real object. Even if a browser can fetch the file,
|
|
800
|
+
decrypting there means giving the browser the `mediaKey`. Keep the
|
|
801
|
+
descriptor on the server and use the `whatsapp` mode of
|
|
802
|
+
`createMediaDownloadRoute` from `@polymorfa/nextjs`.
|
|
803
|
+
- **Privacy.** `media` contains a decryption key. Treat stored webhook
|
|
804
|
+
payloads as secrets, never log them, and delete them when your retention
|
|
805
|
+
period ends. The CDN URL expires, and once WhatsApp removes the object the
|
|
806
|
+
file can no longer be downloaded. Messages with `mediaUrl` (persisted media)
|
|
807
|
+
have no `media` field, so use the Media API for them.
|
|
808
|
+
|
|
809
|
+
The pinned source specifies no maximum download size. Retrieve metadata first
|
|
810
|
+
when an application must enforce its own memory limit. It exposes no Messaging
|
|
811
|
+
media upload, deletion, resumable upload, range-download, or list endpoint.
|
|
812
|
+
Message-send `url` and `base64` fields are send inputs, not media-upload APIs.
|
|
813
|
+
Media routes are absent from the client-token allowlist, so every API method
|
|
814
|
+
requires a server API key. `persist` accepts an idempotency key through the
|
|
815
|
+
standard `RequestOptions`.
|
|
816
|
+
|
|
817
|
+
`MessagingMediaInfo.s3Url` includes `null` because the API returns a null value
|
|
818
|
+
before persistence even though the generated schema marks the field as
|
|
819
|
+
optional. The SDK type reflects the verified response.
|
|
820
|
+
|
|
821
|
+
## Labels and observation policies
|
|
822
|
+
|
|
823
|
+
`MessagingClient.labels` covers all six direct Linked Device label operations.
|
|
824
|
+
Reads require `labels:read`; create, update, delete, and chat-label replacement
|
|
825
|
+
require `labels:manage`.
|
|
826
|
+
|
|
827
|
+
```ts
|
|
828
|
+
const labels = await messaging.labels.list("support", {
|
|
829
|
+
includeObservation: true,
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
await messaging.labels.replaceForChat(
|
|
833
|
+
"support",
|
|
834
|
+
"15551234567@s.whatsapp.net",
|
|
835
|
+
{ labels: ["priority", "customer"] },
|
|
836
|
+
{ idempotencyKey: "replace-customer-labels" },
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
console.log(labels.data.data, labels.metadata.requestId);
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
`replaceForChat` maps the source `setChatLabels` operation and replaces the
|
|
843
|
+
complete label set. Passing an empty array detaches every label. The source has
|
|
844
|
+
no incremental attach/detach route and no message-label route. Label reads are
|
|
845
|
+
not paginated; `includeObservation` selects either the legacy label array or
|
|
846
|
+
the typed observation envelope.
|
|
847
|
+
|
|
848
|
+
The Labels tag also includes the cross-cutting policy routes exposed as
|
|
849
|
+
`MessagingClient.observationPolicies`. Project methods require
|
|
850
|
+
`presence:read` or `presence:observe`; setting `labelMode` additionally requires
|
|
851
|
+
`labels:manage`. Session methods carry the same scopes and are Linked Device
|
|
852
|
+
only. Policy updates replace the supplied presence and typing modes while the
|
|
853
|
+
label mode remains optional in the pinned request schema.
|
|
854
|
+
|
|
855
|
+
Neither labels nor observation policies appears in the client-token action
|
|
856
|
+
allowlist. Use a server API key; the API rejects browser client tokens before
|
|
857
|
+
route handling.
|
|
858
|
+
|
|
859
|
+
## Business App quick replies
|
|
860
|
+
|
|
861
|
+
`MessagingClient.quickReplies` exposes the complete four-operation quick-reply
|
|
862
|
+
subfamily in the Business App contract. Listing requires `profile:read`;
|
|
863
|
+
create, full replacement, and delete require `profile:write`.
|
|
864
|
+
|
|
865
|
+
```ts
|
|
866
|
+
const remembered = await messaging.quickReplies.list("support");
|
|
867
|
+
|
|
868
|
+
const created = await messaging.quickReplies.create(
|
|
869
|
+
"support",
|
|
870
|
+
{
|
|
871
|
+
shortcut: "hours",
|
|
872
|
+
message: "We are open from 09:00 to 18:00.",
|
|
873
|
+
keywords: ["open", "hours"],
|
|
874
|
+
},
|
|
875
|
+
{ idempotencyKey: "create-hours-quick-reply" },
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
await messaging.quickReplies.replace(
|
|
879
|
+
"support",
|
|
880
|
+
created.data.data.id,
|
|
881
|
+
{
|
|
882
|
+
shortcut: "openinghours",
|
|
883
|
+
message: "We are open weekdays from 09:00 to 18:00.",
|
|
884
|
+
keywords: ["open", "hours", "weekday"],
|
|
885
|
+
count: 0,
|
|
886
|
+
},
|
|
887
|
+
{ idempotencyKey: "replace-hours-quick-reply" },
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
console.log(remembered.data.data.status, remembered.metadata.requestId);
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
The list response is a bounded observation collection containing policy,
|
|
894
|
+
freshness status, and associated label IDs. It is not paginated. The update
|
|
895
|
+
route replaces the complete quick reply, so the SDK names it `replace` instead
|
|
896
|
+
of implying a partial update. The source exposes no retrieve-by-ID, send, or
|
|
897
|
+
manual sync operation.
|
|
898
|
+
|
|
899
|
+
The pinned public observation-policy request schemas do not include
|
|
900
|
+
`quickReplyMode`, although policy responses contain that field. Quick-reply
|
|
901
|
+
CRUD therefore does not alter observation policy, and the SDK does not add an
|
|
902
|
+
undocumented policy update field.
|
|
903
|
+
|
|
904
|
+
Quick-reply routes are absent from the browser client-token action allowlist.
|
|
905
|
+
Use a server API key with the required profile scope.
|
|
906
|
+
|
|
907
|
+
## Session profile
|
|
908
|
+
|
|
909
|
+
`MessagingClient.profile` exposes the complete five-operation Profile tag for
|
|
910
|
+
Linked Device sessions. `get` requires `profile:read`; `setName`, `setStatus`,
|
|
911
|
+
`setPicture`, and `deletePicture` require `profile:write`.
|
|
912
|
+
|
|
913
|
+
```ts
|
|
914
|
+
const profile = await messaging.profile.get("support");
|
|
915
|
+
|
|
916
|
+
await messaging.profile.setName(
|
|
917
|
+
"support",
|
|
918
|
+
{ name: "Polymorfa Support" },
|
|
919
|
+
{ idempotencyKey: "profile-name-2026-08-19" },
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
await messaging.profile.setPicture(
|
|
923
|
+
"support",
|
|
924
|
+
{ url: "https://cdn.example.com/support-profile.jpg" },
|
|
925
|
+
{ idempotencyKey: "profile-picture-2026-08-19" },
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
console.log(profile.data.data, profile.metadata.requestId);
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
Picture input is JSON containing optional `url` and `base64` string fields. It
|
|
932
|
+
is not a binary upload or streaming method. The pinned public schema does not
|
|
933
|
+
declare those fields mutually exclusive and does not publish a size limit. The
|
|
934
|
+
pinned runner prefers non-empty base64 when both fields are supplied, rejects a
|
|
935
|
+
payload with neither source, and internally limits fetched or decoded data to
|
|
936
|
+
50 MiB. Use one source per request for unambiguous behavior.
|
|
937
|
+
|
|
938
|
+
Profile routes are absent from the browser client-token action allowlist. Use a
|
|
939
|
+
server API key with the required profile scope. The Profile tag has no profile
|
|
940
|
+
history, picture download, or standalone upload operation.
|
|
941
|
+
|
|
942
|
+
## Privacy
|
|
943
|
+
|
|
944
|
+
`MessagingClient.privacy` exposes the complete three-operation Privacy tag for
|
|
945
|
+
Linked Device sessions. `get` requires `profile:read`; `set` and
|
|
946
|
+
`setDefaultDisappearingTimer` require `profile:write`.
|
|
947
|
+
|
|
948
|
+
```ts
|
|
949
|
+
const privacy = await messaging.privacy.get("support");
|
|
950
|
+
|
|
951
|
+
await messaging.privacy.set(
|
|
952
|
+
"support",
|
|
953
|
+
{ setting: "online", value: "match_last_seen" },
|
|
954
|
+
{ idempotencyKey: "privacy-online-2026-08-19" },
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
await messaging.privacy.setDefaultDisappearingTimer(
|
|
958
|
+
"support",
|
|
959
|
+
{ durationSeconds: 604800 },
|
|
960
|
+
{ idempotencyKey: "privacy-default-timer-2026-08-19" },
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
console.log(privacy.data.data, privacy.metadata.requestId);
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
`PrivacySettingMutation` is discriminated by `setting`; incompatible values
|
|
967
|
+
fail type checking. `PRIVACY_SETTING_VALUES` exposes the same matrix at runtime
|
|
968
|
+
for command parsers and validation:
|
|
969
|
+
|
|
970
|
+
| Setting | Accepted values |
|
|
971
|
+
| --------------------------------------- | ---------------------------------------------- |
|
|
972
|
+
| `groupadd`, `last`, `status`, `profile` | `all`, `contacts`, `contact_blacklist`, `none` |
|
|
973
|
+
| `readreceipts` | `all`, `none` |
|
|
974
|
+
| `online` | `all`, `match_last_seen` |
|
|
975
|
+
| `calladd` | `all`, `known` |
|
|
976
|
+
| `messages` | `all`, `contacts` |
|
|
977
|
+
| `defense` | `on_standard`, `off` |
|
|
978
|
+
| `stickers` | `contacts`, `contact_allowlist`, `none` |
|
|
979
|
+
|
|
980
|
+
Default disappearing-message durations are seconds: `0` disables the account
|
|
981
|
+
default, `86400` is one day, `604800` is seven days, and `7776000` is 90 days.
|
|
982
|
+
This account default does not replace the per-chat timer exposed by
|
|
983
|
+
`MessagingClient.chats.setDisappearingTimer`.
|
|
984
|
+
|
|
985
|
+
Privacy routes are absent from the browser client-token action allowlist. Use a
|
|
986
|
+
server API key with the required profile scope. The Privacy tag has no privacy
|
|
987
|
+
history, allowlist/blacklist member-management, or pagination operation.
|
|
988
|
+
|
|
989
|
+
## Presence
|
|
990
|
+
|
|
991
|
+
`MessagingClient.presence` exposes the complete four-operation Presence tag
|
|
992
|
+
for Linked Device sessions. `get` and `getForChat` require `presence:read`,
|
|
993
|
+
`set` requires `presence:write`, and `subscribe` requires `presence:observe`.
|
|
994
|
+
|
|
995
|
+
```ts
|
|
996
|
+
const self = await messaging.presence.get("support");
|
|
997
|
+
|
|
998
|
+
await messaging.presence.set(
|
|
999
|
+
"support",
|
|
1000
|
+
{ presence: "available" },
|
|
1001
|
+
{ idempotencyKey: "presence-self-2026-08-20" },
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
const subscription = await messaging.presence.subscribe(
|
|
1005
|
+
"support",
|
|
1006
|
+
"15551234567@s.whatsapp.net",
|
|
1007
|
+
{ idempotencyKey: "presence-subscription-2026-08-20" },
|
|
1008
|
+
);
|
|
1009
|
+
const observed = await messaging.presence.getForChat(
|
|
1010
|
+
"support",
|
|
1011
|
+
"15551234567@s.whatsapp.net",
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
console.log(
|
|
1015
|
+
self.data.data,
|
|
1016
|
+
observed.data.data,
|
|
1017
|
+
subscription.metadata.requestId,
|
|
1018
|
+
);
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
`PRESENCE_STATES`, `PRESENCE_OBSERVATION_STATUSES`,
|
|
1022
|
+
`PRESENCE_UNKNOWN_REASONS`, and `PRESENCE_CHAT_STATES` are root runtime
|
|
1023
|
+
exports for input validation and response narrowing. Self presence is the
|
|
1024
|
+
runner's remembered desired and last successfully sent value. Its
|
|
1025
|
+
`authoritative` field is always `false`; `get` does not query remote account
|
|
1026
|
+
state.
|
|
1027
|
+
|
|
1028
|
+
Chat presence is also not a live query. `getForChat` reads the bounded
|
|
1029
|
+
observation projection controlled by `MessagingClient.observationPolicies`.
|
|
1030
|
+
`off` and `events` modes can return an unknown state without retained presence;
|
|
1031
|
+
`cache` mode returns `fresh` or `stale` cached observations. Typing observation
|
|
1032
|
+
is reported alongside presence but remains distinct from
|
|
1033
|
+
`MessagingClient.messages.setTyping`.
|
|
1034
|
+
|
|
1035
|
+
The pinned runtime makes each successful subscription or renewal valid for 120
|
|
1036
|
+
seconds and returns the exact `expiresAt`; consumers must use that timestamp
|
|
1037
|
+
rather than assuming a fixed lifetime. Subscriptions accept user and LID
|
|
1038
|
+
identifiers, while chat reads also accept groups. Observation limits can return
|
|
1039
|
+
429 and place presence subscriptions in a temporary suspension window. The
|
|
1040
|
+
source exposes no stream, watch, history, polling helper, or unsubscribe route.
|
|
1041
|
+
|
|
1042
|
+
Browser client tokens can call `get` and `getForChat` with the
|
|
1043
|
+
`read_presence` action and `subscribe` with `subscribe_presence`. They cannot
|
|
1044
|
+
call `set`; that route requires a server API key. Client-token rules also bind
|
|
1045
|
+
the request to the token's session. The server SDK accepts either credential
|
|
1046
|
+
kind and leaves the live action check to the API.
|
|
1047
|
+
|
|
1048
|
+
The pinned OpenAPI describes the synchronous `set` result as a generic
|
|
1049
|
+
`SuccessResponse`, while the pinned live RPC handler returns
|
|
1050
|
+
`{ success: true, data: { status: "OK" } }`. `SetPresenceResponse` represents
|
|
1051
|
+
both shapes, plus the documented async-accepted envelope. The subscription
|
|
1052
|
+
type likewise includes its documented async response when callers explicitly
|
|
1053
|
+
send `Prefer: respond-async`.
|
|
1054
|
+
|
|
1055
|
+
## Business App
|
|
1056
|
+
|
|
1057
|
+
`MessagingClient.business` exposes the 25 credential-compatible Business App
|
|
1058
|
+
operations outside the separately maintained `quickReplies` resource. Every
|
|
1059
|
+
operation requires a connected Linked Device session. Cloud API sessions,
|
|
1060
|
+
project credentials, browser client tokens, dashboard sessions, and staff
|
|
1061
|
+
credentials cannot use this resource.
|
|
1062
|
+
|
|
1063
|
+
```ts
|
|
1064
|
+
const catalog = await messaging.business.getCatalog("sales", {
|
|
1065
|
+
jid: "15551234567@s.whatsapp.net",
|
|
1066
|
+
limit: 25,
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
const product = await messaging.business.createProduct(
|
|
1070
|
+
"sales",
|
|
1071
|
+
{
|
|
1072
|
+
name: "Mint tea",
|
|
1073
|
+
currency: "USD",
|
|
1074
|
+
price: "12000",
|
|
1075
|
+
images: [{ url: "https://cdn.example.com/tea.jpg" }],
|
|
1076
|
+
},
|
|
1077
|
+
{ idempotencyKey: crypto.randomUUID() },
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
console.log(
|
|
1081
|
+
catalog.data.data.products,
|
|
1082
|
+
catalog.data.data.next,
|
|
1083
|
+
product.metadata.requestId,
|
|
1084
|
+
);
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
The exact server scopes are:
|
|
1088
|
+
|
|
1089
|
+
| Scope | Operations |
|
|
1090
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1091
|
+
| `profile:read` | `getProfile`, `getMerchantCompliance` |
|
|
1092
|
+
| `profile:write` | `updateProfile`, `setCoverPhoto`, `deleteCoverPhoto`, `setMerchantCompliance`, catalog creation/cart mutation, and every product or collection mutation |
|
|
1093
|
+
| `business:read` | `getCatalog`, `getProduct`, `listCollections`, `getCollection`, `getOrder`, `getLinkedAccounts`, `getEligibility` |
|
|
1094
|
+
|
|
1095
|
+
All 17 non-GET operations accept `RequestOptions`, including idempotency keys.
|
|
1096
|
+
They also represent the live `{ success: true, data: { requestId } }` result
|
|
1097
|
+
returned when a caller explicitly sends `Prefer: respond-async`. This includes
|
|
1098
|
+
`getOrder`, which is a token-bearing POST lookup despite its read semantics.
|
|
1099
|
+
GET operations remain synchronous.
|
|
1100
|
+
|
|
1101
|
+
### Profile and account state
|
|
1102
|
+
|
|
1103
|
+
The profile surface is `getProfile`, `updateProfile`, `setCoverPhoto`, and
|
|
1104
|
+
`deleteCoverPhoto`. Profile updates accept address, email, description, one or
|
|
1105
|
+
two HTTP(S) websites, or business hours. `specific_hours` days require
|
|
1106
|
+
distinct minute-of-day `openTime` and `closeTime` values; `open_24h` and
|
|
1107
|
+
`appointment_only` reject those fields. A profile update must contain at least
|
|
1108
|
+
one field, and a week cannot contain duplicate days.
|
|
1109
|
+
|
|
1110
|
+
`business.getProfile` always addresses the connected account's own Business
|
|
1111
|
+
App profile. It reuses the public `BusinessProfile` response type but remains
|
|
1112
|
+
distinct from `contacts.businessProfile`, which reads another contact's
|
|
1113
|
+
profile by identifier.
|
|
1114
|
+
|
|
1115
|
+
Cover photos use one JSON source: an HTTP(S) URL fetched by the API or base64
|
|
1116
|
+
data. There is no multipart, binary, file, streaming, resumable, or separate
|
|
1117
|
+
upload operation. The API permits at most 5 MiB after decoding and 6,990,508
|
|
1118
|
+
base64 characters. Remote downloads are capped at 5 MiB, require a public
|
|
1119
|
+
host, reject URL credentials and private/reserved addresses, revalidate each
|
|
1120
|
+
redirect, and time out after 60 seconds. The SDK's union prevents supplying a
|
|
1121
|
+
URL and base64 together.
|
|
1122
|
+
|
|
1123
|
+
`getMerchantCompliance` and `setMerchantCompliance` read or completely replace
|
|
1124
|
+
the merchant entity, customer-care, and grievance-officer fields. The server
|
|
1125
|
+
trims strings and enforces the documented UTF-8 byte limits. `getLinkedAccounts`
|
|
1126
|
+
returns optional Facebook Page, Facebook Business, Instagram Professional, and
|
|
1127
|
+
WhatsApp ad-identity records. `getEligibility` returns at most the six exact
|
|
1128
|
+
feature kinds declared by `BusinessFeature` with live upstream status strings.
|
|
1129
|
+
Neither read offers history or pagination.
|
|
1130
|
+
|
|
1131
|
+
### Catalogs, products, collections, and orders
|
|
1132
|
+
|
|
1133
|
+
`getCatalog` requires a public business-owner ID. It accepts an opaque `after`
|
|
1134
|
+
cursor, `limit` from 1 through 100, and optional image dimensions from 1 through 1024. Its response contains `products`, optional `next`, and optional
|
|
1135
|
+
`previous`. `listCollections` uses the same owner ID and cursor model with
|
|
1136
|
+
`collectionLimit` from 1 through 20 and `itemLimit` from 1 through 100; its
|
|
1137
|
+
response contains `collections` and optional `next`. These are explicit cursor
|
|
1138
|
+
fields, not offset pages, and the SDK does not synthesize `hasMore`.
|
|
1139
|
+
|
|
1140
|
+
`getCollection` accepts `after` and a product `limit`, but the pinned response
|
|
1141
|
+
contains only the collection and products—no next cursor. The SDK preserves
|
|
1142
|
+
that source limitation rather than claiming automatic pagination. Product,
|
|
1143
|
+
collection, business-owner, order, cover-photo, and session identifiers are
|
|
1144
|
+
encoded by the SDK. Cursors and order tokens remain query/body values rather
|
|
1145
|
+
than path data.
|
|
1146
|
+
|
|
1147
|
+
Product creation and replacement require one through ten image sources. Each
|
|
1148
|
+
image is exactly one of:
|
|
1149
|
+
|
|
1150
|
+
- `url`: an arbitrary public HTTPS image fetched through the bounded,
|
|
1151
|
+
SSRF-safe downloader, with a 16 MiB response cap;
|
|
1152
|
+
- `base64`: JSON base64 capped at 16 MiB decoded and 22,369,624 encoded
|
|
1153
|
+
characters; or
|
|
1154
|
+
- `mediaUrl`: an existing HTTPS URL on a WhatsApp or Meta host, reused without
|
|
1155
|
+
downloading.
|
|
1156
|
+
|
|
1157
|
+
`videoUrls` contain at most ten existing WhatsApp or Meta HTTPS URLs. The
|
|
1158
|
+
resource does not invent file-path, byte-array, multipart, streaming,
|
|
1159
|
+
resumable-upload, or media-upload methods. Product prices are unsigned integer
|
|
1160
|
+
amounts in thousandths with up to 18 digits. Currency is a three-letter
|
|
1161
|
+
uppercase code and is required with `price`; `currency` and `salePrice` are
|
|
1162
|
+
invalid without `price`. Omitting `hidden` produces `false` in the pinned
|
|
1163
|
+
runner. During replacement, that can trigger the separate visibility mutation
|
|
1164
|
+
and unhide an existing product, so callers preserving a hidden product must
|
|
1165
|
+
send `hidden: true` explicitly.
|
|
1166
|
+
|
|
1167
|
+
Collection creation requires one through 100 unique product IDs. Updates must
|
|
1168
|
+
change the name or include at least one product addition/removal; each list is
|
|
1169
|
+
unique and an ID cannot occur in both. Reordering requires one through 100
|
|
1170
|
+
unique collection moves with indices from 0 through 99. Product and collection
|
|
1171
|
+
appeal reasons are trimmed, nonempty, and capped at 4,096 UTF-8 bytes. The
|
|
1172
|
+
generated OpenAPI exposes a 4,096-character maximum, so multibyte text can pass
|
|
1173
|
+
schema validation and still fail the byte check; the SDK leaves that API error
|
|
1174
|
+
visible as a typed validation error.
|
|
1175
|
+
|
|
1176
|
+
`getOrder` requires the exact order ID and opaque lookup token supplied by the
|
|
1177
|
+
Business App event. It is not an order list, search, history, checkout, or
|
|
1178
|
+
fulfilment API. The source likewise exposes no catalog listing independent of
|
|
1179
|
+
a business-owner ID, no product search, no collection search, and no upload
|
|
1180
|
+
progress.
|
|
1181
|
+
|
|
1182
|
+
## Channels
|
|
1183
|
+
|
|
1184
|
+
`history.sync` payloads distinguish linked-device indexes and their preserved
|
|
1185
|
+
WhatsApp archive from Cloud history's `{ kind: "history", value }` envelope.
|
|
1186
|
+
Narrow `HistorySyncPayload` with `"kind" in payload` before reading provider-specific
|
|
1187
|
+
fields; `LinkedHistorySyncPayload` and `CloudHistorySyncPayload` are exported.
|
|
1188
|
+
|
|
1189
|
+
`MessagingClient.channels` exposes the complete 13-operation Channels tag for
|
|
1190
|
+
connected Linked Device sessions. Channels are WhatsApp newsletters in the
|
|
1191
|
+
protocol layer, but the SDK keeps the public API's `channels` terminology and
|
|
1192
|
+
does not merge them with chats or groups.
|
|
1193
|
+
|
|
1194
|
+
```ts
|
|
1195
|
+
const channels = await messaging.channels.list("support");
|
|
1196
|
+
const channel = await messaging.channels.retrieve(
|
|
1197
|
+
"support",
|
|
1198
|
+
"120363000000000000@newsletter",
|
|
1199
|
+
);
|
|
1200
|
+
const messages = await messaging.channels.listMessages(
|
|
1201
|
+
"support",
|
|
1202
|
+
"120363000000000000@newsletter",
|
|
1203
|
+
{ count: 25, before: 951 },
|
|
1204
|
+
);
|
|
1205
|
+
const updates = await messaging.channels.listMessageUpdates(
|
|
1206
|
+
"support",
|
|
1207
|
+
"120363000000000000@newsletter",
|
|
1208
|
+
{ count: 25, since: 1787212800, after: 951 },
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
console.log(
|
|
1212
|
+
channels.data.data,
|
|
1213
|
+
channel.data.data,
|
|
1214
|
+
messages.data.data,
|
|
1215
|
+
updates.data.data,
|
|
1216
|
+
);
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
The read surface is `list`, `retrieve`, `listMessages`,
|
|
1220
|
+
`listMessageUpdates`, and `subscribeToLiveUpdates`; each requires
|
|
1221
|
+
`channels:read`. `create`, `delete`, `markMessageViewed`, `reactToMessage`,
|
|
1222
|
+
`follow`, `unfollow`, `mute`, and `unmute` require `channels:manage`. All
|
|
1223
|
+
mutations accept `RequestOptions`, including an idempotency key.
|
|
1224
|
+
|
|
1225
|
+
Message history and update history are bounded arrays, not `CursorPage`
|
|
1226
|
+
objects. Both accept `count` from 1 through 100 and default to 50. Message
|
|
1227
|
+
history accepts a positive numeric message server ID as `before`. Update
|
|
1228
|
+
history accepts a non-negative Unix timestamp in seconds as `since` and a
|
|
1229
|
+
positive numeric message server ID as `after`; the pinned runner treats
|
|
1230
|
+
`since: 0` as unset. Responses do not include `next`, `hasMore`, or a cursor,
|
|
1231
|
+
so the SDK does not synthesize them. Update rows use the same `ChannelMessage`
|
|
1232
|
+
shape as message rows, but `text` can be absent because update payloads contain
|
|
1233
|
+
view and reaction counts without message content.
|
|
1234
|
+
|
|
1235
|
+
`subscribeToLiveUpdates` performs a temporary subscription mutation and
|
|
1236
|
+
returns the upstream `durationSeconds`. It does not return a stream, iterator,
|
|
1237
|
+
websocket, or listener. The source exposes no explicit unsubscribe operation;
|
|
1238
|
+
applications can query `listMessageUpdates` while their upstream subscription
|
|
1239
|
+
is active.
|
|
1240
|
+
|
|
1241
|
+
No channel route is present in the browser client-token action allowlist. Use a
|
|
1242
|
+
server API key with `channels:read` or `channels:manage`; client tokens are
|
|
1243
|
+
rejected before route execution. Every operation requires an active Linked
|
|
1244
|
+
Device connection.
|
|
1245
|
+
|
|
1246
|
+
The pinned source has two request/response discrepancies:
|
|
1247
|
+
|
|
1248
|
+
- `CreateChannelRequest` publicly declares optional `picture`, while the
|
|
1249
|
+
runner unmarshals a field named `profileUrl`. The current handler therefore
|
|
1250
|
+
creates the channel without applying the declared picture. The SDK exposes
|
|
1251
|
+
only the contract field and does not claim that picture setup succeeds.
|
|
1252
|
+
- Follow, unfollow, mute, unmute, viewed, and reaction routes declare generic
|
|
1253
|
+
`SuccessResponse` results. The live runner returns data envelopes with the
|
|
1254
|
+
statuses `FOLLOWED`, `UNFOLLOWED`, `MUTED`, `UNMUTED`, `VIEWED`, and
|
|
1255
|
+
`UPDATED`. The action response types represent both shapes, plus the live
|
|
1256
|
+
RPC async-accepted result available through `Prefer: respond-async` even
|
|
1257
|
+
though these routes omit 202 from the pinned OpenAPI.
|
|
1258
|
+
|
|
1259
|
+
The public reaction schema permits at most 32 characters. The runner performs
|
|
1260
|
+
a second check against 32 UTF-8 bytes, so a multibyte reaction can pass route
|
|
1261
|
+
validation and still receive a 400 response. Empty reaction text is preserved
|
|
1262
|
+
and removes the caller's reaction upstream. Channel, session, and public
|
|
1263
|
+
message identifiers are URL-encoded by the SDK.
|
|
1264
|
+
|
|
1265
|
+
## Messaging campaigns
|
|
1266
|
+
|
|
1267
|
+
`MessagingClient.campaigns` exposes the complete nine-operation project-slug
|
|
1268
|
+
campaign workflow: `list`, `create`, `retrieve`, `analytics`, `launch`,
|
|
1269
|
+
`pause`, `resume`, `stop`, and `requeue`. Reads require `campaigns:read`;
|
|
1270
|
+
creation and lifecycle changes require `campaigns:manage`.
|
|
1271
|
+
|
|
1272
|
+
```ts
|
|
1273
|
+
const created = await messaging.campaigns.create(
|
|
1274
|
+
"support",
|
|
1275
|
+
{
|
|
1276
|
+
name: "August launch",
|
|
1277
|
+
templateId: "order-ready",
|
|
1278
|
+
recipientListId: "active-customers",
|
|
1279
|
+
scheduledAt: Date.parse("2026-08-25T09:00:00Z"),
|
|
1280
|
+
},
|
|
1281
|
+
{ idempotencyKey: "campaign-august-create" },
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
const launched = await messaging.campaigns.launch(
|
|
1285
|
+
"support",
|
|
1286
|
+
created.data.data.id,
|
|
1287
|
+
{},
|
|
1288
|
+
{ idempotencyKey: "campaign-august-launch" },
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
console.log(launched.data.data.operationId, launched.metadata.requestId);
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
Launch, pause, resume, and stop append durable lifecycle commands and return the
|
|
1295
|
+
campaign's current persisted state plus an `operationId`. They do not wait for
|
|
1296
|
+
the campaign state to change. Read the campaign resource to inspect its status,
|
|
1297
|
+
or follow the returned operation with `Client.operations.wait(operationId)` and
|
|
1298
|
+
stop it with `Client.operations.cancel(operationId)`. The API exposes no
|
|
1299
|
+
campaign watcher or stream route of its own. Launch accepts an optional
|
|
1300
|
+
epoch-millisecond schedule. Pause requires a running campaign, resume requires
|
|
1301
|
+
a paused campaign, and stop accepts draft, running, or paused campaigns.
|
|
1302
|
+
|
|
1303
|
+
`requeue` is a direct transaction, not a durable operation. It moves failed
|
|
1304
|
+
recipients back to pending and can also include recipients skipped with an
|
|
1305
|
+
error. Its `{ requeued }` result is the number actually moved. Lists are
|
|
1306
|
+
complete newest-first arrays; the source exposes no cursor, page token, search,
|
|
1307
|
+
event history, replay, or delivery-listener endpoint.
|
|
1308
|
+
|
|
1309
|
+
This Messaging family is distinct from `Client.campaigns`, which maps
|
|
1310
|
+
the Management API's organization-key campaign model. The Messaging routes
|
|
1311
|
+
accept organization API keys and project tokens bound to the exact path
|
|
1312
|
+
project. Browser client tokens are not allowlisted for any campaign action and
|
|
1313
|
+
fail before the handler.
|
|
1314
|
+
Campaigns are project control-plane objects and have no Linked Device versus
|
|
1315
|
+
Cloud session-mode discriminator.
|
|
1316
|
+
|
|
1317
|
+
For organization-key calls, the live list and create handlers resolve the path
|
|
1318
|
+
project slug. The other seven handlers currently authorize the organization
|
|
1319
|
+
and campaign ID but do not verify that the campaign belongs to the supplied
|
|
1320
|
+
slug. Callers must still supply the intended project slug; the SDK encodes it
|
|
1321
|
+
and does not weaken this source behavior. The pinned OpenAPI campaign schema
|
|
1322
|
+
omits several JSON repository fields and leaves analytics untyped. The SDK
|
|
1323
|
+
exports the exact live analytics counters and preserves the extra campaign
|
|
1324
|
+
fields as optional `unknown` values rather than asserting undocumented shapes.
|
|
1325
|
+
|
|
1326
|
+
`create` and `launch` send an `Idempotency-Key` on every call, and the API
|
|
1327
|
+
records it for 24 hours, so a retry after an unseen success never creates a
|
|
1328
|
+
second campaign or launches twice (see [Idempotent sends](#idempotent-sends)).
|
|
1329
|
+
The other lifecycle commands are retried only when you pass an idempotency
|
|
1330
|
+
key, and the API does not persist that header for them. Repeating one can
|
|
1331
|
+
conflict with the resulting state or append another intent; repeating requeue
|
|
1332
|
+
normally reports zero after the matching recipients have already moved. The live API reports entitlement failures as `402` and invalid
|
|
1333
|
+
lifecycle state conflicts as `400`, rather than the more specific statuses
|
|
1334
|
+
suggested by their semantics.
|
|
1335
|
+
|
|
1336
|
+
The source exposes no Messaging campaign update, deletion, archive, duplicate,
|
|
1337
|
+
recipient listing, or campaign event inspection operation. The SDK does not
|
|
1338
|
+
substitute similarly named Management API routes or `raw.request` calls for
|
|
1339
|
+
those gaps.
|
|
1340
|
+
|
|
1341
|
+
## Chats
|
|
1342
|
+
|
|
1343
|
+
`MessagingClient.chats` exposes the credential-compatible Linked Device chat
|
|
1344
|
+
management surface. Every operation requires `chats:manage`.
|
|
1345
|
+
|
|
1346
|
+
```ts
|
|
1347
|
+
await messaging.chats.editMessage(
|
|
1348
|
+
"support",
|
|
1349
|
+
"15551234567@s.whatsapp.net",
|
|
1350
|
+
"message-id",
|
|
1351
|
+
{ text: "Corrected copy" },
|
|
1352
|
+
{ idempotencyKey: "edit-message-id" },
|
|
1353
|
+
);
|
|
1354
|
+
|
|
1355
|
+
await messaging.chats.setDisappearingTimer(
|
|
1356
|
+
"support",
|
|
1357
|
+
"15551234567@s.whatsapp.net",
|
|
1358
|
+
{ durationSeconds: 604800 },
|
|
1359
|
+
);
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
The duration is typed to the four values accepted by the API: disabled, one
|
|
1363
|
+
day, one week, or 90 days. The resource also provides `deleteMessage`,
|
|
1364
|
+
`archive`, and `unarchive`. Chat operations are not available for Cloud API
|
|
1365
|
+
sessions.
|
|
1366
|
+
|
|
1367
|
+
## Webhooks and events
|
|
1368
|
+
|
|
1369
|
+
`MessagingClient.webhooks` lists, creates, retrieves, updates, and deletes
|
|
1370
|
+
webhook registrations with a server credential carrying `webhooks:manage`.
|
|
1371
|
+
Webhook mutations accept the same `RequestOptions` as every other resource,
|
|
1372
|
+
including idempotency keys, cancellation, timeouts, and API-version overrides.
|
|
1373
|
+
|
|
1374
|
+
Use `webhooks.verify` with the exact raw request bytes before inspecting an
|
|
1375
|
+
inbound Messaging delivery. `isEvent` narrows known event names to their
|
|
1376
|
+
exported payload types:
|
|
1377
|
+
|
|
1378
|
+
```ts
|
|
1379
|
+
const event = await webhooks.verify({
|
|
1380
|
+
body: rawBody,
|
|
1381
|
+
signature,
|
|
1382
|
+
secret: webhookSecret,
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
if (isEvent(event, "history.sync")) {
|
|
1386
|
+
if ("kind" in event.payload) {
|
|
1387
|
+
console.log("Meta Cloud API history batch", event.payload.value);
|
|
1388
|
+
} else {
|
|
1389
|
+
console.log(event.payload.syncType, event.payload.progress);
|
|
1390
|
+
}
|
|
1391
|
+
} else if (isEvent(event, "contact.sync")) {
|
|
1392
|
+
console.log(event.payload.kind, event.payload.value);
|
|
1393
|
+
} else if (isEvent(event, "message.echo")) {
|
|
1394
|
+
console.log(event.payload.source, event.externalId);
|
|
1395
|
+
} else if (isEvent(event, "call.received")) {
|
|
1396
|
+
console.log(
|
|
1397
|
+
event.payload.callId,
|
|
1398
|
+
event.payload.from.id,
|
|
1399
|
+
event.payload.hasVideo,
|
|
1400
|
+
);
|
|
1401
|
+
} else if (isEvent(event, "call.accepted")) {
|
|
1402
|
+
// answeredBy and exclusive say who answered and whether they claimed it.
|
|
1403
|
+
console.log(event.payload.answeredBy, event.payload.exclusive === true);
|
|
1404
|
+
} else if (isEvent(event, "message.failed")) {
|
|
1405
|
+
if (event.payload.error === "blocked_by_safety") {
|
|
1406
|
+
console.log(event.payload.code, event.payload.retryAfter);
|
|
1407
|
+
}
|
|
1408
|
+
} else if (isEvent(event, "bansafe.action")) {
|
|
1409
|
+
console.log(event.payload.rung, event.payload.requires);
|
|
1410
|
+
} else if (isEvent(event, "customer.pairing_link.connected")) {
|
|
1411
|
+
console.log(event.payload.customerId, event.payload.sessionId);
|
|
1412
|
+
}
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
The catalog also types Customer lifecycle events (`customer.*`), BanSafe events
|
|
1416
|
+
(`bansafe.health_threshold`, `bansafe.enforcement`, `bansafe.action`,
|
|
1417
|
+
`bansafe.incident`, and `bansafe.claim`), campaign progress events
|
|
1418
|
+
(`campaign.*`), `message.failed`, and `template.status`. `message.failed`
|
|
1419
|
+
reports `blocked_by_safety` when BanSafe stops a send, with an optional `code`
|
|
1420
|
+
and `retryAfter` in seconds. Unknown event names still parse as
|
|
1421
|
+
`UnknownWebhookEvent`.
|
|
1422
|
+
|
|
1423
|
+
`contact.sync` delivers a Meta Cloud API contact batch as
|
|
1424
|
+
`{ kind: "contacts", value }`. `message.echo` reports a message sent from the
|
|
1425
|
+
WhatsApp Business app on a connected Meta Cloud API number as
|
|
1426
|
+
`{ source: "whatsapp_business_app", value }`. Events for a session created by a
|
|
1427
|
+
QuickLink include its optional `externalId`.
|
|
1428
|
+
|
|
1429
|
+
Development builds also export `CallEndedPayload` and `CallTelemetryPayload`.
|
|
1430
|
+
For `call.ended`, check `from` before reading its identity: it is `null` when
|
|
1431
|
+
the media host disappeared before reporting the caller. The reason is
|
|
1432
|
+
`pod_lost` for those recovered terminal events. Telemetry fields `recvKbps`
|
|
1433
|
+
and `sendKbps` contain cumulative kilobits, not rates.
|
|
1434
|
+
|
|
1435
|
+
`webhooks.verifySignature()` performs the same production signature check and
|
|
1436
|
+
returns a boolean without parsing. `webhooks.createFixture()` creates an exact
|
|
1437
|
+
JSON byte sequence and matching production signature for local tests.
|
|
1438
|
+
`webhooks.verifyLocal()` verifies the timestamped signature used by local CLI
|
|
1439
|
+
forwarding. These helpers are credential-free. The older
|
|
1440
|
+
`constructWebhookEvent` and `verifyWebhookSignature` exports remain available
|
|
1441
|
+
through the first stable major. A later major can remove them with a migration
|
|
1442
|
+
release.
|
|
1443
|
+
|
|
1444
|
+
The management `Client` owns a separate durable developer API at both
|
|
1445
|
+
organization and project scope:
|
|
1446
|
+
|
|
1447
|
+
- `events.list`, `retrieve`, and `replay`
|
|
1448
|
+
- `webhooks.list`, `create`, `retrieve`, `update`, `delete`, `test`, and
|
|
1449
|
+
`rotateSecret`
|
|
1450
|
+
- `webhookDeliveries.list`, `retrieve`, `listAttempts`, `retrieveAttempt`, and
|
|
1451
|
+
`retry`
|
|
1452
|
+
- `operations.list`, `get`, `listTransitions`, `cancel`, and `wait`
|
|
1453
|
+
|
|
1454
|
+
```ts
|
|
1455
|
+
const deliveries = await project.webhookDeliveries.list({
|
|
1456
|
+
webhookId: "wh_123",
|
|
1457
|
+
limit: 25,
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
const delivery = deliveries.items[0];
|
|
1461
|
+
if (delivery) {
|
|
1462
|
+
const attempts = await project.webhookDeliveries.listAttempts(delivery.id);
|
|
1463
|
+
if (attempts.items[0]) {
|
|
1464
|
+
const attempt = await project.webhookDeliveries.retrieveAttempt(
|
|
1465
|
+
delivery.id,
|
|
1466
|
+
attempts.items[0].id,
|
|
1467
|
+
);
|
|
1468
|
+
// Failed HTTP responses carry a redacted excerpt of at most 8192 UTF-8 bytes.
|
|
1469
|
+
console.log(attempt.data.response?.excerpt);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const replay = await project.events.replay(
|
|
1474
|
+
"evt_123",
|
|
1475
|
+
{ webhookId: "wh_123" },
|
|
1476
|
+
{ idempotencyKey: crypto.randomUUID() },
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
console.log(replay.data.operationId);
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
List methods return `CursorPage<T>`. Mutations return owner-specific typed
|
|
1483
|
+
receipts and preserve response metadata, request IDs, and idempotency receipts.
|
|
1484
|
+
The SDK has no operation inspection or cancellation methods.
|
|
1485
|
+
|
|
1486
|
+
Console and staff routes remain absent from the server client and its raw
|
|
1487
|
+
guidance. The CLI listener protocol stays private to the CLI.
|
|
1488
|
+
|
|
1489
|
+
### Stream events in real time
|
|
1490
|
+
|
|
1491
|
+
`events.stream()` follows a project's server-sent event stream. It needs a
|
|
1492
|
+
credential with `events:listen` and a team enrolled in the Event streams beta;
|
|
1493
|
+
without enrollment the iterator throws `PolymorfaAuthorizationError` with code
|
|
1494
|
+
`feature_unavailable`. Organization clients pass `projectId`.
|
|
1495
|
+
|
|
1496
|
+
```ts
|
|
1497
|
+
const controller = new AbortController();
|
|
1498
|
+
const stream = client.project(projectId).events.stream({
|
|
1499
|
+
types: ["message.*", "session.connected"],
|
|
1500
|
+
since: savedCursor, // optional: resume after this cursor
|
|
1501
|
+
signal: controller.signal,
|
|
1502
|
+
onGap: (gap) => console.warn(`${gap.missedEvents} events expired`),
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
for await (const item of stream) {
|
|
1506
|
+
if (item.webhook) handle(item.webhook); // the exact webhook body
|
|
1507
|
+
await saveCursor(item.cursor);
|
|
1508
|
+
}
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
Each item carries the event metadata (`item.event`, the same fields as
|
|
1512
|
+
`events.retrieve`), the decoded webhook body (`item.webhook`, or `null` when
|
|
1513
|
+
hosted message storage did not keep it), and its `cursor`. The iterator
|
|
1514
|
+
reconnects with exponential backoff and jitter after a dropped connection, an
|
|
1515
|
+
`expiry`, a missed heartbeat, `429`, `5xx`, or a recoverable gap, resuming from
|
|
1516
|
+
the last delivered cursor. It ends with an error on an invalid or expired
|
|
1517
|
+
cursor, an authentication or authorization failure, or a `revoked` stream.
|
|
1518
|
+
Aborting `signal` or leaving the loop ends it without an error.
|
|
1519
|
+
|
|
1520
|
+
Pass `ack: "manual"` to have the server wait for your processing, and confirm
|
|
1521
|
+
progress with
|
|
1522
|
+
`events.acknowledgeStream(item.streamId, { cursor: item.cursor, sequence: item.sequence })`.
|
|
1523
|
+
|
|
1524
|
+
`events.liveSource()` returns a `LiveEventSource` for `@polymorfa/store`:
|
|
1525
|
+
|
|
1526
|
+
```ts
|
|
1527
|
+
connectEventSource(
|
|
1528
|
+
store,
|
|
1529
|
+
client.project(projectId).events.liveSource({ types: ["message.*"] }),
|
|
1530
|
+
);
|
|
1531
|
+
```
|
|
1532
|
+
|
|
1533
|
+
It passes webhook bodies to the store with their cursors and skips events whose
|
|
1534
|
+
body was not kept. Use it on a server or trusted worker; server credentials must
|
|
1535
|
+
not reach a browser.
|
|
1536
|
+
|
|
1537
|
+
## Platform automation
|
|
1538
|
+
|
|
1539
|
+
Organization API keys can use handwritten campaign, audience, opt-out, and
|
|
1540
|
+
media resources:
|
|
1541
|
+
|
|
1542
|
+
```ts
|
|
1543
|
+
const campaign = await platform.campaigns.create(
|
|
1544
|
+
{
|
|
1545
|
+
projectId: "project_123",
|
|
1546
|
+
name: "August launch",
|
|
1547
|
+
},
|
|
1548
|
+
{
|
|
1549
|
+
idempotencyKey: "campaign-august-2026",
|
|
1550
|
+
timeoutMs: 10_000,
|
|
1551
|
+
},
|
|
1552
|
+
);
|
|
1553
|
+
|
|
1554
|
+
console.log(campaign.data.data, campaign.metadata.requestId);
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
The pinned contract defines these operation payloads as open objects, exposed
|
|
1558
|
+
as `PlatformPayload`. Templates and Flows are not methods on `Client`:
|
|
1559
|
+
their endpoints require a dashboard bearer and reject the organization API key
|
|
1560
|
+
used by the server client.
|
|
1561
|
+
|
|
1562
|
+
## Billing and usage
|
|
1563
|
+
|
|
1564
|
+
`Client.billing` exposes the complete organization-key billing family.
|
|
1565
|
+
Reads require `sessions:read`. Credit quantities, including fields ending in
|
|
1566
|
+
`Cents`, support up to six decimal places. They are not cash minor units.
|
|
1567
|
+
Team warnings follow the fixed one-day and two-hour insufficiency forecast;
|
|
1568
|
+
notification preferences are managed in the Console.
|
|
1569
|
+
|
|
1570
|
+
```ts
|
|
1571
|
+
const [balance, usage, transactions, pricing] = await Promise.all([
|
|
1572
|
+
platform.billing.retrieve(),
|
|
1573
|
+
platform.billing.usage(),
|
|
1574
|
+
platform.billing.listTransactions(),
|
|
1575
|
+
platform.billing.listPricing(),
|
|
1576
|
+
]);
|
|
1577
|
+
|
|
1578
|
+
console.log({
|
|
1579
|
+
balance: balance.data.data,
|
|
1580
|
+
usage: usage.data.data,
|
|
1581
|
+
transactions: transactions.data.data,
|
|
1582
|
+
pricing: pricing.data.data,
|
|
1583
|
+
requestId: usage.metadata.requestId,
|
|
1584
|
+
});
|
|
1585
|
+
```
|
|
1586
|
+
|
|
1587
|
+
### Change a number tier
|
|
1588
|
+
|
|
1589
|
+
Create a quote, show its credit charge and effective time, then confirm its ID
|
|
1590
|
+
only after the customer accepts. Upgrades buy a fresh 24-hour window and replace
|
|
1591
|
+
the remaining paid time. Downgrades apply when the paid window ends.
|
|
1592
|
+
|
|
1593
|
+
```ts
|
|
1594
|
+
const reviewed = await platform.sessions.quoteTierChange(sessionId, {
|
|
1595
|
+
tierOverride: "pro",
|
|
1596
|
+
});
|
|
1597
|
+
const quote = reviewed.data.data;
|
|
1598
|
+
console.log(quote.quote.amountCents, quote.quote.effectiveAtMs);
|
|
1599
|
+
|
|
1600
|
+
// After the customer confirms this exact quote:
|
|
1601
|
+
await platform.sessions.setTierOverride(sessionId, { quoteId: quote.id });
|
|
1602
|
+
const result = await platform.sessions.retrieveTierChange(sessionId, quote.id);
|
|
1603
|
+
console.log(result.data.data.status);
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
A queued result has not granted the tier. Poll until it is applied or rejected.
|
|
1607
|
+
A quote expires after ten minutes and can become invalid if the number or price
|
|
1608
|
+
changes. Show a new quote for confirmation after a conflict; never silently
|
|
1609
|
+
purchase a replacement. Set `tierOverride: null` when quoting to restore project
|
|
1610
|
+
inheritance. The old `setTierOverride({tierOverride})` request and
|
|
1611
|
+
`billing.updateReminderSettings` method are removed.
|
|
1612
|
+
|
|
1613
|
+
## Organization access and security
|
|
1614
|
+
|
|
1615
|
+
The organization view exposes key metadata, members, audit logs, session bans,
|
|
1616
|
+
security incidents, and project-token metadata:
|
|
1617
|
+
|
|
1618
|
+
```ts
|
|
1619
|
+
const [keys, members, audit, bans, incidents, tokens] = await Promise.all([
|
|
1620
|
+
platform.apiKeys.list(),
|
|
1621
|
+
platform.members.list(),
|
|
1622
|
+
platform.auditLogs.list({
|
|
1623
|
+
action: "session.stop",
|
|
1624
|
+
resource: "session",
|
|
1625
|
+
limit: 100,
|
|
1626
|
+
}),
|
|
1627
|
+
platform.sessionBans.listActive(),
|
|
1628
|
+
platform.securityIncidents.list(),
|
|
1629
|
+
platform.projectTokens.list("018f0000-0000-7000-8000-000000000002"),
|
|
1630
|
+
]);
|
|
1631
|
+
|
|
1632
|
+
await platform.securityIncidents.acknowledge(incidents.data.data[0]!.id, {
|
|
1633
|
+
idempotencyKey: "acknowledge-incident-1",
|
|
1634
|
+
});
|
|
1635
|
+
await platform.apiKeys.deactivate(keys.data.data[0]!.keyId, {
|
|
1636
|
+
idempotencyKey: "deactivate-key-1",
|
|
1637
|
+
});
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
These read operations require `sessions:read`. API-key deactivation and incident
|
|
1641
|
+
acknowledgement require `sessions:manage`. The two mutations are direct
|
|
1642
|
+
organization-scoped writes rather than asynchronous operations. The SDK retries
|
|
1643
|
+
them only when an idempotency key is supplied, but the pinned handlers do not
|
|
1644
|
+
persist that header. Incident acknowledgement is repeatable; an API-key
|
|
1645
|
+
deactivation retry after an unseen successful response can return `404` because
|
|
1646
|
+
the key is already inactive.
|
|
1647
|
+
|
|
1648
|
+
These list responses are complete arrays. The source exposes no cursor or
|
|
1649
|
+
page token. The live audit handler accepts exact `action` and `resource`
|
|
1650
|
+
filters plus a limit bounded to 1 through 500, although those query fields are
|
|
1651
|
+
missing from the pinned OpenAPI operation. Project-token metadata requires an
|
|
1652
|
+
explicit project ID for organization-key calls even though OpenAPI marks the
|
|
1653
|
+
query field optional. Neither token-list operation returns bearer secrets.
|
|
1654
|
+
|
|
1655
|
+
The API-key list handler currently reports the all-scopes mask for each row
|
|
1656
|
+
instead of the stored row-specific mask. The SDK preserves that numeric wire
|
|
1657
|
+
field without interpreting it as proof of the caller's live authorization.
|
|
1658
|
+
Incident acknowledgement records an empty acting-user value for API-key calls;
|
|
1659
|
+
the subsequent incident list can therefore expose an empty `acknowledgedBy`
|
|
1660
|
+
string rather than a dashboard user ID.
|
|
1661
|
+
|
|
1662
|
+
Organization updates, member role changes, member deletion, invitations,
|
|
1663
|
+
billing top-ups, and console usage insights require a dashboard session and
|
|
1664
|
+
are not exposed by the server SDK. Browser client tokens are rejected by the
|
|
1665
|
+
Management API. Project tokens are accepted only by a project-scoped `Client`;
|
|
1666
|
+
organization-only resources are absent from that view's public type.
|
|
1667
|
+
|
|
1668
|
+
## QuickLink lifecycle and settings
|
|
1669
|
+
|
|
1670
|
+
`MessagingClient.quickLinks` owns the authenticated hosted pairing lifecycle:
|
|
1671
|
+
|
|
1672
|
+
```ts
|
|
1673
|
+
const quickLink = await messaging.quickLinks.create(
|
|
1674
|
+
{
|
|
1675
|
+
projectId: "11111111-2222-4333-8444-555555555555",
|
|
1676
|
+
externalId: "crm-account-42",
|
|
1677
|
+
configuration: { methods: ["qr", "pairing"] },
|
|
1678
|
+
},
|
|
1679
|
+
{ idempotencyKey: crypto.randomUUID() },
|
|
1680
|
+
);
|
|
1681
|
+
|
|
1682
|
+
const status = await messaging.quickLinks.retrieve(quickLink.data.data.id);
|
|
1683
|
+
console.log(quickLink.data.data.url, status.data.data.status);
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
The resource accepts organization API keys or project tokens with
|
|
1687
|
+
`quicklink:manage`. It rejects browser client tokens before transport. An
|
|
1688
|
+
organization key can select `projectId` when creating a link; a project token
|
|
1689
|
+
is bound by the server. `cancel()` invalidates a pending link and removes its
|
|
1690
|
+
pending session. Connected links cannot be cancelled. The source exposes no
|
|
1691
|
+
list, recover, or history operation.
|
|
1692
|
+
|
|
1693
|
+
`Client.quickLinkSettings.retrieve` and `update` map the management
|
|
1694
|
+
`GET /platform/quicklink` and `PUT /platform/quicklink` operations. Use them on the root
|
|
1695
|
+
organization client or an immutable project view:
|
|
1696
|
+
|
|
1697
|
+
```ts
|
|
1698
|
+
const organizationSettings = await platform.quickLinkSettings.retrieve();
|
|
1699
|
+
const projectSettings = await platform
|
|
1700
|
+
.project("project_123")
|
|
1701
|
+
.quickLinkSettings.update(
|
|
1702
|
+
{
|
|
1703
|
+
theme: "dark",
|
|
1704
|
+
enabled: true,
|
|
1705
|
+
successCallbackUrl: "https://app.example.com/whatsapp/connected",
|
|
1706
|
+
failureCallbackUrl: "https://app.example.com/whatsapp/cancelled",
|
|
1707
|
+
allowPhoneChange: false,
|
|
1708
|
+
},
|
|
1709
|
+
{ idempotencyKey: "quicklink-project-123-dark" },
|
|
1710
|
+
);
|
|
1711
|
+
```
|
|
1712
|
+
|
|
1713
|
+
These methods manage saved settings only. Hosted lifecycle methods stay on
|
|
1714
|
+
`MessagingClient.quickLinks`, not `Client` or `client.project(...)`, because
|
|
1715
|
+
the `/messaging/quicklinks/{id}` routes do not carry an immutable project path for an
|
|
1716
|
+
organization-key project view. Console-only logo routes are outside the SDK.
|
|
1717
|
+
|
|
1718
|
+
`successCallbackUrl` and `failureCallbackUrl` are project-only HTTPS
|
|
1719
|
+
destinations; the API copies them into each link when it is issued, and link
|
|
1720
|
+
creation has no callback override. `allowPhoneChange` lets recipients replace a
|
|
1721
|
+
prefilled number and defaults to `false`. `hideWatermark: true` requires Premium
|
|
1722
|
+
team access. Saved settings have no redirect-URI allowlist. `externalId` on
|
|
1723
|
+
creation is an integrator correlation value copied to the resulting session; it
|
|
1724
|
+
can repeat across invitations and does not grant access.
|
|
1725
|
+
|
|
1726
|
+
## Management session lifecycle
|
|
1727
|
+
|
|
1728
|
+
The organization client's `sessions.start` requests a start for one stopped or
|
|
1729
|
+
failed session. It accepts a session UUID or stable slug and an optional project
|
|
1730
|
+
context:
|
|
1731
|
+
|
|
1732
|
+
```ts
|
|
1733
|
+
const start = await platform.sessions.start(
|
|
1734
|
+
"support",
|
|
1735
|
+
{ projectId: "11111111-2222-4333-8444-555555555555" },
|
|
1736
|
+
{ idempotencyKey: "start-support" },
|
|
1737
|
+
);
|
|
1738
|
+
```
|
|
1739
|
+
|
|
1740
|
+
The returned `SessionStartResult` confirms that the start request was accepted;
|
|
1741
|
+
it does not claim that the session has connected. A paid start first reserves
|
|
1742
|
+
credit. An HTTP 402 response throws `PolymorfaPaymentRequiredError`, preserving
|
|
1743
|
+
the API's error code, message, and request ID. It is not automatically retried;
|
|
1744
|
+
resolve the funding or entitlement problem before submitting another start.
|
|
1745
|
+
The charge is committed on successful connection. `sessions.stopMany` and
|
|
1746
|
+
`deleteMany` cover the two bounded batch operations. All three require
|
|
1747
|
+
`sessions:manage`. Batch methods accept `sessionIds` plus an optional
|
|
1748
|
+
`projectId`:
|
|
1749
|
+
|
|
1750
|
+
```ts
|
|
1751
|
+
const stop = await platform.sessions.stopMany(
|
|
1752
|
+
{
|
|
1753
|
+
projectId: "11111111-2222-4333-8444-555555555555",
|
|
1754
|
+
sessionIds: ["support", "sales"],
|
|
1755
|
+
},
|
|
1756
|
+
{ idempotencyKey: "stop-support-sales" },
|
|
1757
|
+
);
|
|
1758
|
+
|
|
1759
|
+
const removal = await platform.sessions.deleteMany(
|
|
1760
|
+
{ sessionIds: ["old-support", "old-sales"] },
|
|
1761
|
+
{ idempotencyKey: "delete-old-support-sales" },
|
|
1762
|
+
);
|
|
1763
|
+
```
|
|
1764
|
+
|
|
1765
|
+
The source accepts 1–100 UUIDs or stable slugs. It trims identifiers and the
|
|
1766
|
+
live handler deduplicates repeats, while OpenAPI declares the array unique.
|
|
1767
|
+
Only matching rows contribute to `{ stopping }` or `{ removed }`; the API does
|
|
1768
|
+
not return per-item results or errors for missing identifiers. Batch stop
|
|
1769
|
+
requires session-control publishing and queues fire-and-forget stop commands.
|
|
1770
|
+
Batch delete removes rows first, then best-effort queues kill commands for
|
|
1771
|
+
rows that were not disconnected. Neither route returns a durable operation ID,
|
|
1772
|
+
stream, watcher, or completion status.
|
|
1773
|
+
|
|
1774
|
+
The transport retries these mutations only when an idempotency key is
|
|
1775
|
+
provided. The pinned handlers do not persist that header. A repeated stop can
|
|
1776
|
+
enqueue another stop command; a repeated delete reports only rows still found.
|
|
1777
|
+
QuickLink settings updates are state upserts and can safely converge on the
|
|
1778
|
+
same supplied values.
|
|
1779
|
+
|
|
1780
|
+
## Session creation and configuration
|
|
1781
|
+
|
|
1782
|
+
Create new sessions with `MessagingClient.quickLinks.create`. Direct
|
|
1783
|
+
`sessions.create` and Platform `sessions.createTesting` have been removed in this
|
|
1784
|
+
breaking contract update. Reconnect and delete still operate on existing sessions.
|
|
1785
|
+
|
|
1786
|
+
```ts
|
|
1787
|
+
const link = await messaging.quickLinks.create({
|
|
1788
|
+
projectId,
|
|
1789
|
+
configuration: {
|
|
1790
|
+
connectionPreference: "linked",
|
|
1791
|
+
historySync: { consent: "ask" },
|
|
1792
|
+
},
|
|
1793
|
+
});
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
Page text, appearance, legal links, and callbacks belong in saved
|
|
1797
|
+
`Client.quickLinkSettings`, not individual invitations. Links report nullable
|
|
1798
|
+
`expiresAt`; new invitations remain usable until completion or cancellation.
|
|
1799
|
+
Free-tier real-account pairing is available only in the authenticated Console.
|
|
1800
|
+
|
|
1801
|
+
Use `Client.sessionConfiguration` for team defaults and
|
|
1802
|
+
`client.project(projectId).sessionConfiguration` for project defaults. Session
|
|
1803
|
+
updates take `{revision, configuration: {set, reset}}`; resets remove explicit
|
|
1804
|
+
overrides so later defaults continue to apply. Reads expose effective values,
|
|
1805
|
+
sources, consent restrictions, and pending runtime application.
|
|
1806
|
+
|
|
1807
|
+
For simulation, create a QuickLink with `configuration.testing`, including initial
|
|
1808
|
+
`configuration` and the explicit `editable` subset delegated to the recipient.
|
|
1809
|
+
Test access is checked independently; simulation cannot contact real accounts.
|
|
1810
|
+
|
|
1811
|
+
Test history content is uploaded separately from session configuration:
|
|
1812
|
+
|
|
1813
|
+
```ts
|
|
1814
|
+
const fixture = await messaging.testing.createHistoryFixture(projectId, {
|
|
1815
|
+
messages: [
|
|
1816
|
+
{
|
|
1817
|
+
id: "example-1",
|
|
1818
|
+
senderPhone: testPhone,
|
|
1819
|
+
text: "Demo",
|
|
1820
|
+
timestamp: 1,
|
|
1821
|
+
fromMe: false,
|
|
1822
|
+
},
|
|
1823
|
+
],
|
|
1824
|
+
});
|
|
1825
|
+
const invitation = await messaging.quickLinks.create({
|
|
1826
|
+
projectId,
|
|
1827
|
+
configuration: {
|
|
1828
|
+
testing: { configuration: { historyFixtureId: fixture.data.fixtureId } },
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
```
|
|
1832
|
+
|
|
1833
|
+
Fixture senders must be existing simulated numbers in that project. Test-number
|
|
1834
|
+
entitlements and history consent still apply; uploading a fixture does not enable
|
|
1835
|
+
hosted message storage.
|
|
1836
|
+
|
|
1837
|
+
### Trigger test events
|
|
1838
|
+
|
|
1839
|
+
Fire a named, signed test event for a Test number. The event reaches your
|
|
1840
|
+
webhooks and event history with `source: "test"` and does not change the Test
|
|
1841
|
+
number. Real numbers are refused with a `PolymorfaValidationError`, and each
|
|
1842
|
+
project can trigger 30 test events per minute (`PolymorfaRateLimitError`).
|
|
1843
|
+
|
|
1844
|
+
```ts
|
|
1845
|
+
import { TEST_EVENT_FIXTURES } from "@polymorfa/sdk";
|
|
1846
|
+
|
|
1847
|
+
const result = await messaging.testing.triggerEvent(projectId, {
|
|
1848
|
+
session: "my-test-number",
|
|
1849
|
+
event: "message.received", // one of TEST_EVENT_FIXTURES
|
|
1850
|
+
overrides: { text: "hi", from: "+15550100001" },
|
|
1851
|
+
});
|
|
1852
|
+
console.log(result.data.eventId);
|
|
1853
|
+
|
|
1854
|
+
// Rare events: failed delivery, ban warning, incoming call, template rejection.
|
|
1855
|
+
await messaging.testing.triggerEvent(projectId, {
|
|
1856
|
+
session: "my-test-number",
|
|
1857
|
+
event: "template.status",
|
|
1858
|
+
overrides: { templateStatus: "REJECTED", reason: "INVALID_FORMAT" },
|
|
1859
|
+
});
|
|
1860
|
+
|
|
1861
|
+
const { data } = await messaging.testing.listEventFixtures(projectId);
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
Set `fromSession` on a `message.received` request to send a simulated text
|
|
1865
|
+
from another connected Test number in the same project instead; the response
|
|
1866
|
+
has `delivery: "simulated"` and the event arrives as ordinary Test number
|
|
1867
|
+
activity. Both methods require an organization API key or project token with
|
|
1868
|
+
`sandbox:write` (trigger) or `sandbox:read` (list) and Test numbers access.
|
|
1869
|
+
|
|
1870
|
+
Pass `{ idempotencyKey }` as the third argument to `triggerEvent` to retry
|
|
1871
|
+
safely. Repeating the request with the same key and body reuses the same event
|
|
1872
|
+
ID, so a retry after an uncertain response never creates a second event or
|
|
1873
|
+
duplicate webhook deliveries. With a key, the SDK also retries network and
|
|
1874
|
+
5xx failures.
|
|
1875
|
+
|
|
1876
|
+
Trusted servers continue an issued Meta Cloud API invitation with
|
|
1877
|
+
`messaging.cloudOnboarding.advance({ quicklinkId, projectId, result })`.
|
|
1878
|
+
`result` contains the Embedded Signup authorization code, selected WABA and phone
|
|
1879
|
+
IDs, and Coexistence/history choices. This method does not create a session or
|
|
1880
|
+
accept Meta app secrets. Its progress response is not proof that messaging is
|
|
1881
|
+
ready; inspect the QuickLink status.
|