Handwritten API clients, UI packages, and developer tooling for Polymorfa.
The development branch contains the TypeScript server SDK, a framework-neutral
browser runtime, shared UI contracts, Web Components, React bindings, thin
Next.js server helpers, and a production-gated developer assistant. It follows
the Messaging and Platform contracts from API source commit
2dd0c1563b1fc4e6525f708afc12e0685110cfe0, now merged into API dev
by PR #260. Graph-compatible
APIs are outside this SDK's initial scope.
The same API revision adds an enrolled hosted message history beta.
MessagingClient.chats has typed conversation and message reads for server
credentials. These methods do not make the beta available before enrollment,
HMS enablement, deployment, and SDK publication.
| Package | Runtime | Responsibility |
|---|---|---|
@polymorfa/sdk |
Node.js 20+ | Messaging, management, system, and Bridge server clients |
@polymorfa/sdk/calls |
Node.js 22+, Browser | Calls lifecycle, answer/join/leave, and programmatic media sockets |
@polymorfa/browser |
Browser | Client-token transport and framework-neutral product controllers |
@polymorfa/ui |
Isomorphic | Appearance, locale, direction, motion, and diagnostic contracts |
@polymorfa/elements |
Browser | Portable custom elements for React-free, Vue, Svelte, and plain HTML applications |
@polymorfa/react |
Browser | React bindings over the same controllers |
@polymorfa/store |
Browser | Opt-in IndexedDB store for webhook-shaped events, with live sources and chat data |
@polymorfa/nextjs |
Server | App Router-compatible client-token and webhook helpers |
@polymorfa/devtools |
Development browser | Configuration, theme, viewport, network, and redacted diagnostic assistant |
The public packages are complete development artifacts on dev. They
publish to npm only as dev prereleases, never as latest. Their names are the intended public identities in the
Polymorfa npm organization. No mobile-native binding is part of this milestone.
For a small browser preview against staging, see the Calls example.
Each push to dev publishes the public packages to npm under the dev
dist-tag, with versions such as 0.1.0-dev.20260919094454:
npm install @polymorfa/sdk@dev
npm install @polymorfa/browser@dev # browser appsPin an exact 0.1.0-dev.<timestamp> version for reproducible installs. No
stable (latest) release exists. Publishing starts once the npm scope and
trusted publisher are configured; until npm view @polymorfa/sdk dist-tags
shows a dev tag, build from source and install the packed tarballs:
git clone --branch dev https://github.com/polymorfa/sdks.git
cd sdks
npm ci
npm run build:workspaces
npm pack -w @polymorfa/sdk # add -w @polymorfa/browser for browser apps
npm install /path/to/sdks/polymorfa-sdk-0.1.0-dev.0.tgz # from your applicationSee docs/releasing.md for the version scheme and the publishing workflow.
npm install github:polymorfa/sdks#dev no longer installs the SDK: the
repository root is a private workspace, and @polymorfa/sdk lives in
packages/typescript.
The published package name and root import are already stable:
import {
BridgeClient,
Client,
MessagingClient,
SystemClient,
} from "@polymorfa/sdk";Node.js 20 or newer is required. The package has no runtime dependencies.
The programmatic Calls client ships inside the same package as the
@polymorfa/sdk/calls subpath; there is no separate Calls package to install.
@polymorfa/browser depends on @polymorfa/sdk and uses this same Calls
client, so errors raised by browser calls are the classes exported from
@polymorfa/sdk/calls:
import { CallsClient } from "@polymorfa/sdk/calls";@polymorfa/sdk/calls needs Node.js 22 or newer for its built-in WebSocket.
On older runtimes, pass a WebSocket implementation to CallsClient.
import { MessagingClient } from "@polymorfa/sdk";
const messaging = new MessagingClient({
credential: {
type: "apiKey",
value: process.env.POLYMORFA_MESSAGING_API_KEY!,
},
apiVersion: "2026-09-22",
});
const sessions = await messaging.sessions.list();
console.log(sessions.data.data, sessions.metadata.requestId);
const sent = await messaging.messages.send(
"support",
{
conversation: { phoneNumber: "+15551234567" },
content: { text: "Hello" },
},
{ idempotencyKey: crypto.randomUUID() },
);
console.log(sent.data.data.id, sent.metadata.attempts);Messaging credentials use an explicit discriminator. Organization server keys
use { type: "apiKey", value }, project tokens use
{ type: "projectToken", value }, and short-lived client tokens use
{ type: "clientToken", value }. A discriminator/prefix mismatch fails before
any network request. Server credentials are rejected in browser runtimes.
The handwritten Messaging resources in this milestone are:
banSafe: retrieve and update project Safe Mode, warm-up, Ban Insurance evidence, and Health policy settings, and one number's Safe Mode override, with an organization API key or project tokensessions: list, retrieve, update, delete, start, stop, restart, logout, account, and entitlement-gated direct JSON QR or phone pairingquickLinks: create, retrieve, and cancel hosted QuickLink pairing sessionscloudOnboarding: continue an issued Meta Cloud API QuickLink from a trusted serverbusiness: manage the connected Business App profile, commerce catalog, products, collections, orders, compliance, linked accounts, and eligibilitycalls: reject an identified incoming Linked Device callvoip: place, accept, reject, leave, and end Polymorfa Calls, add participants, read a person's call permission on a Cloud API Number, check a destination before dialing, and read or update a session's call settingscampaigns: list, create (with inline recipients), retrieve, inspect analytics, launch, pause, resume, stop, requeue, and page or append campaign recipients through the Messaging control planemessages: send every contract-defined message kind through one typed send union, mark seen, set typing state, react, and starmedia: download binary media, retrieve metadata, and request durable object persistencechats: edit or delete sent messages, archive or unarchive chats, and set disappearing-message timerschannels: list, create, retrieve, and delete channels; page channel messages and updates; and manage viewing, reactions, live-update subscriptions, following, and mute statecontacts: list, check registration, retrieve contact metadata, inspect the blocklist, profile picture, user info, devices, and business profile, and block or unblock a contactgroups: list, retrieve, create, join, leave, manage invite codes and participants, and update group profile and permission settingslabels: list, create, update, delete, list a chat's labels, and replace a chat's complete label setidentities: resolve one phone number, public user ID, BSUID, or username to known identity aliasesobservationPolicies: retrieve and update project ceilings and session overrides for presence, typing, and label observationprofile: retrieve the session profile and set its name, status, or JSON URL/base64 picture source, plus delete the pictureprivacy: retrieve account privacy, update setting-specific values, and set the account default disappearing-message timerpresence: set and inspect the session's own non-authoritative presence snapshot, inspect retained chat observations, and subscribe to user presencequickReplies: list, create, replace, and delete remembered Business App quick repliestemplates: list, create, retrieve, update, delete, preview, and submit to Metausers: retrieve a display-only identity verification code for a stable LID-backed user IDwebhooks: list, create, retrieve, update, and delete
import { Client } from "@polymorfa/sdk";
const client = new Client({
credential: {
type: "organizationApiKey",
value: process.env.POLYMORFA_PLATFORM_API_KEY!,
},
});
const projects = await client.projects.list();
const sessions = await client.sessions.list({
projectId: projects.data.data[0]?._id,
});
const project = client.project("project_123");
const events = await project.events.list({ limit: 25 });
console.log(events.items, events.response.metadata.requestId);
const indexed = await project.events.list({ afterOffset: "0" });
console.log(indexed.highWatermark, indexed.nextOffset);Client binds its ownership context when you construct it. An organization
API key without projectId creates an organization client. Call
client.project(projectId) to create an immutable project view, or construct a
project view directly with an organization key or a pmfa_pt_ project token:
const project = new Client({
credential: {
type: "projectToken",
value: process.env.POLYMORFA_PROJECT_TOKEN!,
},
projectId: "project_123",
});Project tokens require an explicit project ID. The server verifies the initial
token-to-project binding. A later attempt to bind that client to another
project fails before transport. Client also rejects browser client tokens and
the CLI-only pmfa_ls_ listener credential before transport. Organization
keys must use the single v1 form pmfa_ plus 72 unpadded base64url characters;
project tokens must use pmfa_pt_ plus 94. The SDK validates that grammar
without decoding the credential. Retired call-agent and socket tickets and
simulated-device capabilities are also rejected before transport.
Both organization and project views expose owner-bound resources:
events: list, retrieve, and replay durable eventswebhooks: list, create, retrieve, update, delete, test, and rotate secretswebhookDeliveries: list and retrieve deliveries, list and retrieve their physical attempts, and retry a deliveryquickLinkSettings: retrieve and update the saved QuickLink configurationoperations: list, get, wait for, list transitions of, and cancel asynchronous operations
Cursor list methods return CursorPage<T>. With afterOffset,
project.events.list returns a FollowableIndexedEventPage; use nextOffset
and nextPage() to continue in ingestion order. Mutations return typed receipts
with the resource, operation, and idempotency identifiers supplied by the API.
const enrollment = await client.projects.requestProductionEnrollment(
"project_123",
{ business },
);
const done = await client.operations.wait(enrollment.data.data.operationId, {
maxWaitMs: 10 * 60_000,
});
if (done.data.status !== "succeeded") {
console.error(done.data.status, done.data.error?.code);
}operations.wait chains server long-polls (up to 30 seconds each) until the
operation succeeds, fails, or is cancelled, its sequence passes
afterSequence, or maxWaitMs (default 5 minutes) ends. It returns the latest
state, so check status. operations.get(id, { wait }) makes one long-poll
read. operations.cancel(id) works only while capabilities.cancellable is
true and sends a generated Idempotency-Key unless you pass one. Reads need
operations:read; cancellation needs operations:cancel. Organization
clients see team and project operations and accept a projectId filter;
project clients see only their project.
The organization view also exposes these management resources:
organizations: retrieve the organization visible to the API keyapiKeys: list key metadata and deactivate an organization API keymembers: list organization membersauditLogs: list the organization audit trail with live action, resource, and limit filterssessionBans: list all or active session banssecurityIncidents: list and acknowledge leaked-credential incidentsprojectTokens: list token metadata for an explicit projectsipTrunks: list, create, retrieve, update, delete, and rotate the credentials of a project's SIP trunks, and read the SIP address your PBX points at withendpoint()(also on project clients)calls: call statistics, paginated call detail records, and CSV or NDJSON export of call records for the team or one project (also on project clients)voice: Voice Automation beta audio uploads, synthesis, previews, retention, deletion and provider credentials, subject to API enrollment and deploymentcallRetention: retrieve and update how long Polymorfa keeps the team's call data (also readable on project clients; changes need a team API key)billing: retrieve balance and currency, inspect usage meters, list transactions and tier pricingusage: read metered call usage for a month, list or iterate usage records for a call or number (also on project clients), and read usage gate modes, limits and decisions (organization clients only). Usage is measured, not charged.banSafe: inspect Health, telemetry collection, signal definitions, findings, restrictions, incidents, claims, and Health action history; report and retract customer incidentsprojects: list, create, request production enrollment, approve, and cancel; retrieve and update Safe Mode, warm-up, Ban Insurance evidence, and Health policy settingssessions: list, start, stop, or delete one session; stop or delete a bounded batch; review and confirm a tier change; create a testing session; and retrieve or update the session Safe Mode overridecampaigns: list, create, retrieve, update, delete, lifecycle actions, analytics, events, and paged or appended recipients. The single-campaign operations require the owningprojectId. This resource is available only on organization clients.createrequiresCreatePlatformCampaignRequestwithnameandprojectId; its named JSON fields pass through unchanged.updateacceptsrecipientListIdto point an unlaunched draft at another audience, or null to detach itcustomers: enable Customers for a project; create, list, retrieve, update, archive, and restore Customers; inspect Numbers and events; create, list, and revoke pairing links; and transfer Numbers between Customersaudiences: list, create from inline members or a spreadsheet import, retrieve, delete, create an upload URL, and add, page, or remove membersoptOuts: list, create one, create a batch, delete by phone number, and read or replace the organization's STOP/START keyword settingscallPolicy: retrieve and replace the team's blocked country codes for callscallOptOuts: list, add one, import up to 5,000, and remove entries on the team's do-not-call listmedia: retrieve a URL, delete, and create an upload URL
Customer creation and pairing-link creation require caller-supplied
idempotency keys. The SDK returns the pairing URL only on the first successful
creation attempt. Customer list responses retain their cursor metadata under
response.data.page.
Audience creation and membership, campaign creation and recipients, and opt-out
settings are fully typed. The remaining campaign, audience, opt-out, and media operations
expose their payloads as open objects in the pinned contract, so those methods
use the exported PlatformPayload type instead of claiming fields the contract
does not define.
Client.campaigns.recipients returns a cursor page. status finds, for
example, the recipients a campaign skipped because they opted out:
let cursor: string | undefined;
do {
const page = await client.campaigns.recipients(campaignId, {
projectId,
status: "skipped",
...(cursor === undefined ? {} : { cursor }),
});
for (const recipient of page.data.data) {
console.log(recipient.phone, recipient.lastError);
}
cursor = page.data.page.nextCursor ?? undefined;
} while (cursor !== undefined);Appending recipients or audience members accepts partial success: the result
reports added, duplicateCount, invalidCount and up to 20 invalidRows.
Platform template and Flow endpoints require a live dashboard bearer and reject
organization server keys. They are intentionally absent from Client;
browser template tooling must reach them through an application-owned server
adapter that authorizes the signed-in user.
Polymorfa checks every call against the team's call policy before the
destination rings. The policy is team-wide and needs an organization key;
project tokens and client tokens receive 403.
import { Client, MessagingClient } from "@polymorfa/sdk";
const client = new Client({
credential: {
type: "organizationApiKey",
value: process.env.POLYMORFA_ORG_KEY!,
},
});
const policy = await client.callPolicy.retrieve();
await client.callPolicy.update({
blockedCountryCodes: ["44", "1876"],
expectedRevision: policy.data.revision,
});
const added = await client.callOptOuts.create({
phoneNumber: "+14155550123",
note: "Asked not to be called on 2026-09-18",
});
added.metadata.status; // 201 for a new entry, 200 when already listed
for await (const entry of await client.callOptOuts.list({ limit: 100 })) {
console.log(entry.phoneNumber ?? entry.bsuid, entry.source);
}Blocked codes are country calling codes or longer dialing prefixes, 1 to 4
digits without +. update replaces the whole list; send [] to allow every
country. Pass the revision you read as expectedRevision to refuse an
overwrite (409 state_conflict). import adds up to 5,000 entries at once and
reports invalid ones in rejected. A full list (100,000 entries) fails with
409 call_opt_out_limit.
A Cloud API Number can call a person only after that person grants permission.
Ask with callPermissionRequest content, then read the answer:
const messaging = new MessagingClient({
credential: {
type: "apiKey",
value: process.env.POLYMORFA_MESSAGING_API_KEY!,
},
});
await messaging.messages.send("support", {
conversation: { phoneNumber: "+14155550123" },
content: {
callPermissionRequest: {
body: "We would like to call you about order 1522.",
},
},
});
const permission = await messaging.voip.retrieveCallPermission(
"support",
"+14155550123",
);
permission.data.data.status; // "none" | "temporary" | "permanent" | "revoked"
const check = await messaging.voip.check({
session: "support",
to: "+14155550123",
});
check.data.data.refusal; // null, or the first reason a call would failretrieveCallPermission asks WhatsApp during the request: fresh is false
when WhatsApp could not be reached and the stored state is returned with
actions: null. check runs the same checks a placement runs without placing
a call or reserving anything. Both need a server credential.
retrieveCallPermission answers 409 unsupported_for_connection on a
linked-device Number; check supports linked-device Numbers and returns
permission: null for them. If required call-check state is unavailable,
check raises PolymorfaServerError (503 service_unavailable); no allow or
refusal result is returned.
A send refused by WhatsApp's request limit raises PolymorfaRateLimitError
with code call_permission_request_limited, rateLimitReason
call_permission_request, and the retry-after header in
error.metadata.headers. An already permanent permission raises
PolymorfaConflictError (call_permission_granted). A refused placement
raises PolymorfaAuthorizationError with call_recipient_opted_out or
call_destination_blocked.
Permission changes arrive as the call.permission_changed webhook event, typed
as CallPermissionChangedPayload. No event is sent when a temporary permission
reaches expiresAt.
SystemClient calls the credential-free status, version, readiness, and
liveness routes. It does not accept a credential:
import { SystemClient } from "@polymorfa/sdk";
const system = new SystemClient();
const [status, version, health, ping] = await Promise.all([
system.status(),
system.version(),
system.health(),
system.ping(),
]);BridgeClient accepts only a project token and exposes one discovery method:
import { BridgeClient } from "@polymorfa/sdk";
const bridge = new BridgeClient({
credential: {
type: "projectToken",
value: process.env.POLYMORFA_PROJECT_TOKEN!,
},
});
const route = await bridge.routes.resolve();
console.log(route.data.wsUrl, route.data.expiresAt);Route discovery returns the regional Bridge connection details. The client does
not open the WebSocket, manage reconnects, or participate in the CLI listener
protocol. A pmfa_ls_ listener credential is rejected before transport.
Every request resolves to an ApiResponse<T>:
interface ApiResponse<T> {
readonly data: T;
readonly metadata: {
readonly status: number;
readonly requestId?: string;
readonly apiVersion?: string;
readonly attempts: number;
readonly headers: Readonly<Record<string, string>>;
};
}Failures use exported error classes for configuration, validation, authentication, authorization, not found, conflict, rate limiting, server, connection, timeout, and caller-cancellation cases. HTTP errors carry status, request ID, decoded details, and response metadata when the server supplied them.
Every API error also exposes the fields from its error body:
import { PolymorfaError, PolymorfaRateLimitError } from "@polymorfa/sdk";
try {
await messaging.messages.send("sales", message);
} catch (error) {
if (error instanceof PolymorfaError) {
error.code; // "conversation_window_closed", typed as PolymorfaErrorCode
error.requestId; // body `request_id`, else the X-Request-Id header
error.requestLogUrl; // Console request log, for team keys and project tokens
error.docUrl; // https://docs.polymorfa.com/api/errors#conversation-window-closed
}
if (error instanceof PolymorfaRateLimitError) {
error.rateLimitReason; // "whatsapp", "request_rate", ...
}
}PolymorfaErrorCode lists the documented codes, including
recipient_not_on_whatsapp, conversation_window_closed,
template_not_approved, media_too_large, whatsapp_rate_limited,
new_chat_limit_reached, whatsapp_account_restricted, the BanSafe codes, the Calls and SIP trunk codes,
and the call consent codes (call_recipient_opted_out,
call_destination_blocked, call_permission_request_limited,
call_permission_granted, call_opt_out_limit),
and still accepts codes a newer API adds. POLYMORFA_ERROR_CODES and
isKnownPolymorfaErrorCode() are exported. requestLogUrl is absent for
client tokens and for requests the API did not log. BrowserError exposes
code, requestId, and docUrl from the same body, so browser callers get the
request ID even when the X-Request-Id header is not readable.
Client defaults are a 30-second timeout and two network retries. Configure them on the client or override them for one request:
const controller = new AbortController();
const response = await client.projects.create(
{ name: "Support" },
{
signal: controller.signal,
timeoutMs: 5_000,
maxNetworkRetries: 1,
idempotencyKey: "project-support-2026-08-19",
},
);GET, HEAD, and OPTIONS requests can retry transient connection failures,
timeouts, HTTP 408, 409, 429, and server failures. POST, PUT, PATCH, and DELETE
requests retry only when the caller supplies an idempotency key. The transport
honors Retry-After, then uses bounded exponential backoff with jitter.
Campaign recipient and audience member appends (campaigns.addRecipients on
both clients and audiences.addMembers) are sent once. The API does not replay
them, so a retry after a lost response would count the first attempt's rows as
duplicates. They retry only when that request sets both maxNetworkRetries
and idempotencyKey; the key does not make the API replay the append.
Set apiVersion on a client or a single request. Use a date-form API revision,
such as 2026-09-22. The SDK sends it as the Polymorfa-Version header.
Native calls default to 2026-09-22; the API accepts explicit revisions from
2026-03-20 and rejects earlier pins.
Every client exposes raw.request<T>() for deliberate API escape hatches:
const response = await client.raw.request<{ data: unknown }>({
method: "GET",
path: "/platform/events/event_123",
});Organization raw paths remain relative API paths. Project raw paths are
relative to the bound project and receive the encoded
/platform/projects/{projectId} prefix automatically. Project raw requests reject
absolute URLs, traversal, explicit project prefixes, backslashes, and
Authorization overrides before transport. Raw requests retain typed errors,
metadata, cancellation, API versions, retry rules, and idempotency.
raw.paginate() accepts a page decoder and returns CursorPage<T>, which
supports items, nextCursor, hasMore, nextPage(), and async item
iteration. The current contract has few cursor endpoints; the primitive is
available for new endpoints without reimplementing transport behavior in the
CLI.
Verify the exact raw request body before parsing:
import { isEvent, webhooks } from "@polymorfa/sdk";
const event = await webhooks.verify({
body: rawBody,
signature: signatureHeader,
secret: webhookSecret,
});
if (isEvent(event, "message.received")) {
console.log(event.payload);
}Native deliveries use the hexadecimal X-Webhook-Signature value. The helper
also accepts the sha256=<hex> compatibility form. Verification uses
HMAC-SHA256 and constant-time comparison over the unmodified bytes. Recognized
events narrow to exported payload types, including messages, sessions, groups,
presence, contacts, chats, calls, labels, history sync, Meta Cloud API contact
sync and Business app echoes, command results, call permission changes, and business quick replies. Unknown event names and payloads are preserved for
forward compatibility.
webhooks.verifySignature() returns a boolean without parsing.
webhooks.createFixture() creates exact-byte local fixtures, and
webhooks.verifyLocal() verifies payloads re-signed by local CLI forwarding.
The older constructWebhookEvent and verifyWebhookSignature exports remain
available through the first stable major. A later major can remove them with a
migration release.
Messaging server credentials can manage webhook registrations through
MessagingClient.webhooks. The management Client owns the separate durable
organization and project event, webhook, delivery, attempt, and operation
resources described above. Dashboard and staff routes retain their separate
credential requirements.
Client.events.stream() exposes the server event stream as an AsyncIterable,
and Client.events.liveSource() adapts it for @polymorfa/store. Both require
the server event stream's scope and beta access. polymorfa listen connects to
a separate CLI-only forwarding protocol; its pmfa_ls_ credential cannot be
used by Client, MessagingClient, or their raw request helpers.
MessagingClient.quickLinks.create(), retrieve(), and cancel() map the
authenticated hosted lifecycle at /messaging/quicklinks. They accept organization
API keys or project tokens with quicklink:manage; browser client tokens fail
before transport. Organization keys can set projectId on creation, while a
project token remains bound by the server.
These methods expose the short-lived connection URL and status record. They do not add list, recovery, or history operations that the API does not provide.
client.quickLinkSettings.retrieve() and update() map only the management
GET /platform/quicklink and PUT /platform/quicklink settings contract. The same methods
on client.project(projectId) use the immutable project ownership context.
Saved settings hold the project's successCallbackUrl and failureCallbackUrl
HTTPS destinations and allowPhoneChange, which controls whether recipients can
replace a prefilled number (default false). The API copies callback
destinations into each link when it is issued. Settings have no redirect-URI
allowlist. hideWatermark: true requires an active Branded QuickLink add-on.
Browser code accepts only short-lived pmfa_ct_ tokens returned by an
application callback. It rejects server credentials and absolute request URLs.
The framework-neutral controllers cover conversations, composing,
template building, and one-to-one and group calls. They expose immutable snapshots through
getSnapshot() and subscribe(); React and Web Components render those same
objects rather than reimplementing product state.
QuickLink is a hosted Polymorfa page, not a browser SDK surface. Create the link
on your server with MessagingClient.quickLinks.create() and send the person
to the returned data.url.
import {
BrowserMessagingClient,
createClientTokenProvider,
} from "@polymorfa/browser";
const getClientToken = createClientTokenProvider();
const messaging = new BrowserMessagingClient({
session: "support",
getClientToken,
});
await messaging.messages.setTyping({
conversation: { id: "739182640518203" },
state: "typing",
});The browser Messaging client is session-bound and exposes only the runtime's
exact client-token action allowlist. createBrowserComposerActions connects
text/reply compose boxes to that client. Conversation history, template
management, media upload, and call lifecycle/control stay behind explicit
application-owned adapters because client tokens cannot call those routes.
The canonical template builder is the paired path for template management:
MessagingClient.templates performs server operations,
createTemplateBuilderRoute authorizes and scopes same-origin browser actions,
and createSameOriginTemplateBuilderTransport connects the framework-neutral
controller without exposing a server key, project slug, or submission session.
The controller models standard, carousel, authentication, and limited-time
offer definitions and keeps saving separate from Meta submission.
@polymorfa/elements provides custom elements for plain HTML and for frameworks
that interoperate with the Custom Elements standard. @polymorfa/react
provides idiomatic hooks and components, including controlled and SDK-owned
controller lifecycles.
createBrowserCalls connects the shared @polymorfa/sdk/calls model to the
existing browser controller and WebRTC media. It places calls directly with a
short-lived client token, receives lifecycle events, and exposes the active
model as controller.call. Pass its controller to React or Web Components.
import { createBrowserCalls } from "@polymorfa/browser";
const calls = createBrowserCalls({ session: "support", getClientToken });
await calls.connect();
await calls.controller.place("+15550100");
// Release the widget and connections when leaving the application.
await calls.dispose();The token needs voip_place, voip_answer and voip_signal actions. Your
server mints it with POST /platform/client-tokens; the browser uses it
directly for REST calls and for the first frame of each call socket. No
calling ticket is involved. Requests use the token's bound session.
Incoming calls ring until a participant answers or declines them. Several
calls can ring at once, and the widget never declines one for you. Answer
takes an exclusive choice: false (the default) leaves other participants
ringing so they can join, and true claims the call. A call another
participant answered without a claim offers Join; a claimed call shows as
answered elsewhere. Leave closes only this browser's connection; hang-up ends
the call for everyone. A call you placed offers only hang-up until it
connects, and a microphone failure while it rings ends it. Each remote participant's video arrives as its own
stream in controller.remoteVideos; call audio is merged. Signaling, media
negotiation and socket transports are internal to the SDK; the packages export
only these calling operations.
Each call reports its capabilities (video, invite, mute), and every
component gates its controls on them. A placed call starts with video and
invitations allowed and takes the platform's report when the callee answers.
The
controller also owns capture/playback device choice (setPreferredDevices,
switchDevice, refreshDevices) so a microphone or camera swap mid-call is a
track replacement, not a renegotiation. The shared call model supports
participant invitations. Browser WebRTC calls receive live participant joins,
state changes, and departures through the lifecycle stream.
The browser and Calls clients send call diagnostics for their own media
connections (quality figures and error codes, no personal data) so the Console
can show why a call sounded bad or failed. Pass diagnostics: false to turn
this off; MessagingClient.voip.report() sends your own.
@polymorfa/react ships the complete call UI: CallSurface (incoming card,
stage, control dock, and a pop-out window), plus IncomingCallCard,
CallStage, CallControls, ParticipantVideoGrid, ParticipantList, and
DialPad for composition. The design mirrors
the official WhatsApp desktop call windows in a monochrome Material-3 voice;
colors derive from the shared appearance variables.
@polymorfa/store keeps an opt-in IndexedDB copy of webhook-shaped events for
applications that store messages themselves. Your backend receives webhooks
and streams them to the browser; the store files messages, conversations,
contacts, presence, calls, labels, sessions, templates, and other events into
separate stores and feeds ConversationController through
createStoreConversationSource(). Message content is written to the device;
see the store guide for encryption,
redaction, and retention.
@polymorfa/nextjs builds Web Request/Response handlers, so it has no Next.js
runtime dependency. Applications provide their own authorization and minting
logic; webhook helpers read raw bytes once and delegate verification to
webhooks.verify from the server SDK.
Template routes use the same application-owned authorization boundary. Project scope and Cloud API session selection are resolver callbacks that run only on the server; browser-provided replacements are ignored.
@polymorfa/devtools provides an explicit development overlay for appearance,
RTL, motion, viewport and network testing plus redacted request diagnostics. It
only enables when trusted build and token environments match and are both
non-production. Query-string flags cannot enable it, and the production
subpath exports an inert mount function.
contracts/coverage.json records every Messaging and Platform operation in its
pinned contracts. Covered mappings include methods in the server, browser, and
Calls packages. A mapping records an HTTP operation, not package publication or
live-call readiness. The durable management resources and QuickLink settings
map to typed Client resources; dashboard, staff, and CLI-listener routes keep
explicit credential-boundary exclusions.
npm run check:coverage requires every contract operation to have a ledger
row. It permits explicit missing and excluded entries; passing that check does
not establish API parity. Raw requests never count as typed coverage. See the
contract notes for source hashes and reconciliation
rules.
The repository is an npm workspace: packages/typescript is @polymorfa/sdk,
and packages/calls is a private workspace compiled into
@polymorfa/sdk/calls.
npm install
npm test
npm run lint
npm run format:check
npm run typecheck
npm run build
npm run build:workspaces
npm run check:coverage
npm run check:namesPackage publication, tags, and GitHub releases require a separate explicit release instruction.
The typed webhook catalog includes session.restriction_updated with
type, active, enforcementType, expiresAt, and observedAt.
session.logged_out requires a numeric code and a reason of banned,
device_removed, or unknown. Test event requests support the restriction
fixture with restrictionActive and the call-end reason call_restricted.
Client.callRetention covers the team call-retention settings. Client.calls
covers the three public call analytics and export operations. Client.voice
covers the Voice audio and credential operations. Client.callPolicy and
Client.callOptOuts cover consent controls. The contract snapshot is pinned
to API source commit 2dd0c1563b1fc4e6525f708afc12e0685110cfe0, now
merged into API dev by PR #260.
Message receipts and webhook message references expose whatsapp_ids, with
linked_devices, official_api, or both observed provider references. Unknown
keys are omitted. A temporary optional whatsapp_id alias remains for older consumers. Use the separate Polymorfa id
for replies and actions. The server SDK exports WhatsAppMessageIds; the browser
SDK exports BrowserWhatsAppMessageIds. See the pinned component revision in
contract notes.
The development SDK types include Hybrid Link controls. Their presence does not enable the private preview: the API checks live team/project enrollment, Number entitlement, and operational availability. Browser client tokens cannot use the Hybrid control or message-operation methods, and Hybrid sends through browser client tokens are unavailable in this preview.
Use quickLinks.availability({projectId, session}) before offering an added
connection. Initial setup uses quickLinks.create({connectionGoal: "hybrid"});
adding a transport uses purpose: "add_connection", the existing session, and
addConnection: "linked_devices" | "official_api". The Number and Customer stay
the same. configuration.connectionPreference: "both" still chooses one transport.
QuickLink status includes hybridPhase for Cloud setup, Linked pairing, repair,
and readiness.
Native send/reaction requests and edits accept transport: "auto" | "linked_devices" | "official_api". chats.deleteMessage accepts the choice in
its options. Explicit choices never fall back. Raw Graph-compatible requests can
use graphTransportHeaders(transport); Graph remains outside handwritten method
coverage. Routing details appear in response metadata.transport,
metadata.routingReason, and metadata.operationId when supplied by the API.
An accepted uncertain send raises send_outcome_unknown with its operation ID.
The SDK stops automatic retries when a response carries an accepted operation ID,
even if its body cannot be read. In that case the thrown error carries the ID in
error.metadata.operationId.
Read messages.operationStatus(session, operationId) with the original issuing
server principal. pending and unknown do not permit another send or a
transport switch. A terminal rejected result carries
rejectionCode: "hybrid_authority_unavailable" and proves that this operation
ended before the provider effect. Fix the cause before starting a new operation.
hybridLink.getPolicy(scope) and setPolicy(scope, body) preserve team, project,
or Number authority. Writes require the exact expectedRevision, prefer, and
allowedTransports; narrower policies cannot widen ancestor restrictions.
hybridLink.state(session) reads connection status. setPaused(session, {expectedRevision, paused}) changes routing at the exact current revision.
MIT