diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6b6999..1703aa0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,82 @@ # Changelog +## Unreleased + +- `createSignalingCallsBackend` no longer invents a call id for outbound + calls. Nothing on the client-token surface dials a destination, so an + invented id reached no pod and its offer was refused as not-ready forever. + Outbound calling now goes through a `place` hook that hands the destination + to the application's server and takes back the platform's call id; the + removed `createCallId` option had no working use. +- `CallsSocket` heartbeats. A half-open connection still reports OPEN, so + candidates were dropped while the media factory kept REST polling disabled; + an unanswered `ping` now closes it and reconnects. `heartbeatMs` tunes the + period, 0 disables it. +- `CallsSocket` takes a `line`, carried onto the calls it delivers. It always + reported `linkedDevice`, so a Business Calling API session showed video + controls on an audio-only line. +- `CallsSignalingClient.socketTicket` no longer names a session. The client + token is bound to one, and the API answers 403 when a request names another. +- `CallsSocket` reports a signaling client that cannot mint tickets as an + `unsupported` error and stops, instead of backing off forever against a + failure that can never succeed; ordinary ticket failures still retry, now + reported as `ticket_failed`. +- `CallsSocket` gained `onError`, so a server `error` frame reaches the + application instead of being parsed and dropped while the socket keeps + reconnecting against a permanent failure. +- Timer defaults are bound to `globalThis`. `CallsController`, `CallsSocket`, + `WebRtcMediaFactory`, and the QuickLink controller called the unbound + `setTimeout`/`setInterval` with the instance as receiver, which browsers + reject with `TypeError: Illegal invocation`. +- Calls hardening: a recovered ICE flap leaves `reconnecting` instead of + waiting for a connection-state change that may never come; mute pressed + while the microphone was still being acquired now reaches the tracks; + pushed ICE candidates are held until the answer is applied rather than + discarded; setup failures release the candidate subscription; an audio-only + call no longer keeps an unnegotiated camera track (which had made the video + upgrade unretryable); a failed upgrade rolls the camera back; the calls + WebSocket survives a throwing constructor and validates every frame shape; a + camera button in `pmfa-call` upgrades an audio call over a video-capable + line, as the React dock's already did; a + failed re-offer rolls the peer out of `have-local-offer` so later upgrades + and ICE restarts are not blocked by the stale offer; the audio→video upgrade + acquires the camera chosen during the call rather than the one selected when + it opened; a failed device swap + releases the stream it acquired; and `pmfa-call` localizes the connected + heading rather than rendering a raw status identifier. + +- Added `CallsSocket` (the calls WebSocket: pushed `call.*` lifecycle events, + ICE both ways, ticket-based auth, capped reconnect) as a drop-in lifecycle + source for `createSignalingCallsBackend` and a `candidateTransport` for + `WebRtcMediaFactory`; `CallsSignalingClient` gained `renegotiate`, + `socketTicket`, and `socketUrl`; `BrowserTransport` exposes `baseUrl`. +- `CallsController` gained `enableVideo()` (audio→video upgrade by re-offer), + a `reconnecting` status with ICE restart and a bounded resumption window, + and ends the call as `pod_lost` / `capacity` when the offer is refused + with 410 / 503. The React camera button upgrades audio calls to video; + `CallStage` and `pmfa-call` show "Reconnecting…". +- Added `MessagingClient.voip.socketTicket` and `voip.agentToken`; typed + `maxSetupsPerMinute` in the client rules; refreshed the contracts and the + coverage ledger to polymorfa@b413f2c6a (renegotiate, ws-ticket, + agent-token). + +- Added `MessagingClient.voip.token` for minting the browser call token, typed + the client-rules response the runtime actually returns (including the calls + bindings `maxConcurrency` and `allowedNumber`), and typed client actions. +- Added `createSignalingCallsBackend`, `IncomingCallRelay`, and + `incomingCallFromWebhook` so browser calls run end to end on the REST + signaling surface; `CallsController` now carries the calling `line` and + `capabilities`, device lists and selections, live device switching, and + `connectedAt`. +- Replaced the placeholder `CallSurface` with the complete call UI — + `IncomingCallCard`, `CallStage`, `CallControls`, `DialPad`, a pop-out call + window, and `useCallDuration` — themed from the shared appearance variables + and localized through new `calls.*` messages; `pmfa-call` follows the same + controller contract. +- Refreshed the Messaging and Platform contracts and the coverage ledger for + the `/api/voip/*` signaling routes and the `call.ended` / `call.telemetry` + webhook events. + ## 0.1.0-dev.0 - 2026-08-19 - Added handwritten Messaging clients for sessions, messages, and webhooks. diff --git a/README.md b/README.md index cf846509..e6220eb5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 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 recorded at source revision -`f156af2dda13e62b6b106a542fdedb39524bdb66`. Graph-compatible APIs are outside +`b413f2c6a701c86ec593241a70145ee89cc2bcd2`. Graph-compatible APIs are outside this SDK's initial scope. ## Package architecture @@ -84,6 +84,7 @@ The handwritten Messaging resources in this milestone are: - `business`: manage the connected Business App profile, commerce catalog, products, collections, orders, compliance, linked accounts, and eligibility - `calls`: reject an identified incoming Linked Device call +- `voip`: mint the browser call token used by `@polymorfa/browser` signaling - `campaigns`: list, create, retrieve, inspect analytics, launch, pause, resume, stop, and requeue project campaigns through the Messaging control plane - `messages`: send every contract-defined message kind through one typed send @@ -338,15 +339,34 @@ that interoperate with the Custom Elements standard. `@polymorfa/react` provides idiomatic hooks and components, including controlled and SDK-owned controller lifecycles. -### Calls boundary - -Call media and signaling follow the implementation in `voip-v2`: browser WebRTC -media, offer/answer exchange, trickle ICE candidate submission and polling, and -the existing `/api/voip/calls/{id}` signaling paths. The application supplies a -`CallsBackend` for place, answer, reject, hangup, and lifecycle events because -that control plane is not exposed by the current `voip-v2` REST signaling -surface. The SDK does not invent group rooms, participants, reactions, hand -raising, or waiting rooms. +### Calls + +Call media and signaling follow the platform's browser signaling surface: +WebRTC media, offer/answer exchange, trickle ICE candidate submission and +polling, and idempotent teardown over the `/api/voip/calls/{id}` paths, all on +the client token. `createSignalingCallsBackend` is the `CallsBackend` for that +surface; outbound calls go through its `place` hook, because starting a call +is a server-key operation and the browser can only attach media to a call the +platform already owns. `IncomingCallRelay` is where the application hands the browser the +`call.received` webhook it received on its own server +(`incomingCallFromWebhook` maps the payload). The server mints the browser +token with `MessagingClient.voip.token` after granting the session's client +rules the `voip_place`, `voip_answer`, and `voip_signal` actions; those rules +also bind calls per token (`maxConcurrency`, `allowedNumber`). + +Calls carry a `line`: `linkedDevice` (a paired WhatsApp device session, audio +and video) or `cloudApi` (the WhatsApp Business Calling API, audio only). Every +component gates on the snapshot's `capabilities`, never on the line name. 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 SDK does not invent group rooms, +participants, reactions, hand raising, or waiting rooms. + +`@polymorfa/react` ships the complete call UI: `CallSurface` (incoming card, +stage, control dock, and a pop-out window), plus `IncomingCallCard`, +`CallStage`, `CallControls`, 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. ## Next.js and dev mode diff --git a/contracts/coverage.json b/contracts/coverage.json index 710300c4..77fd99ed 100644 --- a/contracts/coverage.json +++ b/contracts/coverage.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceCommit": "fc681025966227ff918a701cd82b06157702dde6", + "sourceCommit": "b413f2c6a701c86ec593241a70145ee89cc2bcd2", "excludedContracts": [ { "family": "graph", @@ -8,12 +8,80 @@ } ], "operations": [ + { + "family": "messaging", + "method": "DELETE", + "path": "/api/projects/{projectSlug}/templates/{id}", + "operationId": "deleteProjectTemplate", + "fingerprint": "cd2c6b07f251df24cc5c3df259dba804846f5e26e9364fb02ff234626ad2df61", + "typescript": { + "status": "covered", + "method": "MessagingClient.templates.delete" + } + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/api/sessions/{session}", + "operationId": "deleteSession", + "fingerprint": "d38aa6b9a4d93f424bf074e8efded23980b673c4132aa04d9dfbd484edbf265f", + "typescript": { + "status": "covered", + "method": "MessagingClient.sessions.delete" + } + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/api/sessions/{session}/client-rules", + "operationId": "deleteClientRules", + "fingerprint": "5e776e131aeff24de634c8c23228b96ea9db5f39ef12032934cd95440bd3fd93", + "typescript": { + "status": "covered", + "method": "MessagingClient.clientTokens.deleteRules" + } + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/api/voip/calls/{id}", + "operationId": "voipTeardown", + "fingerprint": "fa59c21ef613147f39e23dfda41cf9ef0bde99d3bd7f888574bb68b93e4afa01", + "typescript": { + "status": "excluded", + "reason": "Browser client-token signaling route served by @polymorfa/browser (CallsSignalingClient over BrowserTransport); it does not accept the server credential used by the TypeScript server SDK.", + "milestone": "not-server-sdk" + } + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/api/webhooks/{id}", + "operationId": "deleteWebhook", + "fingerprint": "1752b4716afc7d953a21b94ae8248b82a16cb91917349b4f6f0a9a8b3d17c1b7", + "typescript": { + "status": "covered", + "method": "MessagingClient.webhooks.delete" + } + }, + { + "family": "messaging", + "method": "DELETE", + "path": "/api/widget/sessions/{id}", + "operationId": "cancelWidgetSession", + "fingerprint": "2fd55587eeedac38f66426d64d6a21a79d41efa723ec76ed80c37b8c2fb23af3", + "typescript": { + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" + } + }, { "family": "messaging", "method": "DELETE", "path": "/api/{session}/business/collections/{collectionId}", "operationId": "deleteBusinessCollection", - "fingerprint": "0f092de90dae553d9af58b63410078d0a135c82e3a277c7f5f5e6619ba0e85a8", + "fingerprint": "792147ceb190658a8b1e08d8865b3f49f3193a4e2dbbe097c59937c9b92d5532", "typescript": { "status": "covered", "method": "MessagingClient.business.deleteCollection" @@ -24,7 +92,7 @@ "method": "DELETE", "path": "/api/{session}/business/products/{productId}", "operationId": "deleteBusinessProduct", - "fingerprint": "881e214d7ca6d6e0d7143e4e3fc3abc271a016216eb47ca5aecdfd764fccf130", + "fingerprint": "194ae03af931a8f74db7078f010c9e1cf2519bc703a359405b60b6051bab7a44", "typescript": { "status": "covered", "method": "MessagingClient.business.deleteProduct" @@ -109,58 +177,203 @@ }, { "family": "messaging", - "method": "DELETE", + "method": "GET", + "path": "/api/info/status", + "operationId": "getStatus", + "fingerprint": "d718b1693efe722ebf046a8fe6525eccb179052320870a2ce40dbeb63e612f87", + "typescript": { + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/info/version", + "operationId": "getVersion", + "fingerprint": "2ab7906884ffa07c978e9378490a193652ce1e1e960459908e23c4ab76838996", + "typescript": { + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/media/{id}", + "operationId": "downloadMedia", + "fingerprint": "60067d01771c53dba1a7ab71cc768f8bd2ca92cefd4cddbef887d0088a4395d9", + "typescript": { + "status": "covered", + "method": "MessagingClient.media.download" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/media/{id}/info", + "operationId": "getMediaInfo", + "fingerprint": "8de9ff6257a2fed2ce72a97ec883dba64859cb938683ee12bda1c2bd95457fe5", + "typescript": { + "status": "covered", + "method": "MessagingClient.media.retrieve" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/operations/{operation}", + "operationId": "getOperation", + "fingerprint": "e13cfd45b2d6b3256c3fa832aca786242656ae6dc71d2d161ac4ab434d5e6688", + "typescript": { + "status": "covered", + "method": "MessagingClient.operations.retrieve" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/projects/{projectId}/observation-policy", + "operationId": "getProjectObservationPolicy", + "fingerprint": "d54ca3815c88eec4211dd5f89b6d55487de4a43b78be1d1511eb31db74053a9b", + "typescript": { + "status": "covered", + "method": "MessagingClient.observationPolicies.retrieveForProject" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/projects/{projectSlug}/campaigns", + "operationId": "listCampaigns", + "fingerprint": "edf002e4bf2e07dbe019805e1b0180fc9f4aab8ea58e1e93bc3bf9d8f9ec6f12", + "typescript": { + "status": "covered", + "method": "MessagingClient.campaigns.list" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/projects/{projectSlug}/campaigns/{id}", + "operationId": "getCampaign", + "fingerprint": "ae8e220be5c1aaac15befd796c43f4fcc3fd61e68b7c954193e73a0082a4eefb", + "typescript": { + "status": "covered", + "method": "MessagingClient.campaigns.retrieve" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/projects/{projectSlug}/campaigns/{id}/analytics", + "operationId": "getCampaignAnalytics", + "fingerprint": "07ed01febe6421b7722f0d3679463d787ed653d60d677744e4bbd5fde524b371", + "typescript": { + "status": "covered", + "method": "MessagingClient.campaigns.analytics" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/projects/{projectSlug}/templates", + "operationId": "listProjectTemplates", + "fingerprint": "d9c2a54361638c27c36cfa0c9c2f5d92daee3ee47f5ae7c7f0b1c4d316b22360", + "typescript": { + "status": "covered", + "method": "MessagingClient.templates.list" + } + }, + { + "family": "messaging", + "method": "GET", "path": "/api/projects/{projectSlug}/templates/{id}", - "operationId": "deleteProjectTemplate", - "fingerprint": "cd2c6b07f251df24cc5c3df259dba804846f5e26e9364fb02ff234626ad2df61", + "operationId": "getProjectTemplate", + "fingerprint": "cb60900192a486c16562867a1cfb3b595444a467b7d91b43ea7aa107faf951c1", "typescript": { "status": "covered", - "method": "MessagingClient.templates.delete" + "method": "MessagingClient.templates.retrieve" } }, { "family": "messaging", - "method": "DELETE", + "method": "GET", + "path": "/api/sessions", + "operationId": "listSessions", + "fingerprint": "d97bd519e00e186ad88bdc449f6a5138b2c384fa31b1f19bf5d27d3c5fbe5c65", + "typescript": { + "status": "covered", + "method": "MessagingClient.sessions.list" + } + }, + { + "family": "messaging", + "method": "GET", "path": "/api/sessions/{session}", - "operationId": "deleteSession", - "fingerprint": "80fc4cf96d1f378bcc4269bcec2d581548463f787f1ddcbe3ee21dc0c7bb382b", + "operationId": "getSession", + "fingerprint": "44932148368b9db333d13748336ea655e2405af69a10e69c542d3e5e5d85250c", "typescript": { "status": "covered", - "method": "MessagingClient.sessions.delete" + "method": "MessagingClient.sessions.retrieve" } }, { "family": "messaging", - "method": "DELETE", + "method": "GET", "path": "/api/sessions/{session}/client-rules", - "operationId": "deleteClientRules", - "fingerprint": "5e776e131aeff24de634c8c23228b96ea9db5f39ef12032934cd95440bd3fd93", + "operationId": "getClientRules", + "fingerprint": "f878fc588bcf2c51faa82d6c1d7e2bf2a3765c01affd9513a7b0004b07e3b5ec", "typescript": { "status": "covered", - "method": "MessagingClient.clientTokens.deleteRules" + "method": "MessagingClient.clientTokens.retrieveRules" } }, { "family": "messaging", - "method": "DELETE", - "path": "/api/webhooks/{id}", - "operationId": "deleteWebhook", - "fingerprint": "1752b4716afc7d953a21b94ae8248b82a16cb91917349b4f6f0a9a8b3d17c1b7", + "method": "GET", + "path": "/api/sessions/{session}/me", + "operationId": "getSessionAccount", + "fingerprint": "b989986752df43ace50259ecd84b655ad9b01e504a46c9b2b449a3c6e23adc00", "typescript": { "status": "covered", - "method": "MessagingClient.webhooks.delete" + "method": "MessagingClient.sessions.account" } }, { "family": "messaging", - "method": "DELETE", - "path": "/api/widget/sessions/{id}", - "operationId": "cancelWidgetSession", - "fingerprint": "2fd55587eeedac38f66426d64d6a21a79d41efa723ec76ed80c37b8c2fb23af3", + "method": "GET", + "path": "/api/voip/calls/{id}/candidates", + "operationId": "voipCandidates", + "fingerprint": "7fe4f817d13b49bf9910a47de56d9cb398d97e15f4573624670a75c94a30a212", "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" + "status": "excluded", + "reason": "Browser client-token signaling route served by @polymorfa/browser (CallsSignalingClient over BrowserTransport); it does not accept the server credential used by the TypeScript server SDK.", + "milestone": "not-server-sdk" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/webhooks", + "operationId": "listWebhooks", + "fingerprint": "8bda5679f018323e4fb249df16a541ae9ebe812121f7c64974d4665d3d407eac", + "typescript": { + "status": "covered", + "method": "MessagingClient.webhooks.list" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/webhooks/{id}", + "operationId": "getWebhook", + "fingerprint": "08a27c707cd6c742eff86a527ef3ef399fab813a117d49ae1e6c1911f49a4245", + "typescript": { + "status": "covered", + "method": "MessagingClient.webhooks.retrieve" } }, { @@ -320,11 +533,33 @@ { "family": "messaging", "method": "GET", - "path": "/api/{session}/contacts/{contactId}", - "operationId": "getContact", - "fingerprint": "42d4a8c0ae7095f7afb26f6c3ee3ec3b7e3f990cf83b54cbcc203cfa296aade7", - "typescript": { - "status": "covered", + "path": "/api/{session}/contacts/blocked", + "operationId": "getBlocklist", + "fingerprint": "16fc9eaddef4c1a05ba9983961b120603b299cc85a1796fd25ca0f01e74ed39d", + "typescript": { + "status": "covered", + "method": "MessagingClient.contacts.blocklist" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/{session}/contacts/check", + "operationId": "checkContacts", + "fingerprint": "e9f7c2578adfb98d309750e55b902f1e833f4652fade0ef8060c5276eba9454e", + "typescript": { + "status": "covered", + "method": "MessagingClient.contacts.check" + } + }, + { + "family": "messaging", + "method": "GET", + "path": "/api/{session}/contacts/{contactId}", + "operationId": "getContact", + "fingerprint": "42d4a8c0ae7095f7afb26f6c3ee3ec3b7e3f990cf83b54cbcc203cfa296aade7", + "typescript": { + "status": "covered", "method": "MessagingClient.contacts.retrieve" } }, @@ -375,34 +610,23 @@ { "family": "messaging", "method": "GET", - "path": "/api/{session}/contacts/blocked", - "operationId": "getBlocklist", - "fingerprint": "16fc9eaddef4c1a05ba9983961b120603b299cc85a1796fd25ca0f01e74ed39d", - "typescript": { - "status": "covered", - "method": "MessagingClient.contacts.blocklist" - } - }, - { - "family": "messaging", - "method": "GET", - "path": "/api/{session}/contacts/check", - "operationId": "checkContacts", - "fingerprint": "e9f7c2578adfb98d309750e55b902f1e833f4652fade0ef8060c5276eba9454e", + "path": "/api/{session}/groups", + "operationId": "listGroups", + "fingerprint": "f83adc402119409f582d9b786b75ce922318436f93511d9fd70022dfd4356498", "typescript": { "status": "covered", - "method": "MessagingClient.contacts.check" + "method": "MessagingClient.groups.list" } }, { "family": "messaging", "method": "GET", - "path": "/api/{session}/groups", - "operationId": "listGroups", - "fingerprint": "f83adc402119409f582d9b786b75ce922318436f93511d9fd70022dfd4356498", + "path": "/api/{session}/groups/join-info", + "operationId": "getGroupJoinInfo", + "fingerprint": "489a00bd78b80ac77b4e49c0a1a55316f3c1633a2c96c22e71c9d9d40b2f4812", "typescript": { "status": "covered", - "method": "MessagingClient.groups.list" + "method": "MessagingClient.groups.getJoinInfo" } }, { @@ -438,17 +662,6 @@ "method": "MessagingClient.groups.listParticipants" } }, - { - "family": "messaging", - "method": "GET", - "path": "/api/{session}/groups/join-info", - "operationId": "getGroupJoinInfo", - "fingerprint": "489a00bd78b80ac77b4e49c0a1a55316f3c1633a2c96c22e71c9d9d40b2f4812", - "typescript": { - "status": "covered", - "method": "MessagingClient.groups.getJoinInfo" - } - }, { "family": "messaging", "method": "GET", @@ -562,9 +775,9 @@ { "family": "messaging", "method": "GET", - "path": "/api/info/status", - "operationId": "getStatus", - "fingerprint": "d718b1693efe722ebf046a8fe6525eccb179052320870a2ce40dbeb63e612f87", + "path": "/health", + "operationId": "getHealth", + "fingerprint": "05bab58b71b5692f804870852a686a9b4e140255899a6058b9aa284b5a0ed45c", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -574,9 +787,9 @@ { "family": "messaging", "method": "GET", - "path": "/api/info/version", - "operationId": "getVersion", - "fingerprint": "2ab7906884ffa07c978e9378490a193652ce1e1e960459908e23c4ab76838996", + "path": "/ping", + "operationId": "ping", + "fingerprint": "59aa902499566c7e84335b9f53e2ef306e5732bdf1228f03d571c03077c7b329", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -586,198 +799,220 @@ { "family": "messaging", "method": "GET", - "path": "/api/media/{id}", - "operationId": "downloadMedia", - "fingerprint": "60067d01771c53dba1a7ab71cc768f8bd2ca92cefd4cddbef887d0088a4395d9", + "path": "/v1/bridge/route", + "operationId": "getBridgeRoute", + "fingerprint": "c6e3c36cb20cdcf0c6c6991a495c676891c1e6d5f53ef83e754d4c9d66a2d801", "typescript": { - "status": "covered", - "method": "MessagingClient.media.download" + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" } }, { "family": "messaging", "method": "GET", - "path": "/api/media/{id}/info", - "operationId": "getMediaInfo", - "fingerprint": "8de9ff6257a2fed2ce72a97ec883dba64859cb938683ee12bda1c2bd95457fe5", + "path": "/widget/handoff/{token}/state", + "operationId": "getWidgetHandoffState", + "fingerprint": "98c32cc9d5796d5b40130bacadb9e8478b07ee9dd10c2cb39f1080f79ecd9123", "typescript": { - "status": "covered", - "method": "MessagingClient.media.retrieve" + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" } }, { "family": "messaging", - "method": "GET", - "path": "/api/operations/{operation}", - "operationId": "getOperation", - "fingerprint": "e13cfd45b2d6b3256c3fa832aca786242656ae6dc71d2d161ac4ab434d5e6688", + "method": "PATCH", + "path": "/api/projects/{projectSlug}/templates/{id}", + "operationId": "updateProjectTemplate", + "fingerprint": "e5a4d23a8452afb1149308858f5a27a26f5ab89ca2e2d6bb24a04bcf0f0c8b87", "typescript": { "status": "covered", - "method": "MessagingClient.operations.retrieve" + "method": "MessagingClient.templates.update" } }, { "family": "messaging", - "method": "GET", - "path": "/api/projects/{projectId}/observation-policy", - "operationId": "getProjectObservationPolicy", - "fingerprint": "d54ca3815c88eec4211dd5f89b6d55487de4a43b78be1d1511eb31db74053a9b", + "method": "PATCH", + "path": "/api/{session}/business/catalog/cart", + "operationId": "setBusinessCartEnabled", + "fingerprint": "c722b6d4949d08ee650dc17d1cb7d94795946f28b22174a989e4171077ff7b33", "typescript": { "status": "covered", - "method": "MessagingClient.observationPolicies.retrieveForProject" + "method": "MessagingClient.business.setCartEnabled" } }, { "family": "messaging", - "method": "GET", - "path": "/api/projects/{projectSlug}/campaigns", - "operationId": "listCampaigns", - "fingerprint": "edf002e4bf2e07dbe019805e1b0180fc9f4aab8ea58e1e93bc3bf9d8f9ec6f12", + "method": "PATCH", + "path": "/api/{session}/business/collections/{collectionId}", + "operationId": "updateBusinessCollection", + "fingerprint": "c3878a49a9451be4b0860062b86654f14d1e4b0dd523b6ddc3d02c101053b2a5", "typescript": { "status": "covered", - "method": "MessagingClient.campaigns.list" + "method": "MessagingClient.business.updateCollection" } }, { "family": "messaging", - "method": "GET", - "path": "/api/projects/{projectSlug}/campaigns/{id}", - "operationId": "getCampaign", - "fingerprint": "ae8e220be5c1aaac15befd796c43f4fcc3fd61e68b7c954193e73a0082a4eefb", + "method": "PATCH", + "path": "/api/{session}/business/products/{productId}/visibility", + "operationId": "setBusinessProductVisibility", + "fingerprint": "8f9718cdfc80ec66188438f872f5e972f9a89388c6b36e2c586522e001447090", "typescript": { "status": "covered", - "method": "MessagingClient.campaigns.retrieve" + "method": "MessagingClient.business.setProductVisibility" } }, { "family": "messaging", - "method": "GET", - "path": "/api/projects/{projectSlug}/campaigns/{id}/analytics", - "operationId": "getCampaignAnalytics", - "fingerprint": "07ed01febe6421b7722f0d3679463d787ed653d60d677744e4bbd5fde524b371", + "method": "PATCH", + "path": "/api/{session}/business/profile", + "operationId": "updateBusinessProfile", + "fingerprint": "bd9456ce05df5424f49b0b70bc9d263a4539a121c72169e5885101cab96101b6", "typescript": { "status": "covered", - "method": "MessagingClient.campaigns.analytics" + "method": "MessagingClient.business.updateProfile" } }, { "family": "messaging", - "method": "GET", - "path": "/api/projects/{projectSlug}/templates", - "operationId": "listProjectTemplates", - "fingerprint": "d9c2a54361638c27c36cfa0c9c2f5d92daee3ee47f5ae7c7f0b1c4d316b22360", + "method": "POST", + "path": "/api/client-tokens", + "operationId": "mintClientToken", + "fingerprint": "26489877b93f4b1b7ef2845f195489e5baf5400f5c5ba5a01e18ba6701146f26", "typescript": { "status": "covered", - "method": "MessagingClient.templates.list" + "method": "MessagingClient.clientTokens.mint" } }, { "family": "messaging", - "method": "GET", - "path": "/api/projects/{projectSlug}/templates/{id}", - "operationId": "getProjectTemplate", - "fingerprint": "cb60900192a486c16562867a1cfb3b595444a467b7d91b43ea7aa107faf951c1", + "method": "POST", + "path": "/api/media/{id}/download-and-save", + "operationId": "downloadAndSaveMedia", + "fingerprint": "16cf1251cfb5957845ed98a794f97c08065e0f4eb11ee76b5884276b4291945e", "typescript": { "status": "covered", - "method": "MessagingClient.templates.retrieve" + "method": "MessagingClient.media.persist" } }, { "family": "messaging", - "method": "GET", - "path": "/api/sessions", - "operationId": "listSessions", - "fingerprint": "d97bd519e00e186ad88bdc449f6a5138b2c384fa31b1f19bf5d27d3c5fbe5c65", + "method": "POST", + "path": "/api/projects/{projectSlug}/campaigns", + "operationId": "createCampaign", + "fingerprint": "0635c38e1ebb410295a6f75fba03e7cc74f21f2d97c58590215104b2efc89ca2", "typescript": { "status": "covered", - "method": "MessagingClient.sessions.list" + "method": "MessagingClient.campaigns.create" } }, { "family": "messaging", - "method": "GET", - "path": "/api/sessions/{session}", - "operationId": "getSession", - "fingerprint": "44932148368b9db333d13748336ea655e2405af69a10e69c542d3e5e5d85250c", + "method": "POST", + "path": "/api/projects/{projectSlug}/campaigns/{id}/launch", + "operationId": "launchCampaign", + "fingerprint": "a44bb8dd674e63cbc77bf3762a770543b3b0b3d9d591ed8fd6a59606dd6e8311", "typescript": { "status": "covered", - "method": "MessagingClient.sessions.retrieve" + "method": "MessagingClient.campaigns.launch" } }, { "family": "messaging", - "method": "GET", - "path": "/api/sessions/{session}/client-rules", - "operationId": "getClientRules", - "fingerprint": "f878fc588bcf2c51faa82d6c1d7e2bf2a3765c01affd9513a7b0004b07e3b5ec", + "method": "POST", + "path": "/api/projects/{projectSlug}/campaigns/{id}/pause", + "operationId": "pauseCampaign", + "fingerprint": "fc1b49e0f1211bead63f6a8d5a54d75127f7ad96ee998dcda9262cfc5c48c42b", "typescript": { "status": "covered", - "method": "MessagingClient.clientTokens.retrieveRules" + "method": "MessagingClient.campaigns.pause" } }, { "family": "messaging", - "method": "GET", - "path": "/api/sessions/{session}/me", - "operationId": "getSessionAccount", - "fingerprint": "b989986752df43ace50259ecd84b655ad9b01e504a46c9b2b449a3c6e23adc00", + "method": "POST", + "path": "/api/projects/{projectSlug}/campaigns/{id}/requeue", + "operationId": "requeueCampaign", + "fingerprint": "c2c71270f6a810c29b46c8f24931538b33440b17a35e50c6460c5e0c9f1f44d3", "typescript": { "status": "covered", - "method": "MessagingClient.sessions.account" + "method": "MessagingClient.campaigns.requeue" } }, { "family": "messaging", - "method": "GET", - "path": "/api/webhooks", - "operationId": "listWebhooks", - "fingerprint": "8bda5679f018323e4fb249df16a541ae9ebe812121f7c64974d4665d3d407eac", + "method": "POST", + "path": "/api/projects/{projectSlug}/campaigns/{id}/resume", + "operationId": "resumeCampaign", + "fingerprint": "1e20868be51fe11638ae6a4f764b7c49777bf380c7b7db0689870a3b20f9b185", "typescript": { "status": "covered", - "method": "MessagingClient.webhooks.list" + "method": "MessagingClient.campaigns.resume" } }, { "family": "messaging", - "method": "GET", - "path": "/api/webhooks/{id}", - "operationId": "getWebhook", - "fingerprint": "08a27c707cd6c742eff86a527ef3ef399fab813a117d49ae1e6c1911f49a4245", + "method": "POST", + "path": "/api/projects/{projectSlug}/campaigns/{id}/stop", + "operationId": "stopCampaign", + "fingerprint": "91b60609041f8f81ae075541f79ef03b762956444b45f064ca08b6d6cc214e47", "typescript": { "status": "covered", - "method": "MessagingClient.webhooks.retrieve" + "method": "MessagingClient.campaigns.stop" } }, { "family": "messaging", - "method": "GET", - "path": "/health", - "operationId": "getHealth", - "fingerprint": "05bab58b71b5692f804870852a686a9b4e140255899a6058b9aa284b5a0ed45c", + "method": "POST", + "path": "/api/projects/{projectSlug}/templates", + "operationId": "createProjectTemplate", + "fingerprint": "8db3ee69f49508f8c359cc55fd8e0cdf4c5217e408c5d792b55366c7e61a5325", "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" + "status": "covered", + "method": "MessagingClient.templates.create" } }, { "family": "messaging", - "method": "GET", - "path": "/ping", - "operationId": "ping", - "fingerprint": "59aa902499566c7e84335b9f53e2ef306e5732bdf1228f03d571c03077c7b329", + "method": "POST", + "path": "/api/projects/{projectSlug}/templates/{id}/preview", + "operationId": "previewProjectTemplate", + "fingerprint": "4182fbd31c05d34d43b537ba52ba3f8afca01d023ff5e716cd4b15c0402cd6d9", "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" + "status": "covered", + "method": "MessagingClient.templates.preview" } }, { "family": "messaging", - "method": "GET", - "path": "/v1/bridge/route", - "operationId": "getBridgeRoute", - "fingerprint": "c6e3c36cb20cdcf0c6c6991a495c676891c1e6d5f53ef83e754d4c9d66a2d801", + "method": "POST", + "path": "/api/projects/{projectSlug}/templates/{id}/submit", + "operationId": "submitProjectTemplateToMeta", + "fingerprint": "d98badffd8b1588ab3d9fe57698e18c940676a690a0339900928fff52427ff4f", + "typescript": { + "status": "covered", + "method": "MessagingClient.templates.submit" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/sessions", + "operationId": "createSession", + "fingerprint": "8e1ae014f98f524c3d8f89b0cf5ec8c674094ee7e01243251d220a139cdc92cb", + "typescript": { + "status": "covered", + "method": "MessagingClient.sessions.create" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/sessions/{session}/handoff-ack", + "operationId": "sessionHandoffAck", + "fingerprint": "24eb38203f6a285b118bc82facb5351a42fe70243ef790cef65be01fd6d71139", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -786,10 +1021,10 @@ }, { "family": "messaging", - "method": "GET", - "path": "/widget/handoff/{token}/state", - "operationId": "getWidgetHandoffState", - "fingerprint": "98c32cc9d5796d5b40130bacadb9e8478b07ee9dd10c2cb39f1080f79ecd9123", + "method": "POST", + "path": "/api/sessions/{session}/handoff-import", + "operationId": "sessionHandoffImport", + "fingerprint": "8912116b0231eb1a64c7d45aedb45aa11c9251c8fed13a0db8e7ca936a4ba5bf", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -798,57 +1033,172 @@ }, { "family": "messaging", - "method": "PATCH", - "path": "/api/{session}/business/catalog/cart", - "operationId": "setBusinessCartEnabled", - "fingerprint": "c722b6d4949d08ee650dc17d1cb7d94795946f28b22174a989e4171077ff7b33", + "method": "POST", + "path": "/api/sessions/{session}/handoff-keys", + "operationId": "sessionHandoffKeys", + "fingerprint": "a528ac23c017ee7e74576d55862bd80793b4bbecf68bb1e353d38b7a3f647fc7", + "typescript": { + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/sessions/{session}/logout", + "operationId": "logoutSession", + "fingerprint": "9e0a95cbaa8d0683ee4a6a6e698a6916d1024a88adb6aad4d69ff44fc78ca7c1", "typescript": { "status": "covered", - "method": "MessagingClient.business.setCartEnabled" + "method": "MessagingClient.sessions.logout" } }, { "family": "messaging", - "method": "PATCH", - "path": "/api/{session}/business/collections/{collectionId}", - "operationId": "updateBusinessCollection", - "fingerprint": "5d819b79b3cfd24695ea47ba4077386a0202fe4756c93e0d597059355393fa9f", + "method": "POST", + "path": "/api/sessions/{session}/restart", + "operationId": "restartSession", + "fingerprint": "0d146d7ee529b38fe399ee5fa5379cf3671fcaf1ef18ffe1e198c80b51f5b32e", "typescript": { "status": "covered", - "method": "MessagingClient.business.updateCollection" + "method": "MessagingClient.sessions.restart" } }, { "family": "messaging", - "method": "PATCH", - "path": "/api/{session}/business/products/{productId}/visibility", - "operationId": "setBusinessProductVisibility", - "fingerprint": "0597624ac4fdfc718209b28cc895e607e07c2f35013274097e0bc4cd9611afce", + "method": "POST", + "path": "/api/sessions/{session}/start", + "operationId": "startSession", + "fingerprint": "e802cd6c357a379fffb7349e5eef0fbdcb532b3696bef07be941362025d3962c", "typescript": { "status": "covered", - "method": "MessagingClient.business.setProductVisibility" + "method": "MessagingClient.sessions.start" } }, { "family": "messaging", - "method": "PATCH", - "path": "/api/{session}/business/profile", - "operationId": "updateBusinessProfile", - "fingerprint": "bd9456ce05df5424f49b0b70bc9d263a4539a121c72169e5885101cab96101b6", + "method": "POST", + "path": "/api/sessions/{session}/stop", + "operationId": "stopSession", + "fingerprint": "3053c511919b8c23fe9bbaee3c31f60c8c1dd818540db6b67857a1979579d68f", "typescript": { "status": "covered", - "method": "MessagingClient.business.updateProfile" + "method": "MessagingClient.sessions.stop" } }, { "family": "messaging", - "method": "PATCH", - "path": "/api/projects/{projectSlug}/templates/{id}", - "operationId": "updateProjectTemplate", - "fingerprint": "e5a4d23a8452afb1149308858f5a27a26f5ab89ca2e2d6bb24a04bcf0f0c8b87", + "method": "POST", + "path": "/api/sessions/{session}/upload-appstate-keys", + "operationId": "sessionUploadAppStateKeys", + "fingerprint": "dcfe439c4855dd14296883928f3fa9a3b0d9b1a2e75394c361880f3dc7dd5af2", + "typescript": { + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/voip/calls/{id}/candidate", + "operationId": "voipCandidate", + "fingerprint": "73dc885b8d7b9df92ab9943e063db75226f45fa91850990a81fea9ff8b36601c", + "typescript": { + "status": "excluded", + "reason": "Browser client-token signaling route served by @polymorfa/browser (CallsSignalingClient over BrowserTransport); it does not accept the server credential used by the TypeScript server SDK.", + "milestone": "not-server-sdk" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/voip/calls/{id}/offer", + "operationId": "voipOffer", + "fingerprint": "da0f2c6c876d767bf8b9d3edebeeac741dab4859157abd4634837e7a009df7bc", + "typescript": { + "status": "excluded", + "reason": "Browser client-token signaling route served by @polymorfa/browser (CallsSignalingClient over BrowserTransport); it does not accept the server credential used by the TypeScript server SDK.", + "milestone": "not-server-sdk" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/voip/token", + "operationId": "voipToken", + "fingerprint": "a31e115dd33ba85dff0897cfdcd24b148e8e1b95273a83ff8b86efafc3120d34", "typescript": { "status": "covered", - "method": "MessagingClient.templates.update" + "method": "MessagingClient.voip.token" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/voip/calls/{id}/renegotiate", + "operationId": "voipRenegotiate", + "fingerprint": "384dfa69325054306bfb5c4708fc4dc8e92e9800d9adfccdebc03801173efbae", + "typescript": { + "status": "excluded", + "reason": "Browser client-token signaling route served by @polymorfa/browser (CallsSignalingClient.renegotiate over BrowserTransport); it does not accept the server credential used by the TypeScript server SDK." + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/voip/ws-ticket", + "operationId": "voipSocketTicket", + "fingerprint": "b95af3bef3974c271b26d0fef1439f1ccb9b0df45db50512399eef06f52dd0fe", + "typescript": { + "status": "covered", + "method": "MessagingClient.voip.socketTicket" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/voip/calls/{id}/agent-token", + "operationId": "voipAgentToken", + "fingerprint": "03d4c5cd14a2c755d10ffb96fc7cbeee43b8ccfd7635d5922fdd0a5087d196a3", + "typescript": { + "status": "covered", + "method": "MessagingClient.voip.agentToken" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/webhooks", + "operationId": "createWebhook", + "fingerprint": "7c827e91f436da0396a50e609d52b4ec85cec7afdc577234e29cad05d04b8b28", + "typescript": { + "status": "covered", + "method": "MessagingClient.webhooks.create" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/widget/sessions", + "operationId": "createWidgetSession", + "fingerprint": "9778c1bcc5fa875a6e012bdc04551009ba06bf8039436e9d11edf646b94bcb86", + "typescript": { + "status": "missing", + "reason": "No handwritten TypeScript resource method exists for this operation yet.", + "milestone": "typescript-parity" + } + }, + { + "family": "messaging", + "method": "POST", + "path": "/api/widget/sessions/{id}/handoff", + "operationId": "createWidgetHandoff", + "fingerprint": "2b6bc4390e3f157b0e425e3eb71521dc9c007accd1211fc10edc03a9c858aefa", + "typescript": { + "status": "covered", + "method": "BrowserMessagingClient.widget.handoff" } }, { @@ -876,23 +1226,23 @@ { "family": "messaging", "method": "POST", - "path": "/api/{session}/business/collections/{collectionId}/appeal", - "operationId": "appealBusinessCollection", - "fingerprint": "0ae828304df8b8592f54cd2b97b44bad989298f8e3387c720c014b0e390fb571", + "path": "/api/{session}/business/collections/reorder", + "operationId": "reorderBusinessCollections", + "fingerprint": "cf299c93ea096eb0155584f2edca82ac5eb4054eecaba5246c36f6a3234779f8", "typescript": { "status": "covered", - "method": "MessagingClient.business.appealCollection" + "method": "MessagingClient.business.reorderCollections" } }, { "family": "messaging", "method": "POST", - "path": "/api/{session}/business/collections/reorder", - "operationId": "reorderBusinessCollections", - "fingerprint": "cf299c93ea096eb0155584f2edca82ac5eb4054eecaba5246c36f6a3234779f8", + "path": "/api/{session}/business/collections/{collectionId}/appeal", + "operationId": "appealBusinessCollection", + "fingerprint": "54a936a7aa8899cd76528e92fdae5e0febb67a316faa31c109ad2064916b84f6", "typescript": { "status": "covered", - "method": "MessagingClient.business.reorderCollections" + "method": "MessagingClient.business.appealCollection" } }, { @@ -922,7 +1272,7 @@ "method": "POST", "path": "/api/{session}/business/products/{productId}/appeal", "operationId": "appealBusinessProduct", - "fingerprint": "b15415e4c99cb659acc5307f9826868f4c897fef0e7aa35240671e72d170f40e", + "fingerprint": "781d6d5d39cdbd196ecd55309a01eaaacb0f6abb42843679a65551db1873ba81", "typescript": { "status": "covered", "method": "MessagingClient.business.appealProduct" @@ -1093,6 +1443,17 @@ "method": "MessagingClient.groups.create" } }, + { + "family": "messaging", + "method": "POST", + "path": "/api/{session}/groups/join", + "operationId": "joinGroup", + "fingerprint": "7a18005ac220467333a78caf77de1bb7f248f01d8a88e79c79d7ee58ce71e22d", + "typescript": { + "status": "covered", + "method": "MessagingClient.groups.join" + } + }, { "family": "messaging", "method": "POST", @@ -1159,17 +1520,6 @@ "method": "MessagingClient.groups.removeParticipants" } }, - { - "family": "messaging", - "method": "POST", - "path": "/api/{session}/groups/join", - "operationId": "joinGroup", - "fingerprint": "7a18005ac220467333a78caf77de1bb7f248f01d8a88e79c79d7ee58ce71e22d", - "typescript": { - "status": "covered", - "method": "MessagingClient.groups.join" - } - }, { "family": "messaging", "method": "POST", @@ -1271,260 +1621,46 @@ }, { "family": "messaging", - "method": "POST", - "path": "/api/client-tokens", - "operationId": "mintClientToken", - "fingerprint": "26489877b93f4b1b7ef2845f195489e5baf5400f5c5ba5a01e18ba6701146f26", - "typescript": { - "status": "covered", - "method": "MessagingClient.clientTokens.mint" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/media/{id}/download-and-save", - "operationId": "downloadAndSaveMedia", - "fingerprint": "16cf1251cfb5957845ed98a794f97c08065e0f4eb11ee76b5884276b4291945e", - "typescript": { - "status": "covered", - "method": "MessagingClient.media.persist" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/campaigns", - "operationId": "createCampaign", - "fingerprint": "0635c38e1ebb410295a6f75fba03e7cc74f21f2d97c58590215104b2efc89ca2", - "typescript": { - "status": "covered", - "method": "MessagingClient.campaigns.create" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/campaigns/{id}/launch", - "operationId": "launchCampaign", - "fingerprint": "a44bb8dd674e63cbc77bf3762a770543b3b0b3d9d591ed8fd6a59606dd6e8311", - "typescript": { - "status": "covered", - "method": "MessagingClient.campaigns.launch" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/campaigns/{id}/pause", - "operationId": "pauseCampaign", - "fingerprint": "fc1b49e0f1211bead63f6a8d5a54d75127f7ad96ee998dcda9262cfc5c48c42b", - "typescript": { - "status": "covered", - "method": "MessagingClient.campaigns.pause" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/campaigns/{id}/requeue", - "operationId": "requeueCampaign", - "fingerprint": "c2c71270f6a810c29b46c8f24931538b33440b17a35e50c6460c5e0c9f1f44d3", - "typescript": { - "status": "covered", - "method": "MessagingClient.campaigns.requeue" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/campaigns/{id}/resume", - "operationId": "resumeCampaign", - "fingerprint": "1e20868be51fe11638ae6a4f764b7c49777bf380c7b7db0689870a3b20f9b185", - "typescript": { - "status": "covered", - "method": "MessagingClient.campaigns.resume" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/campaigns/{id}/stop", - "operationId": "stopCampaign", - "fingerprint": "91b60609041f8f81ae075541f79ef03b762956444b45f064ca08b6d6cc214e47", - "typescript": { - "status": "covered", - "method": "MessagingClient.campaigns.stop" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/templates", - "operationId": "createProjectTemplate", - "fingerprint": "8db3ee69f49508f8c359cc55fd8e0cdf4c5217e408c5d792b55366c7e61a5325", - "typescript": { - "status": "covered", - "method": "MessagingClient.templates.create" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/templates/{id}/preview", - "operationId": "previewProjectTemplate", - "fingerprint": "4182fbd31c05d34d43b537ba52ba3f8afca01d023ff5e716cd4b15c0402cd6d9", - "typescript": { - "status": "covered", - "method": "MessagingClient.templates.preview" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/projects/{projectSlug}/templates/{id}/submit", - "operationId": "submitProjectTemplateToMeta", - "fingerprint": "d98badffd8b1588ab3d9fe57698e18c940676a690a0339900928fff52427ff4f", - "typescript": { - "status": "covered", - "method": "MessagingClient.templates.submit" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions", - "operationId": "createSession", - "fingerprint": "8e1ae014f98f524c3d8f89b0cf5ec8c674094ee7e01243251d220a139cdc92cb", - "typescript": { - "status": "covered", - "method": "MessagingClient.sessions.create" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/handoff-ack", - "operationId": "sessionHandoffAck", - "fingerprint": "24eb38203f6a285b118bc82facb5351a42fe70243ef790cef65be01fd6d71139", - "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/handoff-import", - "operationId": "sessionHandoffImport", - "fingerprint": "8912116b0231eb1a64c7d45aedb45aa11c9251c8fed13a0db8e7ca936a4ba5bf", - "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/handoff-keys", - "operationId": "sessionHandoffKeys", - "fingerprint": "a528ac23c017ee7e74576d55862bd80793b4bbecf68bb1e353d38b7a3f647fc7", - "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/logout", - "operationId": "logoutSession", - "fingerprint": "9e0a95cbaa8d0683ee4a6a6e698a6916d1024a88adb6aad4d69ff44fc78ca7c1", - "typescript": { - "status": "covered", - "method": "MessagingClient.sessions.logout" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/restart", - "operationId": "restartSession", - "fingerprint": "0d146d7ee529b38fe399ee5fa5379cf3671fcaf1ef18ffe1e198c80b51f5b32e", - "typescript": { - "status": "covered", - "method": "MessagingClient.sessions.restart" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/start", - "operationId": "startSession", - "fingerprint": "e802cd6c357a379fffb7349e5eef0fbdcb532b3696bef07be941362025d3962c", - "typescript": { - "status": "covered", - "method": "MessagingClient.sessions.start" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/stop", - "operationId": "stopSession", - "fingerprint": "3053c511919b8c23fe9bbaee3c31f60c8c1dd818540db6b67857a1979579d68f", + "method": "PUT", + "path": "/api/projects/{projectId}/observation-policy", + "operationId": "updateProjectObservationPolicy", + "fingerprint": "3058a9a4792b51a9430c873ad27154ee7682143669fd1981276697a1420189d1", "typescript": { "status": "covered", - "method": "MessagingClient.sessions.stop" - } - }, - { - "family": "messaging", - "method": "POST", - "path": "/api/sessions/{session}/upload-appstate-keys", - "operationId": "sessionUploadAppStateKeys", - "fingerprint": "dcfe439c4855dd14296883928f3fa9a3b0d9b1a2e75394c361880f3dc7dd5af2", - "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" + "method": "MessagingClient.observationPolicies.updateForProject" } }, { "family": "messaging", - "method": "POST", - "path": "/api/webhooks", - "operationId": "createWebhook", - "fingerprint": "7c827e91f436da0396a50e609d52b4ec85cec7afdc577234e29cad05d04b8b28", + "method": "PUT", + "path": "/api/sessions/{session}", + "operationId": "updateSession", + "fingerprint": "e3c595947049d4f125135e8bcf4902f7632164efd63a14dae5e60e7dbd0d8a7e", "typescript": { "status": "covered", - "method": "MessagingClient.webhooks.create" + "method": "MessagingClient.sessions.update" } }, { "family": "messaging", - "method": "POST", - "path": "/api/widget/sessions", - "operationId": "createWidgetSession", - "fingerprint": "9778c1bcc5fa875a6e012bdc04551009ba06bf8039436e9d11edf646b94bcb86", + "method": "PUT", + "path": "/api/sessions/{session}/client-rules", + "operationId": "setClientRules", + "fingerprint": "322a2571256b5bd75b030eeffbfd6b69664530debe3e644685bb879657e431b3", "typescript": { - "status": "missing", - "reason": "No handwritten TypeScript resource method exists for this operation yet.", - "milestone": "typescript-parity" + "status": "covered", + "method": "MessagingClient.clientTokens.updateRules" } }, { "family": "messaging", - "method": "POST", - "path": "/api/widget/sessions/{id}/handoff", - "operationId": "createWidgetHandoff", - "fingerprint": "2b6bc4390e3f157b0e425e3eb71521dc9c007accd1211fc10edc03a9c858aefa", + "method": "PUT", + "path": "/api/webhooks/{id}", + "operationId": "updateWebhook", + "fingerprint": "c6e5866c9bc1bf2174b159d4d9c8e081a20d4afbb5d8d89cc2f9a2354def2ee3", "typescript": { "status": "covered", - "method": "BrowserMessagingClient.widget.handoff" + "method": "MessagingClient.webhooks.update" } }, { @@ -1543,7 +1679,7 @@ "method": "PUT", "path": "/api/{session}/business/products/{productId}", "operationId": "updateBusinessProduct", - "fingerprint": "56b1ab82dd263be59350de2f2099bfa898d2e1176fcf46c8bf4d7eb12d6cdc04", + "fingerprint": "07a34ccaac8c0e68d4c16587cc61f1fe0042342467967eceb7e9cf7bc0c4936a", "typescript": { "status": "covered", "method": "MessagingClient.business.updateProduct" @@ -1673,23 +1809,23 @@ { "family": "messaging", "method": "PUT", - "path": "/api/{session}/labels/{labelId}", - "operationId": "updateLabel", - "fingerprint": "16576f2d9f30772d1ced87422362deaf496218793989a3b0c9f7954d9b11be3d", + "path": "/api/{session}/labels/chats/{chatId}", + "operationId": "setChatLabels", + "fingerprint": "dc3e0855fa95ecf2ce2887571a09e092a0e6fc6cfe723cdf1776026b5d9adffa", "typescript": { "status": "covered", - "method": "MessagingClient.labels.update" + "method": "MessagingClient.labels.replaceForChat" } }, { "family": "messaging", "method": "PUT", - "path": "/api/{session}/labels/chats/{chatId}", - "operationId": "setChatLabels", - "fingerprint": "dc3e0855fa95ecf2ce2887571a09e092a0e6fc6cfe723cdf1776026b5d9adffa", + "path": "/api/{session}/labels/{labelId}", + "operationId": "updateLabel", + "fingerprint": "16576f2d9f30772d1ced87422362deaf496218793989a3b0c9f7954d9b11be3d", "typescript": { "status": "covered", - "method": "MessagingClient.labels.replaceForChat" + "method": "MessagingClient.labels.update" } }, { @@ -1697,7 +1833,7 @@ "method": "PUT", "path": "/api/{session}/observation-policy", "operationId": "updateSessionObservationPolicy", - "fingerprint": "a18bae02ded6d67ad83b199f8afd280fc48d5ce8ae9b8a50640a63e4c5909dac", + "fingerprint": "020414a95992d31d432b8bafa80af8fe21a8eb5f502ef8e5c3706792e9d6359d", "typescript": { "status": "covered", "method": "MessagingClient.observationPolicies.updateForSession" @@ -1706,23 +1842,23 @@ { "family": "messaging", "method": "PUT", - "path": "/api/{session}/privacy/{setting}", - "operationId": "setPrivacySetting", - "fingerprint": "ca1f4d9feff32014c99af243dde71b41fd7ea6da4fc0dd889657d75a6f99f0d2", + "path": "/api/{session}/privacy/disappearing/default", + "operationId": "setDefaultDisappearingTimer", + "fingerprint": "eb74a53d1f5b2873e3da588aadf68e9563c3939899e246fabf066e117dbbc3fd", "typescript": { "status": "covered", - "method": "MessagingClient.privacy.set" + "method": "MessagingClient.privacy.setDefaultDisappearingTimer" } }, { "family": "messaging", "method": "PUT", - "path": "/api/{session}/privacy/disappearing/default", - "operationId": "setDefaultDisappearingTimer", - "fingerprint": "eb74a53d1f5b2873e3da588aadf68e9563c3939899e246fabf066e117dbbc3fd", + "path": "/api/{session}/privacy/{setting}", + "operationId": "setPrivacySetting", + "fingerprint": "ca1f4d9feff32014c99af243dde71b41fd7ea6da4fc0dd889657d75a6f99f0d2", "typescript": { "status": "covered", - "method": "MessagingClient.privacy.setDefaultDisappearingTimer" + "method": "MessagingClient.privacy.set" } }, { @@ -1758,56 +1894,12 @@ "method": "MessagingClient.profile.setStatus" } }, - { - "family": "messaging", - "method": "PUT", - "path": "/api/projects/{projectId}/observation-policy", - "operationId": "updateProjectObservationPolicy", - "fingerprint": "ccb277848728d1b1de46ab4a1def9ada9a92d2dd20710f2040fb2fd5c7945505", - "typescript": { - "status": "covered", - "method": "MessagingClient.observationPolicies.updateForProject" - } - }, - { - "family": "messaging", - "method": "PUT", - "path": "/api/sessions/{session}", - "operationId": "updateSession", - "fingerprint": "e3c595947049d4f125135e8bcf4902f7632164efd63a14dae5e60e7dbd0d8a7e", - "typescript": { - "status": "covered", - "method": "MessagingClient.sessions.update" - } - }, - { - "family": "messaging", - "method": "PUT", - "path": "/api/sessions/{session}/client-rules", - "operationId": "setClientRules", - "fingerprint": "322a2571256b5bd75b030eeffbfd6b69664530debe3e644685bb879657e431b3", - "typescript": { - "status": "covered", - "method": "MessagingClient.clientTokens.updateRules" - } - }, - { - "family": "messaging", - "method": "PUT", - "path": "/api/webhooks/{id}", - "operationId": "updateWebhook", - "fingerprint": "c6e5866c9bc1bf2174b159d4d9c8e081a20d4afbb5d8d89cc2f9a2354def2ee3", - "typescript": { - "status": "covered", - "method": "MessagingClient.webhooks.update" - } - }, { "family": "platform", "method": "DELETE", "path": "/admin/keys/{keyId}", "operationId": "adminRevokeApiKeyApiKey", - "fingerprint": "6ecb2072e1669b07c1242db63f9da1184f529aaf14c663a9142d0ad76d4939f3", + "fingerprint": "a4ad0cb9129d422eb4da110b5c2fe93d0ee437cf7b8878f8afd1b1e1bf0c4736", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -1819,7 +1911,7 @@ "method": "DELETE", "path": "/admin/members/{memberId}", "operationId": "adminRemoveMember", - "fingerprint": "671621e00f4f42acac0bd89087979abfd1a093f802754cc99e5e513118dc4bb4", + "fingerprint": "75d3e00264714ff72c0e528d36e599ff734d6ac84f24a90eeedd353b71e38f41", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -1831,7 +1923,7 @@ "method": "DELETE", "path": "/admin/organization-invitations/{invitationId}", "operationId": "adminRevokeOrganizationInvitation", - "fingerprint": "1e2c41320db02996362e67f1a7dcec06f64f20b4fcb137aa087be0b8450fc57f", + "fingerprint": "fc969ae102b8fa7b5a9cffce1db7cc909764e6937ab248a690ee51d283b22cbf", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -1843,7 +1935,7 @@ "method": "DELETE", "path": "/admin/orgs/{orgId}", "operationId": "adminCascadeDeleteOrganization", - "fingerprint": "27c4f0f969facecba409df8b21f7fb4c58a31a5afba97f8000b468a72e3c73b3", + "fingerprint": "4e099164b6c7541cec09a27f50ae82c89cfa860f24d4c7ba23c0e0331df3c98e", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -1855,7 +1947,7 @@ "method": "DELETE", "path": "/admin/sessions/{sessionId}", "operationId": "adminForceDeleteSession", - "fingerprint": "9c06026466372625c3c92c4ed35acee75b313fc759f14915eb829df652f40ab9", + "fingerprint": "85c5c1a65cc2541606d9eaf2d12843af119952e4e508fae77116bd201a8ad8c6", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -1867,7 +1959,7 @@ "method": "DELETE", "path": "/admin/users/{userId}/onboarding", "operationId": "adminResetOnboardingUser", - "fingerprint": "5574da77ee0d5e6803909ef96ca489ec7a0af0609a4bf3eb0489e79fb51b0987", + "fingerprint": "87e04025c3c6ce6859b53da46e3493d43ede6a17d776b1b24eba3618cd8e1b37", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -1879,7 +1971,7 @@ "method": "DELETE", "path": "/console/auth/factors/{factorId}", "operationId": "deleteUserProfileAuthFactor", - "fingerprint": "8ba5e5d34e89462bddae8ea83a658d08191c03ada30926dc6ac4bac3b29dd492", + "fingerprint": "6e937614ebb76135490ae7d51a08b626d0c565b07b85457f48299f27abea6816", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -1891,7 +1983,7 @@ "method": "DELETE", "path": "/console/auth/providers/{provider}", "operationId": "disconnectProviderUserProfile", - "fingerprint": "98178b17c187c2d31e3405a33aa2dad63ff979d66eab7875a1744e55338a4b8e", + "fingerprint": "643b279e672d065100b9fe985db8e7dfadaba03ae79b27f1ae7078bc553e59d7", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -1903,7 +1995,7 @@ "method": "DELETE", "path": "/console/organization/avatar", "operationId": "removeAvatarOrganization", - "fingerprint": "3b33524ffebb6d5fc5e4861fb5c42427777bc0cd039164cc1830adb1c7909140", + "fingerprint": "9b0a296f693b41ab9eabc5fb1f6edea49997191478e07198a6519f71defc9471", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -1915,7 +2007,7 @@ "method": "DELETE", "path": "/console/profile/avatar", "operationId": "removeAvatarUserProfile", - "fingerprint": "7aac49acf2b8a1422ef1382881315700423b09015237893c144545a15afaf8a1", + "fingerprint": "3b44f283f23d6f6e664450772a0be238b1d84c6987040785e45647fa31380ee1", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -1927,7 +2019,7 @@ "method": "DELETE", "path": "/console/webhooks/{webhookId}", "operationId": "deleteWebhook", - "fingerprint": "9267ae6441a3d7468077889a42382ad3b551ce9b4d63f59909c0859b6bb01d2e", + "fingerprint": "423b57d03ced57221900eee9a192b8111d54f567c11503ba721c5d7c315dd429", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -1939,7 +2031,7 @@ "method": "DELETE", "path": "/console/widget/logo", "operationId": "removeLogoWidgetSettings", - "fingerprint": "59087b96550978cf36ebd54e34066d9c74a2aac552315c136730b98ece441c4e", + "fingerprint": "08bff65e413b2751c0cfe0e93e75ea55cca7875fe91d9a9af85a30fa04d2abfd", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -1951,7 +2043,7 @@ "method": "DELETE", "path": "/v1/audiences/{listId}", "operationId": "deleteAudience", - "fingerprint": "7a3fa2bd90c018fa34736074baced7b9d42dfc7956317ae09a9644bf48d2f301", + "fingerprint": "89db8d46b387f9b7ea94363f3205e01abdc11a81250bd4029e78b0d54dea5726", "typescript": { "status": "covered", "method": "PlatformClient.audiences.delete" @@ -1962,7 +2054,7 @@ "method": "DELETE", "path": "/v1/campaigns/{campaignId}", "operationId": "deleteCampaign", - "fingerprint": "fb5cef90dbfa756213c870fd1ef4461b6c8e062da069cf88b3830e5e71e31413", + "fingerprint": "6e2e2556a22b8f5110eefed455820a0be2a58ed7ca60392ef3b865eef2b073a7", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.delete" @@ -1984,7 +2076,7 @@ "method": "DELETE", "path": "/v1/flows/{flowId}", "operationId": "deleteFlow", - "fingerprint": "9efdd6ef0c41a42ad17c60d85dcd07bf6e97067f3d27bb959a5d1c9ad9df191b", + "fingerprint": "75623976db5350942aef9181408d6d7a3264013ba9c34e37abd1288a671d9f3c", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", @@ -1996,7 +2088,7 @@ "method": "DELETE", "path": "/v1/keys/{keyId}", "operationId": "deactivateApiKey", - "fingerprint": "fbb9ab38051753fe81ee74632b9e4bc98b1d2c8bc0702dd9decc280a07ea77d4", + "fingerprint": "ad299f8afd661838296ea39fce5b41a50c678a6e5aaf8425625815fa1d4885a7", "typescript": { "status": "covered", "method": "PlatformClient.apiKeys.deactivate" @@ -2007,7 +2099,7 @@ "method": "DELETE", "path": "/v1/media/{mediaId}", "operationId": "deleteMedia", - "fingerprint": "bc8a28796934144374417e53862df9309dc502753312658570d6e08b68a810c3", + "fingerprint": "21733c4f3e2c6fa9c0fdb7dc40e32cdd27e22a282b3fa5708b1b008684a301b1", "typescript": { "status": "covered", "method": "PlatformClient.media.delete" @@ -2018,7 +2110,7 @@ "method": "DELETE", "path": "/v1/members/{memberId}", "operationId": "deleteMember", - "fingerprint": "df5037db2a58bed2a74ff8609a89333a4e01d02351a0729d5a87dece5fa85134", + "fingerprint": "88c312dbdc2b8ef1d666704ca7e94d457611c4c6e66ff54d1ea2331baa8e0e55", "typescript": { "status": "excluded", "reason": "This dashboard-only member mutation accepts ConsoleSession authentication and rejects the organization server API key used by PlatformClient.", @@ -2030,18 +2122,30 @@ "method": "DELETE", "path": "/v1/optouts/{phone}", "operationId": "deleteOptOut", - "fingerprint": "e5a79337a1d32509cf310a55213fe99c46000c3d36c9691ec655ffe9c18ac216", + "fingerprint": "1107ac53c52a4b3bf8f517974d437a03abc8b947ba50b47a90b5e308a8907ec9", "typescript": { "status": "covered", "method": "PlatformClient.optOuts.delete" } }, + { + "family": "platform", + "method": "DELETE", + "path": "/v1/projects/{projectId}/webhooks/{webhookId}", + "operationId": "deleteProjectWebhook", + "fingerprint": "2555d7a3c0501769b3560377c8595ee3413e7d38e29d91a21156b8a26cabc47b", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "DELETE", "path": "/v1/sessions/{sessionId}", "operationId": "deletePlatformSession", - "fingerprint": "fb8833af87056a96d80afcd557c814acae4bdbc5231b6300058014cb792a42f3", + "fingerprint": "009cdf86b86ea287b943a22b2958b1d4bdbbd641d67881b6193868d847322d44", "typescript": { "status": "covered", "method": "PlatformClient.sessions.delete" @@ -2052,19 +2156,31 @@ "method": "DELETE", "path": "/v1/templates/{templateId}", "operationId": "deleteMessageTemplate", - "fingerprint": "39e3af44591cbd8b148aae94d965156a88ccb71f6fa97f0017893365272de91b", + "fingerprint": "dd72834096e3536cd909d750f8f8fd88e8f94c3d3ce041cacf760eecbd873fcd", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", "milestone": "credential-boundary" } }, + { + "family": "platform", + "method": "DELETE", + "path": "/v1/webhooks/{webhookId}", + "operationId": "deleteOrganizationWebhook", + "fingerprint": "315c17d7e078e1003e719e9794627813f51d9e1ae40281f1b2c374555a41cfc5", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "GET", "path": "/admin/audit", "operationId": "adminListAuditLogGlobal", - "fingerprint": "d57d969a246643bd531c520741e441757b36678f8a65ebcde9a6857bd821a2a6", + "fingerprint": "cf1754fa03d280a52fefd8da466f6791cb168f5d5212537a49ecb21e9451fb87", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2076,7 +2192,7 @@ "method": "GET", "path": "/admin/audit/actions", "operationId": "adminListAuditLogAdminActions", - "fingerprint": "912d4ae68ad6597e4a067cd85b957047bf946be83454e0b5f36d101726e4a0bc", + "fingerprint": "9db6b2947639b5d9727b846289d5f785004909020203b50bf58d6f602ecc0606", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2088,7 +2204,7 @@ "method": "GET", "path": "/admin/bans", "operationId": "adminListRecentSessionBans", - "fingerprint": "36af9736402bf8f2c61a7cb8cd90a547ced0c5af74b7a8acc403a15d102a423c", + "fingerprint": "31e808f02991d35bc911897f5e56cf5955be273eef1e2d69969fb3b00284c5da", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2100,7 +2216,7 @@ "method": "GET", "path": "/admin/billing/leaderboard", "operationId": "adminTopOrgsByRevenueBilling", - "fingerprint": "4cba59bd6aff3acdccf3c79258cda3a7c9af00595bbdf75131db8071ad51f505", + "fingerprint": "bd551d50b3841132a0d7337450cd6ba3516db319c0fc62ce444c18d6076c5db2", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2112,7 +2228,7 @@ "method": "GET", "path": "/admin/billing/lowbalance", "operationId": "adminLowBalanceOrgsBilling", - "fingerprint": "76147c7c4b7770a21f75dc99e21cd49db9db1ab4ca451fe87711c086cbd9070e", + "fingerprint": "b6c693576751caa97b26aca1f99a775912d709d5ad893f1b96223dc857b27c14", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2124,7 +2240,7 @@ "method": "GET", "path": "/admin/billing/revenue", "operationId": "adminPlatformRevenueBilling", - "fingerprint": "a5f18a66427768c834d9208f8bda487be309ceb3a85065afcbce71c47912cf01", + "fingerprint": "e5efb95d6c75256293676aa700e66d0c6d874720d030ef0dc96ef3c708bd42d7", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2136,7 +2252,7 @@ "method": "GET", "path": "/admin/campaigns", "operationId": "adminListCampaigns", - "fingerprint": "8efc4de29777080762f84c6b7d86c11b4cd9c036f52d096f085d999faa91e1b8", + "fingerprint": "5cf420fbac81f17078fdb6cf52907e73b866a437ebf15a6a094e69bd91f6da56", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2148,7 +2264,7 @@ "method": "GET", "path": "/admin/campaigns/optouts", "operationId": "adminOptOutLeaderboardCampaign", - "fingerprint": "d2db2b2590f714df21747b239f479b6ea810269dbb712dfc6c127ce714676021", + "fingerprint": "9aa141b46fd96c01548f8b69892aa06179fd23d15966b8e7cea1f216ecd2d629", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2160,7 +2276,7 @@ "method": "GET", "path": "/admin/keys", "operationId": "adminListApiKeys", - "fingerprint": "c996e273e094a4eeb01f5528d7fa915a06032a553a66552b2c2b9372cda34122", + "fingerprint": "1ff6c744e353cd170fb5d3d7e2e8061a7b0f47ae2f408a1117929792503d727f", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2172,7 +2288,7 @@ "method": "GET", "path": "/admin/kpis", "operationId": "adminPlatformKpisPlatform", - "fingerprint": "9bbd6a309055dbccc89e1ed40bf690597a3019124b4cd6f9579dda4026bc83f2", + "fingerprint": "3ad6c63b3e7a6caea00363b21c287c1c5bd2dfa08e05fdd41345c7cb8ce1e144", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2184,7 +2300,7 @@ "method": "GET", "path": "/admin/organization-invitations", "operationId": "adminListOrganizationInvitations", - "fingerprint": "d31cccbd47f3234df4129a3da9106c7cb6fc29fd762c931b1d8716dee2d5f263", + "fingerprint": "09dacaf2b15866b1ee597569d2395ddff156029e79b0681e6b4f2aea956fdbe9", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2196,7 +2312,7 @@ "method": "GET", "path": "/admin/orgs", "operationId": "adminListOrganizations", - "fingerprint": "f04485a0cc74b465d96ba60599b2b4df70d4f9ce368d8b1ba7bc1318f12fa2f6", + "fingerprint": "9d41dedfb00afac7a6013df50893fb36be67e179a7ab86dbc06a133bee284a5a", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2206,9 +2322,9 @@ { "family": "platform", "method": "GET", - "path": "/admin/orgs/{orgId}", - "operationId": "adminGetOrganization", - "fingerprint": "036ed57d494d22f97c18c13025382cb088ae519ae3d0cdb509eff43c45470042", + "path": "/admin/orgs/features", + "operationId": "adminListOrganizationFeatureGroups", + "fingerprint": "eb809ba63426a43d95fea2c35a9c2c2d5927d181de64027b99362696a997ca15", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2218,9 +2334,9 @@ { "family": "platform", "method": "GET", - "path": "/admin/orgs/features", - "operationId": "adminListOrganizationFeatureGroups", - "fingerprint": "7fdd410965e89279f69ff102ff9a0f69838d25f226b5f1e389a98a7ee9575ca3", + "path": "/admin/orgs/{orgId}", + "operationId": "adminGetOrganization", + "fingerprint": "6b9c45cf94ebbc2eb88ba59a6d0c65ffd3adb7d85610306624f9200391f232b2", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2232,7 +2348,7 @@ "method": "GET", "path": "/admin/pricing", "operationId": "adminListPricing", - "fingerprint": "fae20bb216793def9745f300643544a11492462f1a5b69100478237b9799d633", + "fingerprint": "6fffbc67d71392d03997c21708e1430cc817706464a1ddf6b4c2973325b03af3", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2244,7 +2360,7 @@ "method": "GET", "path": "/admin/sessions", "operationId": "adminListSessions", - "fingerprint": "5b9856036ad38ba90abc30f960d0265db8200ee1f36f6d67c29c95a7754b2982", + "fingerprint": "8c9f9acacf56ae316a16d182adecb72df4497b2a147a10014e9c9619198701d4", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2256,7 +2372,7 @@ "method": "GET", "path": "/admin/signups", "operationId": "adminRecentSignupsPlatform", - "fingerprint": "053ac5899e5dbf7903276efde73f8b29d7f5bfa375d2e89a88e7c8de5805868c", + "fingerprint": "60138e38218bf8afa1093c6c689f22f7ff78ab1d214f4ac8cc77c7ce40a913fd", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2268,7 +2384,7 @@ "method": "GET", "path": "/admin/tenants", "operationId": "adminListTenants", - "fingerprint": "c60f6ca73ae1c839448f8be6e88dcf994e5802637e6fd2758041d21e627db8e9", + "fingerprint": "1c2524576a92fe2fc94a4151ecd2b9ca15ce8ac71b5fdddc603facda7b149582", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2280,7 +2396,7 @@ "method": "GET", "path": "/admin/users", "operationId": "adminSearchUser", - "fingerprint": "fb2726a16da0f75fd3f95593c7caa33ded0a70f147452194fff10551ea278ebd", + "fingerprint": "70f33c4a358d4b371c7280011504cbaffb466626663175452fb39caa2ed76043", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2292,7 +2408,7 @@ "method": "GET", "path": "/admin/users/{userId}", "operationId": "adminGetUser", - "fingerprint": "1655f9325e118f9f0d3a4d444181017d038724a2182405359c8608c4910a0934", + "fingerprint": "29577189dc3923646b9578bec4cbabbefcbc01e12032d823550e3f582f9c03c2", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2304,7 +2420,7 @@ "method": "GET", "path": "/admin/webhooks", "operationId": "adminListWebhooks", - "fingerprint": "ee329ac7479b1f039181f328aff30c47d00dcf22923a0413caf7dfb0ade5b77e", + "fingerprint": "747e9241cf93f1f137d5b03441e34d2768e196f2fcab1c1f4d9d1cbab4f396fb", "typescript": { "status": "excluded", "reason": "This Polymorfa staff route requires a staff dashboard identity and never accepts server credentials.", @@ -2316,7 +2432,7 @@ "method": "GET", "path": "/console/auth/methods", "operationId": "getUserProfileAuthMethods", - "fingerprint": "a737c8cb84a7935c1e65012762a01b1e05401057918cb53fbf4d4ae440c303b8", + "fingerprint": "1bd623090e122d30e8ff00c302124ded60fbeff9ab6cb74678f70a86ffaf2bef", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2328,7 +2444,7 @@ "method": "GET", "path": "/console/auth/providers", "operationId": "getUserProfileProviderConnectionUrl", - "fingerprint": "6b899f435977261ba00ec93e0634a38838417d7b730b4cc585d5b09ffabfdbfb", + "fingerprint": "802bfce0053dafb44d72010daecab03558f97fcd3998b6f73fe3993b40d907fe", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2340,7 +2456,7 @@ "method": "GET", "path": "/console/deployments", "operationId": "listDeployments", - "fingerprint": "011f164af45fd831283c8b63abcaa064c6bd5468f6a1bdc81fa2941ea7f1a8ff", + "fingerprint": "d74d41b9e41d2abb95f5287598753d51f8693fbbdfe4b7312b4bebfef2dfd647", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2352,7 +2468,7 @@ "method": "GET", "path": "/console/insights/analytics", "operationId": "analyticsInsight", - "fingerprint": "2086db701e228e18308d118de0035261b620c294c90b914cfc7a461f42e41d9e", + "fingerprint": "3fb25690a6d3878ba5a8faae0f85821c468d1a0875abde652dcaa938b2d9b567", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2364,7 +2480,7 @@ "method": "GET", "path": "/console/insights/logs", "operationId": "logsInsight", - "fingerprint": "9ed5e7c3e6514445b08c593d89a6a52ac177719d673e0e536ec804fd3ec4f761", + "fingerprint": "168c568a4bd282fc9adc5456c7c31257da5ef4fc09b0f3f82242033695ca2143", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2376,7 +2492,7 @@ "method": "GET", "path": "/console/insights/preferences", "operationId": "getInsightPreferences", - "fingerprint": "fb0335423413358fd9332ab2265cf7dbe5804700c8533f929dd597e5b4898ef3", + "fingerprint": "5bde670e318a9b03442b454d383787e2ef162af19386b80be786a80a038a2404", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2388,7 +2504,7 @@ "method": "GET", "path": "/console/insights/usage", "operationId": "usageInsight", - "fingerprint": "972da218ad42a45b146e6ae61906bdf8a8ee88293ab21a25fafd9c784f4f2b4d", + "fingerprint": "a328706d9d73cedeed789b9a8c2617936d488153dde340204cdeccdfde7f6f30", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2400,7 +2516,7 @@ "method": "GET", "path": "/console/notifications", "operationId": "listNotificationInbox", - "fingerprint": "692a7b0c47f64e1fae63a2b74f5cb625506584c3811ee3083ff8f4509e234bf5", + "fingerprint": "c832c1d0db125e68078c7c8d23f69de8f3047a2e8181d060757678d74eab39ad", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2412,7 +2528,7 @@ "method": "GET", "path": "/console/notifications/archived", "operationId": "listNotificationArchived", - "fingerprint": "bce4e842c13d376238598e1cb7964825e70f3f4a556d7789423b9a314796238d", + "fingerprint": "9c58b2f8d266433cf1114b9b8576dd10ea0159a3e3260352a1eae3a26ce420bb", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2424,7 +2540,7 @@ "method": "GET", "path": "/console/notifications/count", "operationId": "unreadCountNotification", - "fingerprint": "893313e46bc52e8576e3a8d5028ab4f005e534644532a0c18d8b870278423811", + "fingerprint": "1eda657577a51d81d65d26649334d87f31134f02dd49e390b4bff67b6eb9c27d", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2436,7 +2552,7 @@ "method": "GET", "path": "/console/notifications/preferences", "operationId": "getNotificationPreferences", - "fingerprint": "2730b43eea282c140d79d65f54dce7de191f2a6e69be5f6e689ff992533231ff", + "fingerprint": "1efdd8ff03a007005db5f4ca0cf84d5ab6f3a44a3fe8ce076affd0db245d8e96", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2448,7 +2564,7 @@ "method": "GET", "path": "/console/organizations/{slug}", "operationId": "getOrganizationBySlug", - "fingerprint": "c9f40f0815d7dd7eeaf9b2b6d45482b29cc4cca66da22564aada0e61f2cd32fa", + "fingerprint": "33cd3dd246c9aee3ca87c2817f0865ab6e10e8c1cbf963f11f35f984b4c0780b", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2460,7 +2576,7 @@ "method": "GET", "path": "/console/profile", "operationId": "getUserProfile", - "fingerprint": "eef7bd6da2d98555b1830cf5863a56de4705ac25defd1be474b94076fe5b2a62", + "fingerprint": "268c308bf9b787ff061724c1fbfb4fdb4e41b2f9cc7042bfb4ff603d9d27df9c", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2472,7 +2588,7 @@ "method": "GET", "path": "/console/sandbox/pool", "operationId": "getSandboxProjectPool", - "fingerprint": "330a8642ba195267dfadd9e2598b95866fb058c079dcc2c8711f121d29181e6b", + "fingerprint": "4683dadbc8b7f8899aa2982dc7c8e35f68d05f0cbf2f9096358eb8871d1bae0e", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2484,7 +2600,7 @@ "method": "GET", "path": "/console/teams", "operationId": "listUserProfileTeams", - "fingerprint": "884d5e8c868dfc7c884fe79626d2a0286345e5a8433eff4bf849fb293113a086", + "fingerprint": "bd7be5d4fe965b5f9fb84363b2f4c531f1c4a86303d7a686acce4663950e83b2", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2496,7 +2612,7 @@ "method": "GET", "path": "/console/webhooks", "operationId": "listWebhooks", - "fingerprint": "cf49a2ff1e6c5ba9160c3fe1ce53a2c51238fcd6f32abecedab8905df860fcc4", + "fingerprint": "4858b4efdc8d5f8c8d722bd8c8f84cac9f2fdfa83025204bf54ddbef3d9c4f6e", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2508,7 +2624,7 @@ "method": "GET", "path": "/v1/audiences", "operationId": "listAudiences", - "fingerprint": "c9b7fea6681bda1193d6644e3d8bbecbfe9d3209d92e488709809fd5bdd9ea8f", + "fingerprint": "203826fc7769c25645eceac8e1f711d964344d6de8afc5f591f7a5c51bf0a86a", "typescript": { "status": "covered", "method": "PlatformClient.audiences.list" @@ -2519,7 +2635,7 @@ "method": "GET", "path": "/v1/audiences/{listId}", "operationId": "getAudience", - "fingerprint": "d112bac7a31b48726e5b733b4ccefd1176d18e46b98a955637998e11da393b7e", + "fingerprint": "0faa242ed87367ed7f3776955e9923148ff5ba162e1ecf0e652dd8cbbbfd304a", "typescript": { "status": "covered", "method": "PlatformClient.audiences.retrieve" @@ -2530,7 +2646,7 @@ "method": "GET", "path": "/v1/audit", "operationId": "listAuditLogs", - "fingerprint": "999c49da6658727309d20ff21be19ec3e1b32b0109d4de7e28a58f125bf7783d", + "fingerprint": "677185d7ed1e85806e95ebaff70014686ce20e118712b5ba4f1adb32568d80ee", "typescript": { "status": "covered", "method": "PlatformClient.auditLogs.list" @@ -2541,7 +2657,7 @@ "method": "GET", "path": "/v1/bans", "operationId": "listSessionBans", - "fingerprint": "084cee0e03641281ed8289eaac93ee2a568c08a7b58fe6a91f38160f0cdb26a9", + "fingerprint": "888d886c7dcee97ade64909ad73bc187c5e12696492198579c56e1fb98805ef5", "typescript": { "status": "covered", "method": "PlatformClient.sessionBans.list" @@ -2552,7 +2668,7 @@ "method": "GET", "path": "/v1/bans/active", "operationId": "listActiveSessionBans", - "fingerprint": "156741d1e35d99996ed69dafb4c7b5978a51ad1bc13c5946a3f10eebbe2f2482", + "fingerprint": "2245fb243558391a9e6e0b9253144743bf42dfb5c90d5b1cb0d74f3df38dce61", "typescript": { "status": "covered", "method": "PlatformClient.sessionBans.listActive" @@ -2563,7 +2679,7 @@ "method": "GET", "path": "/v1/billing", "operationId": "getBilling", - "fingerprint": "d02dff1f6a9982c73eaca4bd8ae1cb54c374c59ae8cc07dbff709f1ad4a84cc7", + "fingerprint": "b79ffd5c2db93f74b53fbb286d340853e4193609be3b25c3c221a065e80aa5ac", "typescript": { "status": "covered", "method": "PlatformClient.billing.retrieve" @@ -2574,7 +2690,7 @@ "method": "GET", "path": "/v1/billing/pricing", "operationId": "listTierPricing", - "fingerprint": "11fd2a21332fc6cc122e2453819b5832e03fa24a2ae035700a040749889db798", + "fingerprint": "552594051767287f7d22abb45a38e2bd91fa07242905b92eda6d98d24838e99c", "typescript": { "status": "covered", "method": "PlatformClient.billing.listPricing" @@ -2585,7 +2701,7 @@ "method": "GET", "path": "/v1/billing/transactions", "operationId": "listBillingTransactions", - "fingerprint": "dcd05457f7da5d5133e1283af495c541527d091d9f89cba7fad999692aa8c034", + "fingerprint": "521773873dbc673426bf4d013f058b10c0b3505f65af83e0de49dc114d052940", "typescript": { "status": "covered", "method": "PlatformClient.billing.listTransactions" @@ -2596,7 +2712,7 @@ "method": "GET", "path": "/v1/billing/usage", "operationId": "listUsageMeters", - "fingerprint": "608b3ae4cc856505bca3f0295432a4d27070cfc86e44b99a8d65a717c6587548", + "fingerprint": "49314723af2ef845963a473abbfeba6b3c331fc524b8223f3223bc8569e07a47", "typescript": { "status": "covered", "method": "PlatformClient.billing.usage" @@ -2607,7 +2723,7 @@ "method": "GET", "path": "/v1/campaigns", "operationId": "listPlatformCampaigns", - "fingerprint": "93e1ad98cea3e0c386e94259f325bfa164e730e6ad367e42ab9b2607ebe04dbb", + "fingerprint": "13541c77e70ae753f224408e344829aacf76d3a88416f686cdf19b4434b6dc12", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.list" @@ -2618,7 +2734,7 @@ "method": "GET", "path": "/v1/campaigns/{campaignId}", "operationId": "getPlatformCampaign", - "fingerprint": "da6692869d64aa37690d8547d7b4a9ec27af091c309ccb44e04681a051fcc20e", + "fingerprint": "8a62796d923adf424de9e19e952073a71d11776745221373bbcdd8ab5d2f997b", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.retrieve" @@ -2629,7 +2745,7 @@ "method": "GET", "path": "/v1/campaigns/{campaignId}/analytics", "operationId": "getPlatformCampaignAnalytics", - "fingerprint": "810a9a1d4da4ac527ff5e1bda34d4b17fb740060c9655762cdb4e19cfa794a43", + "fingerprint": "f6af6d45f076a5eafd9ddcb441cd44cc3b0ff6e9b8021dcab8ef9c6708db1daf", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.analytics" @@ -2640,7 +2756,7 @@ "method": "GET", "path": "/v1/campaigns/{campaignId}/events", "operationId": "listCampaignEvents", - "fingerprint": "503b8fe2d1ef229b5fb3c39aab4e0d679f5ac3a97967b5b52113f83fca8062d8", + "fingerprint": "66c0c14c9cf8608854050ae3fd7677cd320d7c8b2289aef64d6f7857c9322e93", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.events" @@ -2651,7 +2767,7 @@ "method": "GET", "path": "/v1/campaigns/{campaignId}/recipients", "operationId": "listCampaignRecipients", - "fingerprint": "8a157a2a3454a6445153b60fce01f2c89748afb3d5fb050782767fec5fd494ff", + "fingerprint": "dc30a8e28bc4801b30c9b95230a61fa6a3392cc56dad1dc6466eb1026837d512", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.recipients" @@ -2693,146 +2809,326 @@ { "family": "platform", "method": "GET", - "path": "/v1/customers/{customerId}/numbers", - "operationId": "listCustomerNumbers", - "fingerprint": "14ddf26d897bc605a839082e43ac77675f4a82e935cfdca8aa80adc398a2a3a2", + "path": "/v1/customers/{customerId}/numbers", + "operationId": "listCustomerNumbers", + "fingerprint": "14ddf26d897bc605a839082e43ac77675f4a82e935cfdca8aa80adc398a2a3a2", + "typescript": { + "status": "covered", + "method": "PlatformClient.customers.listNumbers" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/customers/{customerId}/pairing-links", + "operationId": "listCustomerPairingLinks", + "fingerprint": "6d419c3d4369c5020e8e11588a62e23882dd58f81546d57796235d816f366543", + "typescript": { + "status": "covered", + "method": "PlatformClient.customers.listPairingLinks" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/events", + "operationId": "listOrganizationEvents", + "fingerprint": "5be3e55ec98b3beed960218f6af5a5ace861a5a6811b1c62814085f57e1b238c", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/events/{eventId}", + "operationId": "getOrganizationEvent", + "fingerprint": "160c5e4c7b91b8b633b004961d2039b0a974fd56f3b8560abb67e5dd22ae5dbf", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/flows", + "operationId": "listFlows", + "fingerprint": "b77ce1c733584fa605761e207035cef871bc49341c665cfc82643243debfd47f", + "typescript": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", + "milestone": "credential-boundary" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/flows/{flowId}", + "operationId": "getFlow", + "fingerprint": "30eb629b546bed2aeab2450a75fb32ac25831c3f522fa8ae0fff2ffa5cebf301", + "typescript": { + "status": "excluded", + "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", + "milestone": "credential-boundary" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/incidents", + "operationId": "listSecurityIncidents", + "fingerprint": "c2e69c4b877a1914f1a5bfcbd4820c13749b1ccf70ea0c5e878e1a3fd86c153a", + "typescript": { + "status": "covered", + "method": "PlatformClient.securityIncidents.list" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/keys", + "operationId": "listApiKeys", + "fingerprint": "603de482c9fbe57d09fbbca97920c80f2b9b7fd06a2e4e29bd07b2abcfbde6fe", + "typescript": { + "status": "covered", + "method": "PlatformClient.apiKeys.list" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/media/{mediaId}", + "operationId": "getMediaUrl", + "fingerprint": "860947f02650cea2d14e5827bdd8a8788c2f1714cdd6d3465bf78eb6424b7b77", + "typescript": { + "status": "covered", + "method": "PlatformClient.media.retrieve" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/members", + "operationId": "listMembers", + "fingerprint": "76ff2fcdeb2b299974b01dd5ff26b0b1d209b802e08d0d523fa7c2ea4d09a232", + "typescript": { + "status": "covered", + "method": "PlatformClient.members.list" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/operations", + "operationId": "listOrganizationOperations", + "fingerprint": "b82282f9c6348a836b3f21000e080efb517b701d988758b0fa802ff30dae56b3", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/operations/{operationId}", + "operationId": "getOrganizationOperation", + "fingerprint": "93c96fd49f49773bbf8006cba357f42f26370c3da1af9e9d73518389ba54bfca", + "typescript": { + "status": "covered", + "method": "PlatformClient.operations.retrieve" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/operations/{operationId}/transitions", + "operationId": "listOrganizationOperationTransitions", + "fingerprint": "eb99e217f76bca9939974be5ff17ed8f5f71c4c6a3ff0923981254efdb936486", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/optouts", + "operationId": "listOptOuts", + "fingerprint": "fa236cca96686c6191aa1f2cdb84272e025f89aef0a25504b9cf532028fe1ae6", + "typescript": { + "status": "covered", + "method": "PlatformClient.optOuts.list" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/organization", + "operationId": "getOrganization", + "fingerprint": "1cc1249633ee2ef286e59fdee2f8d51a78648a4a68f6cf67614d39a6fd8c7ef9", + "typescript": { + "status": "covered", + "method": "PlatformClient.organizations.retrieve" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/projects", + "operationId": "listProjects", + "fingerprint": "8b4f569d2828572eecd824d2f28b33775e894faa1dba9693de6729e586fce628", "typescript": { "status": "covered", - "method": "PlatformClient.customers.listNumbers" + "method": "PlatformClient.projects.list" } }, { "family": "platform", "method": "GET", - "path": "/v1/customers/{customerId}/pairing-links", - "operationId": "listCustomerPairingLinks", - "fingerprint": "6d419c3d4369c5020e8e11588a62e23882dd58f81546d57796235d816f366543", + "path": "/v1/projects/{projectId}/customers/status", + "operationId": "getCustomersStatus", + "fingerprint": "2ea59c9703c76cf8bfd04d74166ce4504d70fa53090049fd6614ba1044d103a2", "typescript": { "status": "covered", - "method": "PlatformClient.customers.listPairingLinks" + "method": "PlatformClient.customers.status" } }, { "family": "platform", "method": "GET", - "path": "/v1/flows", - "operationId": "listFlows", - "fingerprint": "ca6c44c532123d6270b56b090b52629605d6dddc151fd3a9381c8145dec3c303", + "path": "/v1/projects/{projectId}/events", + "operationId": "listProjectEvents", + "fingerprint": "ab2f389ed49a29b77d55e2431a671a7dfa328a2fd3bd64b8ea3ca07704dae671", "typescript": { - "status": "excluded", - "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", - "milestone": "credential-boundary" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/flows/{flowId}", - "operationId": "getFlow", - "fingerprint": "26e16fe0f8d5124e2b9538eff308676aa8253370a115373a882d3aa65b82abfb", + "path": "/v1/projects/{projectId}/events/{eventId}", + "operationId": "getProjectEvent", + "fingerprint": "625009ef487d2c3140be83fd59a68577224e782fef48476132702ac96d2ff15a", "typescript": { - "status": "excluded", - "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", - "milestone": "credential-boundary" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/incidents", - "operationId": "listSecurityIncidents", - "fingerprint": "882607c0cff97814db0ad77543491994ef7b67d0e867f5c542b5135d57f4d7b8", + "path": "/v1/projects/{projectId}/operations", + "operationId": "listProjectOperations", + "fingerprint": "402c9db5f517ca805d26c82cb2da0a4d417d1482cba1910c168ef0c7402b4498", "typescript": { - "status": "covered", - "method": "PlatformClient.securityIncidents.list" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/keys", - "operationId": "listApiKeys", - "fingerprint": "a6a4001725caf40d91f2f3fa123df5d9bf3cd6ca3d2e4886950989c9b2d386a6", + "path": "/v1/projects/{projectId}/operations/{operationId}", + "operationId": "getProjectOperation", + "fingerprint": "c2e69bf06e704272dddb0c998ab8a665e4b48df8243ae511c5d64b14febb9b80", "typescript": { - "status": "covered", - "method": "PlatformClient.apiKeys.list" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/media/{mediaId}", - "operationId": "getMediaUrl", - "fingerprint": "b3ebfeb65b98896c8e12a16e93d195891f106629c36af1035ce147250587c59b", + "path": "/v1/projects/{projectId}/operations/{operationId}/transitions", + "operationId": "listProjectOperationTransitions", + "fingerprint": "791d29cded696ad923666f84ed184b18af05d03da0a938a4ffa7fa926adb63d6", "typescript": { - "status": "covered", - "method": "PlatformClient.media.retrieve" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/members", - "operationId": "listMembers", - "fingerprint": "cdc51521bfce0d50de725df0000e54ceb9302d4814f888903adeb9662ab10182", + "path": "/v1/projects/{projectId}/webhook-deliveries", + "operationId": "listProjectWebhookDeliveries", + "fingerprint": "05ef9b32c73de411c31d8f6fc859764a13be04b9d5fdb46fe374a10d982ece12", "typescript": { - "status": "covered", - "method": "PlatformClient.members.list" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/operations/{operationId}", - "operationId": "getPlatformOperation", - "fingerprint": "1525631606ad8d0572664199c617d398036b7a6a8e31bc1851b1b8805820df91", + "path": "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}", + "operationId": "getProjectWebhookDelivery", + "fingerprint": "abb159dcd6242c16f333dd17b7f32b471d8548d17e447285d346622c602563e7", "typescript": { - "status": "covered", - "method": "PlatformClient.operations.retrieve" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/optouts", - "operationId": "listOptOuts", - "fingerprint": "f35a9d0aba83389d3f301c7b6e2fcf74848ecf24da6762de0764f8f57df5e923", + "path": "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}/attempts", + "operationId": "listProjectWebhookDeliveryAttempts", + "fingerprint": "faf7d82f3c2c65faf4397cddcbc66566a51dbc46f0e7390509430168f0a23fd5", "typescript": { - "status": "covered", - "method": "PlatformClient.optOuts.list" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/organization", - "operationId": "getOrganization", - "fingerprint": "b028772e48352c90c8e6a7129105a74984c119f6b3b10664b3dd38dcf83ada76", + "path": "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}/attempts/{attemptId}", + "operationId": "getProjectWebhookDeliveryAttempt", + "fingerprint": "6cb230760cc6dd4c4a5572c634b3350af4ca47762fea75a4b62bfa7014b70aef", "typescript": { - "status": "covered", - "method": "PlatformClient.organizations.retrieve" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/projects", - "operationId": "listProjects", - "fingerprint": "0122a1ccd2d1e67ab67da00897bcefbb3484f6f06ce5e70fe271e4595279817a", + "path": "/v1/projects/{projectId}/webhooks", + "operationId": "listProjectWebhooks", + "fingerprint": "b0ef0ff74c50c943198af2ae17c79bba7cf2321fafc7b82199f10bddc5d3c6f1", "typescript": { - "status": "covered", - "method": "PlatformClient.projects.list" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { "family": "platform", "method": "GET", - "path": "/v1/projects/{projectId}/customers/status", - "operationId": "getCustomersStatus", - "fingerprint": "2ea59c9703c76cf8bfd04d74166ce4504d70fa53090049fd6614ba1044d103a2", + "path": "/v1/projects/{projectId}/webhooks/{webhookId}", + "operationId": "getProjectWebhook", + "fingerprint": "08ef24fe00067de9e010e41acd4c79f285e257652c61186c62ee8f0ebf6a95fb", "typescript": { - "status": "covered", - "method": "PlatformClient.customers.status" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { @@ -2840,7 +3136,7 @@ "method": "GET", "path": "/v1/sessions", "operationId": "listPlatformSessions", - "fingerprint": "23d0ef983428e45d7d6b0f7b059a3d6854d36f407f42832a20d6ca62cc3ead7a", + "fingerprint": "7bfd8c212e34f1f5e496f582608769d1667b0eaee3581609873591566b1c0e89", "typescript": { "status": "covered", "method": "PlatformClient.sessions.list" @@ -2851,7 +3147,7 @@ "method": "GET", "path": "/v1/templates", "operationId": "listMessageTemplates", - "fingerprint": "a922df1848c0555fa235eae538b5c3e99a39ba1900821014ce4066e9d5591910", + "fingerprint": "f38d5515565c7d1ef8ddc82968906af4d7e0c0f15b8ffca2cfeea272583b26fd", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", @@ -2863,7 +3159,7 @@ "method": "GET", "path": "/v1/templates/{templateId}", "operationId": "getMessageTemplate", - "fingerprint": "b5a28dc474e45626de59a6d45295860f3f06112d3e9283acc13f86bae59c9f3e", + "fingerprint": "8cb15afe849c0c8d223fdf4e6c9e9d220715778da80f94f7572b8f5966aaa8de", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", @@ -2875,18 +3171,90 @@ "method": "GET", "path": "/v1/tokens", "operationId": "listPolymorfaTokens", - "fingerprint": "7e8644a38ef0b08cfa71b7b08ad21aab69d22df186fe8c22bdb5fd3b25d96032", + "fingerprint": "b8d5d76ce747845f745082489c05037c0cdaf9d20a03a6d7809459d9a5ce35ce", "typescript": { "status": "covered", "method": "PlatformClient.projectTokens.list" } }, + { + "family": "platform", + "method": "GET", + "path": "/v1/webhook-deliveries", + "operationId": "listOrganizationWebhookDeliveries", + "fingerprint": "b34dcc5f989890f4a244c7b98cd2a1103a1ae7587eaaff5990c6ae35dce6eab3", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/webhook-deliveries/{deliveryId}", + "operationId": "getOrganizationWebhookDelivery", + "fingerprint": "d4fa1bc619a17a6e42609717f2094506d670c6faac93bf1673fcca18c71e99d4", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/webhook-deliveries/{deliveryId}/attempts", + "operationId": "listOrganizationWebhookDeliveryAttempts", + "fingerprint": "9dd8f70e0f94223481cdeebb2308330492b6ee916b1f14cfcc19d92a93b9fbe3", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/webhook-deliveries/{deliveryId}/attempts/{attemptId}", + "operationId": "getOrganizationWebhookDeliveryAttempt", + "fingerprint": "504298df2cd3af0e90a3395523a507d4f053a718b0e346fa75f1f21651fcc366", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/webhooks", + "operationId": "listOrganizationWebhooks", + "fingerprint": "52475b3163f0fbc7862a8462b030677e326da65807f10659d5df0942ffca49e8", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "GET", + "path": "/v1/webhooks/{webhookId}", + "operationId": "getOrganizationWebhook", + "fingerprint": "573b9da2a147e13f7b6191db6554eea579e3a4d8cc555119ae9d65c00e6dd5e9", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "GET", "path": "/v1/widget", "operationId": "getWidgetSettings", - "fingerprint": "b8ca527c78e69322e2e4ea655cf6ba6c1c796da20a9a5571e1f37f34b72d90ba", + "fingerprint": "331db9ff8f2719d3d44f9cecb04351e22d35b4d71df6061bd873cf99bdfe0b9e", "typescript": { "status": "covered", "method": "PlatformClient.widgetSettings.retrieve" @@ -2897,7 +3265,7 @@ "method": "PATCH", "path": "/admin/members/{memberId}", "operationId": "adminSetMemberRole", - "fingerprint": "cc3604ea1e3a1e5751c7110835be4d91bb7135acbba1e8846529a6ba7190251e", + "fingerprint": "1d40f79d50a6ab2e1187fa1efe66060a3200d0d545a7e67099b16c22a97a10a3", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2909,7 +3277,7 @@ "method": "PATCH", "path": "/admin/orgs/{orgId}", "operationId": "adminRenameOrganization", - "fingerprint": "af9395d65cd5cef0ed8c6d2f3aa9f623dc961825b522985d97211ae7576d6907", + "fingerprint": "e0e6d6798056ed6f37d726a3f7a60b1e81a47078a14c9bb7c676b49184974261", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2921,7 +3289,7 @@ "method": "PATCH", "path": "/admin/orgs/{orgId}/features", "operationId": "adminSetOrganizationFeatureGroups", - "fingerprint": "b1d524f641c32aa47c8ca5acff5aef7553c678bc81f273d21448c02777fe1ccb", + "fingerprint": "db5781f4cfddd0bead2de6e502755eac0831478c2ad28353ac95a5fbedc4aa3c", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2933,7 +3301,7 @@ "method": "PATCH", "path": "/admin/pricing/{tier}", "operationId": "adminSetPricingActive", - "fingerprint": "9a32bf4835ae7cda6d1866886a75cd0313bb3e9b80cf0b44394b96b62cadf550", + "fingerprint": "9eddf6277243ee32343ab00f6a70fb184677b604f9e841b6d6ab9f81a209fcbf", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2945,7 +3313,7 @@ "method": "PATCH", "path": "/admin/tenants/{tenantId}", "operationId": "adminSetTenantActive", - "fingerprint": "9dba78ce971063568a2516c17b9a889a03faabf29e204dbc6917ba6ca84b8292", + "fingerprint": "8731e12cb798c00e9b4fcc17912a0164c80d6d2b6656b06e358706a5a23614ed", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -2957,7 +3325,7 @@ "method": "PATCH", "path": "/console/profile", "operationId": "updateUserProfileField", - "fingerprint": "4ca796cf59071fe5f5e76c592a8d5b748c710de5266237780474fba1256b7a70", + "fingerprint": "a43dc80d690a88347809fc17bba97a887a0412ed6c6b2324c39b354f9b2fcbe6", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2969,7 +3337,7 @@ "method": "PATCH", "path": "/console/webhooks/{webhookId}", "operationId": "updateWebhook", - "fingerprint": "0c26fe3c4a9b8d786e6174c8503ea1e9a38bd5947af012a0856315fa1802dfc2", + "fingerprint": "81a5153b9ac25c1a2d29f827af5f9f77c57760cda96013b709e0c2f9e72eac24", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -2981,7 +3349,7 @@ "method": "PATCH", "path": "/v1/billing/reminders", "operationId": "updateBillingReminderSettings", - "fingerprint": "e4daaaab1908b0be01151e3b76ad71321e1948550f0c9ad6d51a007fcebb23f5", + "fingerprint": "58ee9e21033128d7f90773953f5b9900107e4dc7a2c83a024bf3b4b145f195a8", "typescript": { "status": "covered", "method": "PlatformClient.billing.updateReminderSettings" @@ -2992,7 +3360,7 @@ "method": "PATCH", "path": "/v1/campaigns/{campaignId}", "operationId": "updateCampaign", - "fingerprint": "939a5e4ab9df46828313043f36cbef0b04edfe40600518bd5728465f68242056", + "fingerprint": "15a5e192d9251828c843d90675e686f38eb4dabaadc39027e50390bcc8867b20", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.update" @@ -3014,7 +3382,7 @@ "method": "PATCH", "path": "/v1/flows/{flowId}", "operationId": "updateFlow", - "fingerprint": "cb1d868db9fe129780ac228b857655ba0b8335df0309ca8580b5a3f93b62ecf1", + "fingerprint": "5ca2b5ea86279e32bc071141d4e90078fdf01b77afff2bc415846058e8ceb62c", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", @@ -3026,7 +3394,7 @@ "method": "PATCH", "path": "/v1/members/{memberId}", "operationId": "updateMemberRole", - "fingerprint": "4ee4c993b6c6ddf57bc843ccab868bafadb6a837dd42bc3f17634b60e2089742", + "fingerprint": "e8a2544274d583bb7c406bc52818bdc172cc605a9c2fee184a876a7e55c2dbc5", "typescript": { "status": "excluded", "reason": "This dashboard-only member mutation accepts ConsoleSession authentication and rejects the organization server API key used by PlatformClient.", @@ -3038,19 +3406,31 @@ "method": "PATCH", "path": "/v1/organization", "operationId": "updateOrganization", - "fingerprint": "2bebc7153acadcdd51c38194d1953ab83faad44217c89a6024432ac2dd1dc33f", + "fingerprint": "9f1712477bf16a8a2372cbfbec20197d822cd0f29868b49f2f648540b52e31ff", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", "milestone": "credential-boundary" } }, + { + "family": "platform", + "method": "PATCH", + "path": "/v1/projects/{projectId}/webhooks/{webhookId}", + "operationId": "updateProjectWebhook", + "fingerprint": "ab722f393f3d724aff5bcac6b48e7e62e27221fa7e5216d772be7fa1f9668f4f", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "PATCH", "path": "/v1/sessions/{sessionId}", "operationId": "setSessionTierOverride", - "fingerprint": "97d9b6847397ece0d3f8ce97c327543f57906d4fa1d3ccfc5abe82bbc0421220", + "fingerprint": "b9a55c4616ef53fe433885201e23e4a6638f2c2afd78516ac90f2fabf0ba02bb", "typescript": { "status": "covered", "method": "PlatformClient.sessions.setTierOverride" @@ -3061,19 +3441,31 @@ "method": "PATCH", "path": "/v1/templates/{templateId}", "operationId": "updateMessageTemplate", - "fingerprint": "8c142f1a22edc5362f12344d7472b8b81d08fe7ae15b31215e5c7d8f201aa41b", + "fingerprint": "e84a8b125bb33aac5b8bff0443710eba04ba613c7ae117de12f8e178428ff4ec", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", "milestone": "credential-boundary" } }, + { + "family": "platform", + "method": "PATCH", + "path": "/v1/webhooks/{webhookId}", + "operationId": "updateOrganizationWebhook", + "fingerprint": "7a951130d9739ea4f6c3b9af13dd19a91ef5942d06d60f5d353f842a3ace2233", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "POST", "path": "/admin/billing/credits", "operationId": "adminCompCreditsBilling", - "fingerprint": "05a39871bd6ef6c3157dba0a596a80bd375d26235faf5c8dcc20f60baf5d6abf", + "fingerprint": "5dfeaa55ddc13056abf53756e6b7e927b5483a5d4aa20c72ad70e90707d84412", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3085,7 +3477,7 @@ "method": "POST", "path": "/admin/billing/refunds", "operationId": "adminRefundBilling", - "fingerprint": "a56b5568ca8ab8681ed6d8956f72c663284f64b4108d04291107dc906fb7cd01", + "fingerprint": "4a3af41a249fb8bedcde2947e5cd067d0cb4288b56f37458cf143fb85f7d5c17", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3097,7 +3489,7 @@ "method": "POST", "path": "/admin/campaigns/{campaignId}/pause", "operationId": "adminForcePauseCampaign", - "fingerprint": "d394b3659598f1eb52bc1a2b263c9afd0610a8a626177b5edbe0cebd67dde65e", + "fingerprint": "ca6cfea9e0133062915100b3e281ad66ac71495d79025cab5c0f9c8eb9d089e2", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3109,7 +3501,7 @@ "method": "POST", "path": "/admin/campaigns/{campaignId}/stop", "operationId": "adminForceStopCampaign", - "fingerprint": "a2dba0f7a9e167ca271455b1741565e7eb52bfef47df6161a0631f7cc5d45804", + "fingerprint": "e7b428272304f94eebcb48f487c872ccb6cf990adde488509971dd05315cf410", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3121,7 +3513,7 @@ "method": "POST", "path": "/admin/members/invites", "operationId": "adminInviteMember", - "fingerprint": "a7896e67e3be5efb081ce5dda15d03325a296ffe585807c9feed344349baa822", + "fingerprint": "48ded54b69d484be6d960b8f04baa62fda2d0d704314e4492660e23f730e9c24", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3133,7 +3525,7 @@ "method": "POST", "path": "/admin/organization-invitations", "operationId": "adminCreateOrganizationInvitation", - "fingerprint": "feff3a406cf318ff6bb2d5e06c12de33eeb676928ade641af8d0838bb52b79ee", + "fingerprint": "d8f635f3ed51be3f91be8da22e575a52752b423b2915f3ecb80deeee2c655828", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3145,7 +3537,7 @@ "method": "POST", "path": "/admin/orgs/{orgId}/freeze", "operationId": "adminFreezeOrganization", - "fingerprint": "c005672850694fedfe9b4a218c60809b04c857e6f8c998e6683cd0d66f3170b8", + "fingerprint": "29491cbdbba0b1131c42557c2d0da16387028278b6bf6f66e7bae610a9b506d8", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3157,7 +3549,7 @@ "method": "POST", "path": "/admin/orgs/{orgId}/unfreeze", "operationId": "adminUnfreezeOrganization", - "fingerprint": "ae1a6119aecced8d7bc44c631a87d6c9d8677c83aa38d9eac8686c30ab842ca9", + "fingerprint": "471b13a4f50084fd4b7e852f1c430d11c74173f89a0c8de7236a6cb7b6fa4a9d", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3169,7 +3561,7 @@ "method": "POST", "path": "/admin/sessions/{sessionId}/comp", "operationId": "adminCompSessionDaySession", - "fingerprint": "073099cf9e819fabe11227e9e6168a259a89c5e77be0392f196349f08c4295fd", + "fingerprint": "2805d221840da8a5acb4da164b01ff5c6996cb9875db2878057b2a3b39f5f9c8", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3181,7 +3573,7 @@ "method": "POST", "path": "/admin/sessions/{sessionId}/stop", "operationId": "adminForceStopSession", - "fingerprint": "40d86d63ee503ea5a27786afdfc56e8af2bbafe71556766672488f905789f903", + "fingerprint": "59de376f0586fb9fe59349d74928d981feab8226a4edc760108288d81cf47a45", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3193,7 +3585,7 @@ "method": "POST", "path": "/admin/sync", "operationId": "adminEnsureCurrentUserStaffSyncStaffSync", - "fingerprint": "e4b3469c69aa71739eab0920dc8c39751e925e69cee53d08d718d3bd3437713c", + "fingerprint": "f6dcffee982955559462f37e30d42545847d5531e6b4303c34476dde6cee7552", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3205,7 +3597,7 @@ "method": "POST", "path": "/admin/users/{userId}/signout", "operationId": "adminForceSignOutUser", - "fingerprint": "5c24c07e84215784928944b73ae27454e258a026b47fa4a0edf7f37ce9b14d9f", + "fingerprint": "71a604cf9f25b568ae4573f771aa5159b6b576ebde45163b1a5dccf34ee43de1", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3217,7 +3609,7 @@ "method": "POST", "path": "/admin/users/{userId}/suspend", "operationId": "adminSuspendUser", - "fingerprint": "93865085071a0b333eecbd72876135780145e994dd6e82abc9c6b79007aef025", + "fingerprint": "d93139af07a146aaab9f6f8c100dbc05d084099f8566708c6348a60c43125c1c", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3229,7 +3621,7 @@ "method": "POST", "path": "/admin/users/{userId}/unsuspend", "operationId": "adminUnsuspendUser", - "fingerprint": "0a246aaf40a593c66447efdd17134ff3541d8b9b36e35bd4b6f56a62d6a5e97f", + "fingerprint": "026f63ffee48400ed53c6a9c8b106063dc3db734a1f32b432460457b294c7d54", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3241,7 +3633,7 @@ "method": "POST", "path": "/admin/webhooks/{webhookId}/disable", "operationId": "adminForceDisableWebhook", - "fingerprint": "09b2e93d9bb1fbb2ada4b97e0ba009a1c98aa8961d55749c2c94644e4e6a8e0c", + "fingerprint": "d724d71c360d348db65ed19fcba6bb3d490bbb47066f2434ba59a237a2afcf63", "typescript": { "status": "excluded", "reason": "This Polymorfa staff route requires a staff dashboard identity and never accepts server credentials.", @@ -3253,7 +3645,7 @@ "method": "POST", "path": "/console/auth/totp", "operationId": "enrollTotpUserProfile", - "fingerprint": "82f001e9c63ce68224b5af6dfa54160f0562807e9eb22f703047755041d8b500", + "fingerprint": "d5398033a8c35902ca9859ea3d91113fdcd2c915c37d614d7f9df9c4f410fd70", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3265,7 +3657,7 @@ "method": "POST", "path": "/console/auth/totp/verify", "operationId": "verifyTotpUserProfile", - "fingerprint": "4eb76608fc9dacbc0f912914fd8733761b4faf2471c3a3230f2970585b627c5a", + "fingerprint": "1cf1d456bdbaea5a16109ac096435e904cdf3980384ade7fb937269efb885e70", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3277,7 +3669,7 @@ "method": "POST", "path": "/console/billing/topup", "operationId": "topUpBilling", - "fingerprint": "e34c9e90589f3c0438feb5c40927c5c326e60e2cb1b426ab8b7e3aaf22689e41", + "fingerprint": "e47039c55b90e869d18ac9e532ae3c9cc5f54566a93f83ff9105d6fc82a0a2f1", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3289,7 +3681,7 @@ "method": "POST", "path": "/console/keys", "operationId": "mintForConsoleApiKey", - "fingerprint": "5c675dc96eb0750abdcfe50df815b528b80a2566138dd3c4b75a5da3bf4153e3", + "fingerprint": "92f75ff85e56262b4dcc1b92d4e01d4c178df928a81acad9d2010cdd87125624", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3299,9 +3691,9 @@ { "family": "platform", "method": "POST", - "path": "/console/notifications/{notificationId}/archive", - "operationId": "archiveNotification", - "fingerprint": "f9635c51afbcc35ab725a862ff1f8d7d519cf7d0a9e81b6b17216913450055ab", + "path": "/console/notifications/archive", + "operationId": "archiveAllNotification", + "fingerprint": "3962904dffe18ddf7844464004ae1bd8d570266117a17822ad6ff0829e35aefe", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3311,9 +3703,9 @@ { "family": "platform", "method": "POST", - "path": "/console/notifications/{notificationId}/read", - "operationId": "markReadNotification", - "fingerprint": "fadd58321a663ea73087c3f78608171baa796c5ac776148fabfd683a1ae06ac5", + "path": "/console/notifications/read", + "operationId": "markAllReadNotification", + "fingerprint": "434ca3ad9e8c7b0b92e4645300d85f462bb2fde2d045338382f50d84def821c0", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3323,9 +3715,9 @@ { "family": "platform", "method": "POST", - "path": "/console/notifications/archive", - "operationId": "archiveAllNotification", - "fingerprint": "d5bc4463530e8041f76ec16b73da4a5119d71ef5a88e43d5e8819c81e5f433a1", + "path": "/console/notifications/test", + "operationId": "sendTestNotificationEmail", + "fingerprint": "772f46209cdcb495e0cf706f3dc627770b20821e7a4f18f52331ade4cf83c33a", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3335,9 +3727,9 @@ { "family": "platform", "method": "POST", - "path": "/console/notifications/read", - "operationId": "markAllReadNotification", - "fingerprint": "234c8f0889c457657a527fcdd9648981414dfb3a0780430f653293d85574018f", + "path": "/console/notifications/{notificationId}/archive", + "operationId": "archiveNotification", + "fingerprint": "7402d99c66e789e0a934414a04605686dc4d8e8a2793679a204e198a7984a92e", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3347,9 +3739,9 @@ { "family": "platform", "method": "POST", - "path": "/console/notifications/test", - "operationId": "sendTestNotificationEmail", - "fingerprint": "8df8e25b344167f8c58c4670faa5c93a0deda32a17553170a06d3861d63e18d3", + "path": "/console/notifications/{notificationId}/read", + "operationId": "markReadNotification", + "fingerprint": "cc1993d123cd72cc68d18fd2c86b70eaf19585e20d8df21297775fbe51383120", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3361,7 +3753,7 @@ "method": "POST", "path": "/console/organization-invitations/{invitationId}/accept", "operationId": "acceptOrganizationInvitation", - "fingerprint": "24e5b1a8d33ffdd3de5254818dcfdfcfc843747f0f5122decd054ee53f5f5d12", + "fingerprint": "342e922c2423567c11ac0beb046f9689a5ca330c611c126d4d766e569eac5748", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3373,7 +3765,7 @@ "method": "POST", "path": "/console/organization/avatar/uploads", "operationId": "generateOrganizationAvatarUploadUrl", - "fingerprint": "3e2c0d152abdcc88afc64ff52d62028d1081da68b022161d1cee46fa342effd6", + "fingerprint": "68a8b1d3525483b064cc54dd94a5d3333e1467e525a04aec1e81ee23c456ceb7", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3385,7 +3777,7 @@ "method": "POST", "path": "/console/organizations/ensure", "operationId": "ensureForCurrentUserOrganization", - "fingerprint": "1c2e34389d45336ec7d821487e7d9868ef68cda0998171e7a249d6b0e30dab9f", + "fingerprint": "e41d146623249ba1a3edf2c7da8cc76bd22128bf760916673dd02bb0d9758aef", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3397,7 +3789,7 @@ "method": "POST", "path": "/console/profile/avatar/uploads", "operationId": "generateUserProfileAvatarUploadUrl", - "fingerprint": "36bfb0f768b47bfe3a4b15aec4a7e5060ec648eacf20e9f17a9dc6d984541903", + "fingerprint": "b3e6898b57e7a530167455abfa70b608f88d29bc55fa286eb48a59ed35b04204", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3409,7 +3801,7 @@ "method": "POST", "path": "/console/profile/ensure", "operationId": "ensureProfileUserProfile", - "fingerprint": "ebe9b146d650decac4b30ab2494d39150f612f91ba8f5e39c1caf4cc5563226b", + "fingerprint": "f5343a52bec5f48d970c259de93b829c1ad71cae982e9d6817d2d4a05bdd1392", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3421,7 +3813,7 @@ "method": "POST", "path": "/console/projects/{projectId}/icon", "operationId": "generateProjectUploadUrl", - "fingerprint": "2cd32d596b851b4720ef5a38a75f9f57b2996f5b18dcaf02bb2da5ee468efa87", + "fingerprint": "0d650459adeadfa442f907df015aa86f2bf88462fcde8f86435e62b6411a8d15", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3431,9 +3823,9 @@ { "family": "platform", "method": "POST", - "path": "/console/teams/{teamId}/delete", - "operationId": "deleteUserProfileTeam", - "fingerprint": "2e560a2d3d51411d62098074c2d18c0b586bcb48a270512a951d09893932df23", + "path": "/console/teams/leave", + "operationId": "leaveTeamUserProfile", + "fingerprint": "cb45b7d4c951f6ef99363c95f70a8133632347132669064d7f979d85d725f58a", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3443,9 +3835,9 @@ { "family": "platform", "method": "POST", - "path": "/console/teams/leave", - "operationId": "leaveTeamUserProfile", - "fingerprint": "2ee2412690ca6ac2379fe9739eec713e9c0ec4f40325ae71912d0579109ab0a4", + "path": "/console/teams/{teamId}/delete", + "operationId": "deleteUserProfileTeam", + "fingerprint": "b9a29e0bf87c13640da3bfd15434d50d1bfc855687a83e9cb2b5e692e46d3e8e", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3457,7 +3849,7 @@ "method": "POST", "path": "/console/webhooks", "operationId": "createWebhook", - "fingerprint": "23466ce79517195946c9b56f3450bd07a0f775a883c67d6b12f8b57332ada9c0", + "fingerprint": "f277bb8e428a0a4e308a64fdefbd71e1a1ae5c7063908ad7e543ccd81469334a", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3469,7 +3861,7 @@ "method": "POST", "path": "/console/widget/logo/org", "operationId": "useOrgLogoWidgetSettings", - "fingerprint": "75869aa902ccb6827e4a5c40a590fda4b4f3c148c3a2ad0f27d8e869f14b0d0d", + "fingerprint": "744be92f4bb16bc6840f7ff78df0dca15e949b06847d6dd6ea8686d315af4b7d", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3481,7 +3873,7 @@ "method": "POST", "path": "/console/widget/logo/project", "operationId": "useProjectIconWidgetSettings", - "fingerprint": "47a2a33aa12946c7e03be08477a4a2d5ff63e6ffa9bdce7f9e1386144bda49b4", + "fingerprint": "fc155d96d96898c6a40505c15d2a0e955e58ae728596029536eafaf16fc9f1d5", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3493,7 +3885,7 @@ "method": "POST", "path": "/console/widget/logo/uploads", "operationId": "generateWidgetSettingsLogoUploadUrl", - "fingerprint": "ee6a45ced39cbdb520030b15bc8f9140a28ff78c92df748c52e1a3985b48ecdf", + "fingerprint": "ffd922c24d4b0ddf1955711e47fd7746b6ad192772628802504bab44a2886f93", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3505,7 +3897,7 @@ "method": "POST", "path": "/v1/audiences", "operationId": "createAudience", - "fingerprint": "d86398c7616c06d22361b587d9f104209abac626a928abe70e0db75315f21248", + "fingerprint": "b6b0ecd9fc2086a2fd2002f36d9cc7bd7d3d8815cd1646a019058551eece7b79", "typescript": { "status": "covered", "method": "PlatformClient.audiences.create" @@ -3516,7 +3908,7 @@ "method": "POST", "path": "/v1/audiences/uploads", "operationId": "createAudienceUploadUrl", - "fingerprint": "46664ac229f2cc30ff2b46acbebd13b5f846fcc51a5a1349d461cfa5dd9e573d", + "fingerprint": "181687aae5b6bf19a47f468db80b99a60186d2a6a0b607be6f3940e3b31ced0c", "typescript": { "status": "covered", "method": "PlatformClient.audiences.createUpload" @@ -3527,7 +3919,7 @@ "method": "POST", "path": "/v1/campaigns", "operationId": "createPlatformCampaign", - "fingerprint": "cbe45541fca28a1139361507914313f69f597e1827b4b39a04920693d38379aa", + "fingerprint": "e1b2ad3a76c285b136e3930a6f0a757b2c53bbd2e80419bc48ca51e4e15d7d4d", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.create" @@ -3538,7 +3930,7 @@ "method": "POST", "path": "/v1/campaigns/{campaignId}/archive", "operationId": "archiveCampaign", - "fingerprint": "7b25f8b0fee06d3b5abf8d7533e8c3589050869d348695fe8a7ce2a651dc8720", + "fingerprint": "7874cacdb5473efcbf5ec60537359dde02f6c8ed107f65464c103c9a0c956c59", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.archive" @@ -3549,7 +3941,7 @@ "method": "POST", "path": "/v1/campaigns/{campaignId}/duplicate", "operationId": "duplicateCampaign", - "fingerprint": "a938db2b4e224d593324a3af88e098205033ab4a8120129130db957e443f5129", + "fingerprint": "980f5484a2c7d11062cfecc4e089728c96d4e020c419f84e995cd8b927d834ca", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.duplicate" @@ -3560,7 +3952,7 @@ "method": "POST", "path": "/v1/campaigns/{campaignId}/launch", "operationId": "launchPlatformCampaign", - "fingerprint": "3c044ffdaad17d7801459768cd868545f7c1dcc11885803f05f13a3aa8b13763", + "fingerprint": "05e3a309a4b13e603a677acedc813bfeb532f1d2f40724955630e3338bbe76f1", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.launch" @@ -3571,7 +3963,7 @@ "method": "POST", "path": "/v1/campaigns/{campaignId}/pause", "operationId": "pausePlatformCampaign", - "fingerprint": "d774b78c26421d27bcbb3bfae14a138db186efac3c93ed8847245ca51e42502f", + "fingerprint": "525a5e4ace1bea4dbd1fa38c5ab4a34f53381556c2351cf2983fc36461d6ff55", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.pause" @@ -3582,7 +3974,7 @@ "method": "POST", "path": "/v1/campaigns/{campaignId}/requeue", "operationId": "requeueFailedCampaign", - "fingerprint": "90a421bd466de36f55321f83216983219ba9e065d99aeeb0778086ddb1b63fee", + "fingerprint": "9ac17764dba881792d291a1637803a072833bf9647046eca3a7e13cad2913bd6", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.requeue" @@ -3593,7 +3985,7 @@ "method": "POST", "path": "/v1/campaigns/{campaignId}/resume", "operationId": "resumePlatformCampaign", - "fingerprint": "3d65dffea539f5f962287df721fcae3b3123ca04b35553a17a7d6d3b6f839e7c", + "fingerprint": "d41ef90149f1305467359427caa89809cc5cb08796dbc3095f7336fb3c60dbcd", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.resume" @@ -3604,7 +3996,7 @@ "method": "POST", "path": "/v1/campaigns/{campaignId}/stop", "operationId": "stopPlatformCampaign", - "fingerprint": "19bf2cd48aae4aea95daf7a3399c4876bee66723a74ad60382b567c569614e69", + "fingerprint": "e7fafb4cf9730d92363f799dcb536b89c4d5a42a07fc5d5765e4df622258d2c8", "typescript": { "status": "covered", "method": "PlatformClient.campaigns.stop" @@ -3665,12 +4057,24 @@ "method": "PlatformClient.customers.restore" } }, + { + "family": "platform", + "method": "POST", + "path": "/v1/events/{eventId}/replays", + "operationId": "replayOrganizationEvent", + "fingerprint": "eba8005fe5674692bf863d4ae911fe03819398729b8369a4448189b7fe129e0d", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "POST", "path": "/v1/flows", "operationId": "createFlow", - "fingerprint": "413a774608f29e1c32ae00b761506f038e958c70aaea25ae23240ea2b45ea692", + "fingerprint": "88b3342941cdb519d5725b10edad1faa124b74785571b88fc6df7248a4de805e", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", @@ -3682,7 +4086,7 @@ "method": "POST", "path": "/v1/incidents/{incidentId}/acknowledge", "operationId": "acknowledgeSecurityIncident", - "fingerprint": "a5366ffa31645927c2cdf22b8cb482f16c1b328592ccd7a2e7a1dc9ba5239c7f", + "fingerprint": "e53ce0bd9846707155d28030c572ca4ced5b494c62d242274b18f579f653350a", "typescript": { "status": "covered", "method": "PlatformClient.securityIncidents.acknowledge" @@ -3693,7 +4097,7 @@ "method": "POST", "path": "/v1/media/uploads", "operationId": "createMediaUploadUrl", - "fingerprint": "29f21fbe7dc3e1c291f6c8d771e9cb5ee3cb3b30eabdd1099897111dd422f6da", + "fingerprint": "1b72058d61aebe5d619aee99e5547b1de93d3f90c8232805cadfdbe8d944e750", "typescript": { "status": "covered", "method": "PlatformClient.media.createUpload" @@ -3704,19 +4108,31 @@ "method": "POST", "path": "/v1/members/invites", "operationId": "inviteMember", - "fingerprint": "29bf6c458801a53ef5188da13122013563995a9188517172aaa6b5d603c9a391", + "fingerprint": "2762d14d85c446fd0d0144bfefe588590b4cbf93fd5bcb75a2e6b50a12c9223e", "typescript": { "status": "excluded", "reason": "This dashboard-only invitation accepts ConsoleSession authentication and rejects the organization server API key used by PlatformClient.", "milestone": "credential-boundary" } }, + { + "family": "platform", + "method": "POST", + "path": "/v1/operations/{operationId}/cancel", + "operationId": "cancelOrganizationOperation", + "fingerprint": "a7a33415d944a39d49dc5d7907b498c9985a2c9967fe71975fe76b6bf5061c23", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "POST", "path": "/v1/optouts", "operationId": "createOptOut", - "fingerprint": "4872a99a6210546ffd0a1e49574935cd90d905e7bdcda12101fd39d63185bd05", + "fingerprint": "fc46f65b20f6df48dc7abd557fa6ab8c63d0381f8b85f3810c813387b6f3816b", "typescript": { "status": "covered", "method": "PlatformClient.optOuts.create" @@ -3727,7 +4143,7 @@ "method": "POST", "path": "/v1/optouts/batch", "operationId": "createOptOuts", - "fingerprint": "1e35d121e42a7a602cc585774e8792db92ea2f4ad4378f76ca2b41f06bd2ee0a", + "fingerprint": "af7de5f6d5c838aa65da25ddfa1376ac38cba86ff9ba05b79b7d695eb72134ee", "typescript": { "status": "covered", "method": "PlatformClient.optOuts.createBatch" @@ -3738,7 +4154,7 @@ "method": "POST", "path": "/v1/projects", "operationId": "createProject", - "fingerprint": "4483d314cabf80e61f321a873a186b5003ea0dc38dc979f449f77a1118fed57f", + "fingerprint": "61809f2781d79bffe3ec2c4ef23a9dc04b0ad1df60cc0f9823390f15d24a6e93", "typescript": { "status": "covered", "method": "PlatformClient.projects.create" @@ -3755,12 +4171,36 @@ "method": "PlatformClient.customers.enable" } }, + { + "family": "platform", + "method": "POST", + "path": "/v1/projects/{projectId}/events/{eventId}/replays", + "operationId": "replayProjectEvent", + "fingerprint": "2339cad5119c3d95b56f85e05d252373c32197a75a739ce174f8ce8e7f0b9af4", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/projects/{projectId}/operations/{operationId}/cancel", + "operationId": "cancelProjectOperation", + "fingerprint": "368d5301a8dc02d5348208e3b6d1789adb8cb11867e2cb32511e90efdbc7d3e2", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "POST", "path": "/v1/projects/{projectId}/production-enrollments/{operationId}/approve", "operationId": "approveProductionEnrollment", - "fingerprint": "b66b919fd3eeee77b5fc24ebd4404bcd55bc40cf7d6df0aa2a2606d12b260843", + "fingerprint": "2895433633925a3c7cf2486e569584c47f80c14265202df7c5538274202ab154", "typescript": { "status": "covered", "method": "PlatformClient.projects.approveProductionEnrollment" @@ -3771,7 +4211,7 @@ "method": "POST", "path": "/v1/projects/{projectId}/production-enrollments/{operationId}/cancel", "operationId": "cancelProductionEnrollment", - "fingerprint": "2aa59e8a082526f895d8cbb8f45a15ec61d45f9fe593d38712597d6522422924", + "fingerprint": "3ed9c7d417bc287059160de229e698c11feea210c2c5300bc6dcf55a1bfaf662", "typescript": { "status": "covered", "method": "PlatformClient.projects.cancelProductionEnrollment" @@ -3782,7 +4222,7 @@ "method": "POST", "path": "/v1/projects/{projectId}/promote", "operationId": "requestProductionEnrollment", - "fingerprint": "6588e169b039b3f441f7ac3ee6299e405b704296944505931efa44a8279db248", + "fingerprint": "9ef810d6c24aeca305cd3c325af2354cea607798e82247a3e95a0ee0f97425bf", "typescript": { "status": "covered", "method": "PlatformClient.projects.requestProductionEnrollment" @@ -3791,12 +4231,49 @@ { "family": "platform", "method": "POST", - "path": "/v1/sessions/{sessionId}/stop", - "operationId": "stopPlatformSession", - "fingerprint": "ab9c3026a29516462fd223e2aeba20edd6d8447e6bbf5d871e753f2a52df606b", + "path": "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}/retry", + "operationId": "retryProjectWebhookDelivery", + "fingerprint": "38edb41227ef675157277de18e0d9bf7f6639677818f8c2a8f69e280b45214df", "typescript": { - "status": "covered", - "method": "PlatformClient.sessions.stop" + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/projects/{projectId}/webhooks", + "operationId": "createProjectWebhook", + "fingerprint": "1a84d699c6a8fc774bfe85b4eaaddbd31addd61809c403bbaa010a6dd6021a59", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/projects/{projectId}/webhooks/{webhookId}/secret-rotations", + "operationId": "rotateProjectWebhookSecret", + "fingerprint": "b6db4b2f05eacb82576c3fb6a72cbecd7bcefbd1cb605308822ff5bd7a5b050a", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/projects/{projectId}/webhooks/{webhookId}/tests", + "operationId": "testProjectWebhook", + "fingerprint": "41f3f02723d73d84ee76a503e44a4ac4320f53d40c728577379eaf6016455376", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" } }, { @@ -3804,7 +4281,7 @@ "method": "POST", "path": "/v1/sessions/delete", "operationId": "deleteSessions", - "fingerprint": "2150d1fe457eebf3faddf85219ef4a18bc8fcb294210a6d4eb353aa3193c19ed", + "fingerprint": "7729d4347e0be6d3cbdfc9b092449fe2b9203126eefef68dff93091fb6acab10", "typescript": { "status": "covered", "method": "PlatformClient.sessions.deleteMany" @@ -3815,7 +4292,7 @@ "method": "POST", "path": "/v1/sessions/stop", "operationId": "stopSessions", - "fingerprint": "040ec9cbadb4c095278cecae967f43c9d8c6fd296b0724671e38331b89d68b74", + "fingerprint": "30243b9f92c42b9d34e62970ef42f7a275594bf9bc87c313858162bd6b146cb2", "typescript": { "status": "covered", "method": "PlatformClient.sessions.stopMany" @@ -3826,30 +4303,101 @@ "method": "POST", "path": "/v1/sessions/testing", "operationId": "createTestingSession", - "fingerprint": "6164760a9b74570debfdc06164473427d692857d600f340b53d54e95592c4482", + "fingerprint": "64cb2649c764e746de9cc02c2ac90b18dee5c0d1c44db5f5e4a3e487f8d49aef", "typescript": { "status": "covered", "method": "PlatformClient.sessions.createTesting" } }, + { + "family": "platform", + "method": "POST", + "path": "/v1/sessions/{sessionId}/start", + "operationId": "startPlatformSession", + "fingerprint": "6c00282c3715a7b87ea17c2add03033ad5fa03a8dd6b12d709fa03aecae62157", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/sessions/{sessionId}/stop", + "operationId": "stopPlatformSession", + "fingerprint": "066950a07954551deda3e02880db38a88cb47f8b3466d69a243fff357dc53d8c", + "typescript": { + "status": "covered", + "method": "PlatformClient.sessions.stop" + } + }, { "family": "platform", "method": "POST", "path": "/v1/templates", "operationId": "createMessageTemplate", - "fingerprint": "b600a82bba354e631778da9b1024152a5f14b7bded099a306c1e8a6596412243", + "fingerprint": "5a6b8831c17b9a824f1d20f8de747176199e2b88828f841ccbdbe16ee3dd49da", "typescript": { "status": "excluded", "reason": "This dashboard-only route does not accept the organization server API key used by PlatformClient.", "milestone": "credential-boundary" } }, + { + "family": "platform", + "method": "POST", + "path": "/v1/webhook-deliveries/{deliveryId}/retry", + "operationId": "retryOrganizationWebhookDelivery", + "fingerprint": "3c4842a2d3eff7bc2d5d4c96cb93d44edd4466eb98ef9d0919489f66574c2957", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/webhooks", + "operationId": "createOrganizationWebhook", + "fingerprint": "310c07a9659ec0c5f399ecc795e31d9210387a547a8e5db59b33887c1ebf8d4f", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/webhooks/{webhookId}/secret-rotations", + "operationId": "rotateOrganizationWebhookSecret", + "fingerprint": "eb920d819c359f828ebef495c874215afc8d0a5b5cf0e5a5f9b02c164cf4fd00", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, + { + "family": "platform", + "method": "POST", + "path": "/v1/webhooks/{webhookId}/tests", + "operationId": "testOrganizationWebhook", + "fingerprint": "b65a74eb4c4e71a057a0d0176eeca1aa3fcc67fe1b89cdac4917e3f5e2660abb", + "typescript": { + "status": "missing", + "reason": "Operation is absent from the coverage ledger.", + "milestone": "coverage-triage" + } + }, { "family": "platform", "method": "PUT", "path": "/admin/pricing/{tier}", "operationId": "adminUpdatePricing", - "fingerprint": "864738a22657a6f413ebf1a3c808925f9c1846c8afe76670bb7d1917609530c0", + "fingerprint": "5b0e99a7aea998fd8b86838992f36690b5e21cc047d820fc061973c5e113c18a", "typescript": { "status": "missing", "reason": "No handwritten TypeScript resource method exists for this operation yet.", @@ -3861,7 +4409,7 @@ "method": "PUT", "path": "/console/insights/preferences", "operationId": "updateInsightPreferences", - "fingerprint": "8880397b1c2c9cb43fcb736fddce503db77ca5f78aeae96f60ea3fc427b682e4", + "fingerprint": "5eb287702103b8c6f1cb595d7752e9f76eb15df199c9d12bd70a4ffdcabf7022", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3873,7 +4421,7 @@ "method": "PUT", "path": "/console/notifications/preferences", "operationId": "updateNotificationPreferences", - "fingerprint": "55542ed453866e508a2849150579fbd8f7ad8687f72b6c2f1087db262bcf7c29", + "fingerprint": "8b51b88a2c37347ae59bf7236b59a66f1b342f3216c4c5699a7fdd2d002ae675", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3885,7 +4433,7 @@ "method": "PUT", "path": "/console/organization/avatar", "operationId": "saveAvatarOrganization", - "fingerprint": "36f51bd023d7eee2470d00ea89d535b76f8d4a9bda58d04d0c9f359195c654e0", + "fingerprint": "0d2f09465bff752473d48f26728bd88ff4a608834fae3e6f67287492a3f3fbd8", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3897,7 +4445,7 @@ "method": "PUT", "path": "/console/profile/avatar", "operationId": "saveAvatarUserProfile", - "fingerprint": "bc10246081a092f1f2505999efe0038708fe4e89194f31f1857043ea7d3834ac", + "fingerprint": "67bd212cd391ac5fdc57e95499d418395af2319d72a31e2ecaa96ef68361b722", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3909,7 +4457,7 @@ "method": "PUT", "path": "/console/widget/logo", "operationId": "attachLogoWidgetSettings", - "fingerprint": "e56adb74a4d3077cceca942ba2f9dfe1d1bac71f0750d556f8590d08cda16de7", + "fingerprint": "8e6619cc3b0abdd1a9d08babfe12fdc1bd0cc209d79ffdcf02e959150bb16d4d", "typescript": { "status": "excluded", "reason": "This dashboard or staff route does not accept the server credential used by the TypeScript server SDK.", @@ -3921,7 +4469,7 @@ "method": "PUT", "path": "/v1/widget", "operationId": "updateWidgetSettings", - "fingerprint": "61b7ddd6c3953b63b1dc9f163aba1cc716b5ba86b4c5e5bf8f3fb53b15127be1", + "fingerprint": "acfcf19dc22cdb5d392c574adca0019ced960d222be04335072f72587c7e006f", "typescript": { "status": "covered", "method": "PlatformClient.widgetSettings.update" diff --git a/contracts/openapi.messaging.json b/contracts/openapi.messaging.json index 332e8bf0..70d6662f 100644 --- a/contracts/openapi.messaging.json +++ b/contracts/openapi.messaging.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Polymorfa WhatsApp API", - "version": "1.0.0", + "version": "openapi-dump", "description": "Public Polymorfa REST API for sessions, messaging, media, webhooks, campaigns, templates, client tokens, widgets, and bridge routing.", "contact": { "name": "Polymorfa API", @@ -24,16 +24,19 @@ "JIDRef": { "type": "object", "properties": { + "id": { + "type": "string" + }, "phoneNumber": { "type": "string" }, + "username": { + "type": "string" + }, "lid": { "type": "string", "deprecated": true }, - "id": { - "type": "string" - }, "mode": { "type": "string", "enum": [ @@ -41,9 +44,6 @@ "lid" ], "deprecated": true - }, - "username": { - "type": "string" } }, "required": [ @@ -144,6 +144,29 @@ "revokedId": { "type": "string" }, + "nativeFlowResponse": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "paramsJson": { + "type": "string", + "minLength": 1, + "maxLength": 262144 + }, + "version": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "name", + "paramsJson" + ] + }, "media": { "type": "string" }, @@ -164,6 +187,37 @@ }, "unavailableReason": { "type": "string" + } + }, + "required": [ + "id", + "from", + "sender", + "fromMe", + "timestamp", + "pushName", + "isGroup", + "type" + ] + }, + "CloudMessagePayload": { + "type": "object", + "properties": { + "messageId": { + "type": "string" + }, + "from": { + "type": "string", + "description": "Meta wa_id. This is not a LID-backed user ID." + }, + "timestamp": { + "type": "string" + }, + "type": { + "type": "string" + }, + "senderName": { + "type": "string" }, "nativeFlowResponse": { "type": "object", @@ -187,19 +241,29 @@ "name", "paramsJson" ] + }, + "interactive": { + "type": "object", + "additionalProperties": {} } }, "required": [ - "id", + "messageId", "from", - "sender", - "fromMe", "timestamp", - "pushName", - "isGroup", "type" ] }, + "MessageReceivedPayload": { + "oneOf": [ + { + "$ref": "#/components/schemas/MessagePayload" + }, + { + "$ref": "#/components/schemas/CloudMessagePayload" + } + ] + }, "MessageReceivedEvent": { "type": "object", "properties": { @@ -1013,13 +1077,19 @@ "ContactUpdatePayload": { "type": "object", "properties": { - "phoneNumber": { + "id": { "type": "string" }, "lid": { "type": "string", "deprecated": true }, + "phoneNumber": { + "type": "string" + }, + "username": { + "type": "string" + }, "fullName": { "type": "string" }, @@ -1043,12 +1113,6 @@ }, "pictureRemoved": { "type": "boolean" - }, - "id": { - "type": "string" - }, - "username": { - "type": "string" } }, "required": [ @@ -1321,11 +1385,17 @@ }, "callId": { "type": "string" + }, + "hasVideo": { + "type": "boolean", + "description": "Whether the offered call advertised video.", + "example": false } }, "required": [ "from", - "callId" + "callId", + "hasVideo" ] }, "CallReceivedEvent": { @@ -1501,6 +1571,182 @@ "payload" ] }, + "CallEndedPayload": { + "type": "object", + "properties": { + "from": { + "$ref": "#/components/schemas/JIDRef" + }, + "callId": { + "type": "string" + }, + "durationSeconds": { + "type": "integer", + "description": "Total connected duration in seconds (0 if never answered).", + "example": 42 + }, + "reason": { + "type": "string", + "description": "Termination reason: user_hangup | timeout | lost_connection | rejected | capacity.", + "example": "user_hangup" + }, + "direction": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ], + "description": "Call direction.", + "example": "inbound" + }, + "hadVideo": { + "type": "boolean", + "description": "Whether the call carried video.", + "example": false + } + }, + "required": [ + "from", + "callId", + "durationSeconds", + "reason", + "direction", + "hadVideo" + ] + }, + "CallEndedEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique event identifier" + }, + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CallEndedPayload" + } + }, + "required": [ + "id", + "session", + "timestamp", + "event", + "payload" + ] + }, + "CallTelemetryPayload": { + "type": "object", + "properties": { + "callId": { + "type": "string" + }, + "setupMs": { + "type": "integer", + "description": "Time from offer to media_ready, in milliseconds.", + "example": 850 + }, + "ringMs": { + "type": "integer", + "description": "Time spent ringing before accept, in milliseconds.", + "example": 3200 + }, + "durationSeconds": { + "type": "integer", + "description": "Total connected duration in seconds.", + "example": 42 + }, + "terminateReason": { + "type": "string", + "description": "Termination reason: user_hangup | timeout | lost_connection | rejected | capacity.", + "example": "user_hangup" + }, + "codec": { + "type": "string", + "description": "Negotiated audio codec's bare subtype; empty when no codec stats.", + "example": "opus" + }, + "jitterMs": { + "type": "number", + "description": "Inbound RTP jitter for the audio stream, in milliseconds.", + "example": 12.5 + }, + "packetsLost": { + "type": "integer", + "description": "Cumulative inbound RTP packets lost for the audio stream.", + "example": 3 + }, + "rttMs": { + "type": "number", + "description": "Estimated round-trip time from the remote-inbound report, in milliseconds.", + "example": 48.2 + }, + "recvKbps": { + "type": "number", + "description": "Total inbound volume across all RTP streams, in kilobits (cumulative).", + "example": 2048 + }, + "sendKbps": { + "type": "number", + "description": "Total outbound volume across all RTP streams, in kilobits (cumulative).", + "example": 2011 + } + }, + "required": [ + "callId", + "setupMs", + "ringMs", + "durationSeconds", + "terminateReason", + "codec", + "jitterMs", + "packetsLost", + "rttMs", + "recvKbps", + "sendKbps" + ] + }, + "CallTelemetryEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique event identifier" + }, + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CallTelemetryPayload" + } + }, + "required": [ + "id", + "session", + "timestamp", + "event", + "payload" + ] + }, "NewsletterUpdatePayload": { "type": "object", "properties": { @@ -1553,24 +1799,25 @@ "BlocklistChange": { "type": "object", "properties": { - "phoneNumber": { + "id": { "type": "string" }, "lid": { "type": "string", "deprecated": true }, - "action": { + "phoneNumber": { "type": "string" }, - "id": { + "username": { "type": "string" }, - "username": { + "action": { "type": "string" } }, "required": [ + "id", "action" ] }, @@ -1720,32 +1967,113 @@ "payload" ] }, - "HistorySyncPayload": { + "BusinessQuickReplyUpdatePayload": { "type": "object", "properties": { - "messageId": { + "id": { "type": "string" }, - "originalMessageId": { + "shortcut": { "type": "string" }, - "mode": { - "type": "string", - "enum": [ - "deliver" - ] - }, - "syncType": { + "message": { "type": "string" }, - "chunkOrder": { - "type": "integer", - "minimum": 0 + "keywords": { + "type": "array", + "items": { + "type": "string" + } }, - "progress": { + "count": { "type": "integer", - "minimum": 0, - "maximum": 100 + "minimum": 0 + }, + "deleted": { + "type": "boolean" + }, + "associatedLabelIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "observedAt": { + "type": "integer" + }, + "fromFullSync": { + "type": "boolean" + } + }, + "required": [ + "id", + "shortcut", + "message", + "keywords", + "count", + "deleted", + "associatedLabelIds", + "observedAt", + "fromFullSync" + ] + }, + "BusinessQuickReplyUpdateEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique event identifier" + }, + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/BusinessQuickReplyUpdatePayload" + } + }, + "required": [ + "id", + "session", + "timestamp", + "event", + "payload" + ] + }, + "HistorySyncPayload": { + "type": "object", + "properties": { + "messageId": { + "type": "string" + }, + "originalMessageId": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": [ + "deliver" + ] + }, + "syncType": { + "type": "string" + }, + "chunkOrder": { + "type": "integer", + "minimum": 0 + }, + "progress": { + "type": "integer", + "minimum": 0, + "maximum": 100 }, "fileLength": { "type": "integer", @@ -1872,972 +2200,1396 @@ "payload" ] }, - "PingResponse": { + "CustomerEnabledPayload": { "type": "object", "properties": { - "status": { + "eventId": { "type": "string", - "example": "ok" - } - }, - "required": [ - "status" - ] - }, - "HealthCheck": { - "type": "object", - "properties": { - "status": { + "format": "uuid" + }, + "occurredAt": { "type": "string", - "example": "healthy" + "format": "date-time" }, - "error": { - "type": "string" - } - }, - "required": [ - "status" - ] - }, - "HealthResponse": { - "type": "object", - "properties": { - "status": { + "organizationId": { "type": "string", - "example": "healthy" + "format": "uuid" }, - "checks": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/HealthCheck" - } + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", + "enum": [ + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" + ] + }, + "migratedNumberCount": { + "type": "integer", + "minimum": 0 } }, "required": [ - "status", - "checks" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "migratedNumberCount" + ], + "additionalProperties": false }, - "VersionResponse": { + "CustomerEnabledEvent": { "type": "object", "properties": { - "version": { - "type": "string" + "id": { + "type": "string", + "description": "Unique event identifier" }, - "buildTime": { - "type": "string" + "session": { + "type": "string", + "description": "Session that produced this event" }, - "env": { - "type": "string" + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" }, - "apiVersion": { - "type": "string" + "event": { + "type": "string", + "description": "Event type" }, - "minSupportedVersion": { - "type": "string" + "payload": { + "$ref": "#/components/schemas/CustomerEnabledPayload" } }, "required": [ - "version", - "buildTime", - "env", - "apiVersion", - "minSupportedVersion" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "StatusResponse": { + "CustomerCreatedPayload": { "type": "object", "properties": { - "status": { - "type": "string" + "eventId": { + "type": "string", + "format": "uuid" }, - "uptime": { - "type": "string" + "occurredAt": { + "type": "string", + "format": "date-time" }, - "version": { - "type": "string" + "organizationId": { + "type": "string", + "format": "uuid" }, - "env": { - "type": "string" - } - }, - "required": [ - "status", - "uptime", - "version", - "env" - ] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "error": { + "projectId": { "type": "string", - "description": "Human-readable error message" + "format": "uuid" }, - "docs": { + "customerId": { "type": "string", - "description": "URL to the documentation for this error" + "format": "uuid" + }, + "actorKind": { + "type": "string", + "enum": [ + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" + ] } }, "required": [ - "error" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind" + ], + "additionalProperties": false }, - "SuccessResponse": { + "CustomerCreatedEvent": { "type": "object", "properties": { - "success": { - "type": "boolean" + "id": { + "type": "string", + "description": "Unique event identifier" }, - "message": { - "type": "string" + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerCreatedPayload" } }, "required": [ - "success" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "ClientToken": { + "CustomerUpdatedPayload": { "type": "object", "properties": { - "token": { + "eventId": { "type": "string", - "description": "Client token with pmfa_ct_ prefix" + "format": "uuid" }, - "expiresAt": { - "type": "string" - } - }, - "required": [ - "token", - "expiresAt" - ] - }, - "MintClientTokenResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "$ref": "#/components/schemas/ClientToken" + "fields": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "name", + "phone", + "externalCustomerId" + ] + }, + "minItems": 1, + "maxItems": 3 } }, "required": [ - "success", - "data" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "fields" + ], + "additionalProperties": false }, - "MintClientTokenRequest": { + "CustomerUpdatedEvent": { "type": "object", "properties": { + "id": { + "type": "string", + "description": "Unique event identifier" + }, "session": { "type": "string", - "minLength": 1 + "description": "Session that produced this event" }, - "ephemeralId": { + "timestamp": { "type": "string", - "minLength": 1 + "description": "ISO 8601 timestamp" }, - "ttlSeconds": { - "type": "integer" + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerUpdatedPayload" } }, "required": [ + "id", "session", - "ephemeralId" - ] + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "ClientRules": { + "CustomerArchivingPayload": { "type": "object", "properties": { - "actions": { - "type": "array", - "items": { - "type": "string" - } + "eventId": { + "type": "string", + "format": "uuid" }, - "recipientMode": { - "type": "string" + "occurredAt": { + "type": "string", + "format": "date-time" }, - "verifiedJids": { - "type": "array", - "items": { - "type": "string" - } + "organizationId": { + "type": "string", + "format": "uuid" }, - "rateLimits": { - "type": "object", - "properties": { - "perMinute": { - "type": "integer", - "minimum": 0 - }, - "perDay": { - "type": "integer", - "minimum": 0 - } - } - } - }, - "required": [ - "actions", - "recipientMode" - ] - }, - "GetClientRulesResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "$ref": "#/components/schemas/ClientRules" + "blockingNumberCount": { + "type": "integer", + "minimum": 0 + }, + "revokedPairingLinkCount": { + "type": "integer", + "minimum": 0 } }, "required": [ - "success", - "data" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "blockingNumberCount", + "revokedPairingLinkCount" + ], + "additionalProperties": false }, - "SetClientRulesRequest": { + "CustomerArchivingEvent": { "type": "object", "properties": { - "recipientMode": { - "type": "string" - }, - "allowedActions": { + "id": { "type": "string", - "default": "" + "description": "Unique event identifier" }, - "rateLimit": { - "type": "integer", - "default": 0 + "session": { + "type": "string", + "description": "Session that produced this event" }, - "maxDaily": { - "type": "integer", - "default": 0 + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" }, - "allowedOrigins": { + "event": { "type": "string", - "default": "" + "description": "Event type" }, - "enabled": { - "type": "boolean" + "payload": { + "$ref": "#/components/schemas/CustomerArchivingPayload" } }, "required": [ - "recipientMode", - "enabled" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "Operation": { + "CustomerArchivedPayload": { "type": "object", "properties": { - "id": { + "eventId": { "type": "string", "format": "uuid" }, - "kind": { - "type": "string" - }, - "resourceType": { - "type": "string" + "occurredAt": { + "type": "string", + "format": "date-time" }, - "resourceId": { - "type": "string" + "organizationId": { + "type": "string", + "format": "uuid" }, "projectId": { "type": "string", - "nullable": true, "format": "uuid" }, - "status": { + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { "type": "string", "enum": [ - "pending", - "running", - "action_required", - "succeeded", - "failed", - "cancelled" + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] - }, - "progressCode": { + } + }, + "required": [ + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind" + ], + "additionalProperties": false + }, + "CustomerArchivedEvent": { + "type": "object", + "properties": { + "id": { "type": "string", - "nullable": true + "description": "Unique event identifier" }, - "failureCode": { + "session": { "type": "string", - "nullable": true + "description": "Session that produced this event" }, - "createdAt": { + "timestamp": { "type": "string", - "format": "date-time" + "description": "ISO 8601 timestamp" }, - "updatedAt": { + "event": { "type": "string", - "format": "date-time" + "description": "Event type" }, - "completedAt": { - "type": "string", - "nullable": true, - "format": "date-time" + "payload": { + "$ref": "#/components/schemas/CustomerArchivedPayload" } }, "required": [ "id", - "kind", - "resourceType", - "resourceId", - "projectId", - "status", - "progressCode", - "failureCode", - "createdAt", - "updatedAt", - "completedAt" - ] + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "Session": { + "CustomerRestoredPayload": { "type": "object", "properties": { - "sessionId": { + "eventId": { "type": "string", - "description": "Canonical per-org session identifier. Use this in /api/sessions/{session}/* paths." - }, - "name": { - "type": "string" + "format": "uuid" }, - "tenantId": { - "type": "string" - }, - "connection": { + "occurredAt": { "type": "string", - "enum": [ - "linked_device", - "cloud_api" - ], - "description": "How the session connects to WhatsApp. 'linked_device' pairs as a WhatsApp linked device; 'cloud_api' uses the Meta Cloud API. Immutable after create." + "format": "date-time" }, - "testMode": { - "type": "boolean", - "description": "Whether this session was created as a simulated testing number." + "organizationId": { + "type": "string", + "format": "uuid" }, - "status": { + "projectId": { "type": "string", - "description": "Current status: CONNECTING, CONNECTED, or DISCONNECTED" + "format": "uuid" }, - "statusReason": { - "type": "string" + "customerId": { + "type": "string", + "format": "uuid" }, - "runnerId": { - "type": "string" + "actorKind": { + "type": "string", + "enum": [ + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" + ] + } + }, + "required": [ + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind" + ], + "additionalProperties": false + }, + "CustomerRestoredEvent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique event identifier" }, - "proxy": { - "type": "string" + "session": { + "type": "string", + "description": "Session that produced this event" }, - "config": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" }, - "createdAt": { - "type": "string" + "event": { + "type": "string", + "description": "Event type" }, - "updatedAt": { - "type": "string" + "payload": { + "$ref": "#/components/schemas/CustomerRestoredPayload" } }, "required": [ - "name", - "tenantId", - "connection", - "testMode", - "status", - "config", - "createdAt", - "updatedAt" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "ListSessionsResponse": { + "CustomerPairingLinkCreatedPayload": { "type": "object", "properties": { - "success": { - "type": "boolean", + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" - } + "pairingLinkId": { + "type": "string", + "format": "uuid" } }, "required": [ - "success", - "data" - ] - }, - "SessionOperation": { - "allOf": [ - { - "$ref": "#/components/schemas/Session" - }, - { - "type": "object", - "properties": { - "operationId": { - "type": "string", - "format": "uuid" - } - } - } - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "pairingLinkId" + ], + "additionalProperties": false }, - "CreateSessionResponse": { + "CustomerPairingLinkCreatedEvent": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "id": { + "type": "string", + "description": "Unique event identifier" }, - "data": { - "$ref": "#/components/schemas/SessionOperation" + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerPairingLinkCreatedPayload" } }, "required": [ - "success", - "data" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "CloudApiCredentials": { + "CustomerPairingLinkOpenedPayload": { "type": "object", "properties": { - "phoneNumberId": { + "eventId": { "type": "string", - "minLength": 1 + "format": "uuid" }, - "wabaId": { + "occurredAt": { "type": "string", - "minLength": 1 + "format": "date-time" }, - "businessAccountId": { - "type": "string" + "organizationId": { + "type": "string", + "format": "uuid" }, - "appId": { + "projectId": { "type": "string", - "minLength": 1 + "format": "uuid" }, - "appSecret": { + "customerId": { "type": "string", - "minLength": 1 + "format": "uuid" }, - "systemUserToken": { + "actorKind": { "type": "string", - "minLength": 1 + "enum": [ + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" + ] }, - "webhookVerifyToken": { + "pairingLinkId": { "type": "string", - "minLength": 1 + "format": "uuid" } }, "required": [ - "phoneNumberId", - "wabaId", - "appId", - "appSecret", - "systemUserToken", - "webhookVerifyToken" + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "pairingLinkId" ], - "description": "Meta credentials. Required when connection='cloud_api'. Encrypted at rest before storage." + "additionalProperties": false }, - "HistorySyncPolicy": { + "CustomerPairingLinkOpenedEvent": { "type": "object", "properties": { - "mode": { + "id": { "type": "string", - "enum": [ - "metadata_only", - "deliver" - ], - "default": "metadata_only" + "description": "Unique event identifier" }, - "requestFull": { - "type": "boolean", - "default": false + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerPairingLinkOpenedPayload" } }, + "required": [ + "id", + "session", + "timestamp", + "event", + "payload" + ], "additionalProperties": false }, - "CreateSessionRequest": { + "CustomerPairingLinkConnectedPayload": { "type": "object", "properties": { - "projectId": { + "eventId": { "type": "string", - "minLength": 1, - "description": "Project id the session belongs to. Required." + "format": "uuid" }, - "sessionId": { + "occurredAt": { "type": "string", - "minLength": 1, - "description": "Canonical per-org session identifier (1-64 alphanumeric chars, dots, hyphens, underscores). Caller-supplied or generated; returned in the response. This is the id used in all /api/sessions/{session}/* paths." + "format": "date-time" }, - "name": { + "organizationId": { "type": "string", - "minLength": 1, - "description": "Optional human-friendly label for the session. Not an id." + "format": "uuid" }, - "start": { - "type": "boolean" + "projectId": { + "type": "string", + "format": "uuid" }, - "connection": { + "customerId": { "type": "string", - "enum": [ - "linked_device", - "cloud_api" - ], - "description": "How the session connects to WhatsApp. 'linked_device' (default) pairs as a WhatsApp linked device; 'cloud_api' uses the Meta Cloud API and requires the `cloudApi` creds block. Immutable after create." + "format": "uuid" }, - "testMode": { - "type": "boolean", - "description": "Create a simulated testing number (behaves like a real session via the simulated universe). Defaults to false." - }, - "bartenderMode": { + "actorKind": { "type": "string", "enum": [ - "magic", - "passthrough", - "passthrough_plus" - ], - "description": "Bartender privacy tier. Defaults to 'magic'. Mutable via the handoff ceremony at POST /api/sessions/{name}/handoff-keys + /handoff-import. 'magic' — Polymorfa holds the Signal session; full message visibility. 'passthrough' — Polymorfa holds noise creds only; neither messages nor app state are decryptable on Polymorfa's side. 'passthrough_plus' — Track P + customer has uploaded AppStateSyncKeys for selected collections (contacts, labels, mute/archive, blocklist), so Polymorfa sees metadata mutations but never message content." - }, - "cloudApi": { - "$ref": "#/components/schemas/CloudApiCredentials" + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" + ] }, - "config": { - "type": "object", - "additionalProperties": true + "pairingLinkId": { + "type": "string", + "format": "uuid" }, - "historySync": { - "$ref": "#/components/schemas/HistorySyncPolicy" + "sessionId": { + "type": "string", + "format": "uuid" } }, "required": [ - "projectId" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "pairingLinkId", + "sessionId" + ], + "additionalProperties": false }, - "GetSessionResponse": { + "CustomerPairingLinkConnectedEvent": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "id": { + "type": "string", + "description": "Unique event identifier" }, - "data": { - "$ref": "#/components/schemas/Session" + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerPairingLinkConnectedPayload" } }, "required": [ - "success", - "data" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "UpdateSessionResponse": { + "CustomerPairingLinkFailedPayload": { "type": "object", "properties": { - "success": { - "type": "boolean", + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "$ref": "#/components/schemas/Session" + "pairingLinkId": { + "type": "string", + "format": "uuid" + }, + "errorCode": { + "type": "string", + "pattern": "^[a-z0-9_]{1,64}$" } }, "required": [ - "success", - "data" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "pairingLinkId", + "errorCode" + ], + "additionalProperties": false }, - "UpdateSessionRequest": { + "CustomerPairingLinkFailedEvent": { "type": "object", "properties": { - "config": { - "type": "object", - "additionalProperties": true + "id": { + "type": "string", + "description": "Unique event identifier" }, - "historySync": { - "$ref": "#/components/schemas/HistorySyncPolicy" + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerPairingLinkFailedPayload" } - } + }, + "required": [ + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "OperationAccepted": { + "CustomerPairingLinkExpiredPayload": { "type": "object", "properties": { - "success": { - "type": "boolean", + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "message": { - "type": "string" - }, - "operationId": { + "pairingLinkId": { "type": "string", "format": "uuid" } }, "required": [ - "success", - "message", - "operationId" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "pairingLinkId" + ], + "additionalProperties": false }, - "WhatsAppAccount": { + "CustomerPairingLinkExpiredEvent": { "type": "object", "properties": { - "lid": { - "type": "string" - }, - "phoneNumber": { - "type": "string" + "id": { + "type": "string", + "description": "Unique event identifier" }, - "pushName": { - "type": "string" + "session": { + "type": "string", + "description": "Session that produced this event" }, - "businessName": { - "type": "string" + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" }, - "platform": { - "type": "string" + "event": { + "type": "string", + "description": "Event type" }, - "profilePicUrl": { - "type": "string" + "payload": { + "$ref": "#/components/schemas/CustomerPairingLinkExpiredPayload" } }, "required": [ - "lid", - "pushName" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "GetSessionAccountResponse": { + "CustomerPairingLinkRevokedPayload": { "type": "object", "properties": { - "success": { - "type": "boolean", + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "$ref": "#/components/schemas/WhatsAppAccount" + "pairingLinkId": { + "type": "string", + "format": "uuid" + }, + "reason": { + "type": "string", + "enum": [ + "customer_archived", + "phone_mismatch_limit", + "exchange_failure_limit", + "terminal_failure" + ] } }, "required": [ - "success", - "data" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "pairingLinkId" + ], + "additionalProperties": false }, - "HandoffKeysResponse": { + "CustomerPairingLinkRevokedEvent": { "type": "object", "properties": { - "runnerPublicKeyB64": { + "id": { "type": "string", - "description": "Curve25519 public key the customer must seal their snapshot to. Base64 (URL-safe, no padding)." + "description": "Unique event identifier" }, - "handoffId": { - "type": "string" + "session": { + "type": "string", + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerPairingLinkRevokedPayload" } }, "required": [ - "runnerPublicKeyB64", - "handoffId" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "HandoffKeysSuccess": { + "CustomerNumberAttachedPayload": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "eventId": { + "type": "string", + "format": "uuid" }, - "data": { - "$ref": "#/components/schemas/HandoffKeysResponse" - } - }, - "required": [ - "success", - "data" - ] - }, - "HandoffImportSuccess": { - "type": "object", - "properties": { - "success": { - "type": "boolean", + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "$ref": "#/components/schemas/SuccessResponse" + "sessionId": { + "type": "string", + "format": "uuid" + }, + "pairingLinkId": { + "type": "string", + "format": "uuid" } }, "required": [ - "success", - "data" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "sessionId" + ], + "additionalProperties": false }, - "HandoffImportRequest": { + "CustomerNumberAttachedEvent": { "type": "object", "properties": { - "handoffId": { + "id": { "type": "string", - "description": "Round-trip id returned by POST /handoff-keys." + "description": "Unique event identifier" }, - "sealedEnvelopeB64": { + "session": { "type": "string", - "description": "Sealed-box envelope (ephemeral_pub(32) || ciphertext) wrapping the customer's SignalStoreSnapshot. Base64." + "description": "Session that produced this event" }, - "snapshotHashHex": { + "timestamp": { "type": "string", - "description": "SHA-256 of the unsealed snapshot JSON, hex-encoded. The runner verifies after decrypt before flipping bartenderMode." + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerNumberAttachedPayload" } }, "required": [ - "handoffId", - "sealedEnvelopeB64", - "snapshotHashHex" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "UploadAppStateKeysSuccess": { + "CustomerNumberTransferredPayload": { "type": "object", "properties": { - "success": { - "type": "boolean", + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "$ref": "#/components/schemas/SuccessResponse" + "sessionId": { + "type": "string", + "format": "uuid" + }, + "sourceCustomerId": { + "type": "string", + "format": "uuid" } }, "required": [ - "success", - "data" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "sessionId", + "sourceCustomerId" + ], + "additionalProperties": false }, - "UploadAppStateKeysRequest": { + "CustomerNumberTransferredEvent": { "type": "object", "properties": { - "keys": { - "type": "array", - "items": { - "type": "object", - "properties": { - "keyId": { - "type": "string", - "description": "Base64-encoded AppStateSyncKey id (URL-safe-no-pad)." - }, - "keyB64": { - "type": "string", - "description": "Base64-encoded 32-byte symmetric key bytes." - }, - "fingerprintB64": { - "type": "string", - "description": "Base64-encoded WA fingerprint blob (optional)." - }, - "timestamp": { - "type": "number", - "description": "Unix-seconds timestamp WA stamped on the key." - } - }, - "required": [ - "keyId", - "keyB64", - "timestamp" - ] - } + "id": { + "type": "string", + "description": "Unique event identifier" }, - "upgradeTo": { + "session": { "type": "string", - "enum": [ - "passthrough_plus" - ], - "description": "When set, atomically flips sessions.config.bartenderMode → passthrough_plus after the keys persist. Eliminates the window where mode is set but no keys are staged." + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerNumberTransferredPayload" } }, "required": [ - "keys" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "HandoffAckSuccess": { + "CustomerNumberDisconnectedPayload": { "type": "object", "properties": { - "success": { - "type": "boolean", + "eventId": { + "type": "string", + "format": "uuid" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "actorKind": { + "type": "string", "enum": [ - true + "better_auth", + "org_key", + "project_token", + "cli_grant", + "system" ] }, - "data": { - "$ref": "#/components/schemas/SuccessResponse" + "sessionId": { + "type": "string", + "format": "uuid" + }, + "reason": { + "type": "string", + "pattern": "^[a-z0-9_]{1,64}$" } }, "required": [ - "success", - "data" - ] + "eventId", + "occurredAt", + "organizationId", + "projectId", + "customerId", + "actorKind", + "sessionId", + "reason" + ], + "additionalProperties": false }, - "HandoffAckRequest": { + "CustomerNumberDisconnectedEvent": { "type": "object", "properties": { - "handoffId": { - "type": "string" + "id": { + "type": "string", + "description": "Unique event identifier" }, - "snapshotHashHex": { + "session": { "type": "string", - "description": "SHA-256 of the snapshot the customer received via mode.handoff_export. API verifies this matches what the runner exported before flipping bartenderMode." + "description": "Session that produced this event" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 timestamp" + }, + "event": { + "type": "string", + "description": "Event type" + }, + "payload": { + "$ref": "#/components/schemas/CustomerNumberDisconnectedPayload" } }, "required": [ - "handoffId", - "snapshotHashHex" - ] + "id", + "session", + "timestamp", + "event", + "payload" + ], + "additionalProperties": false }, - "QRResponse": { + "PingResponse": { "type": "object", "properties": { - "qr": { + "status": { "type": "string", - "description": "QR code data to encode into a scannable image" - }, - "event": { - "type": "string", - "description": "QR channel event (e.g. success, timeout)" + "example": "ok" } - } + }, + "required": [ + "status" + ] }, - "GetQRCodeResponse": { + "HealthCheck": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "status": { + "type": "string", + "example": "healthy" }, - "data": { - "$ref": "#/components/schemas/QRResponse" + "error": { + "type": "string" } }, "required": [ - "success", - "data" + "status" ] }, - "PairCodeResponse": { + "HealthResponse": { "type": "object", "properties": { - "code": { + "status": { "type": "string", - "description": "Numeric pairing code for device linking" + "example": "healthy" + }, + "checks": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/HealthCheck" + } } }, "required": [ - "code" + "status", + "checks" ] }, - "RequestPairCodeResponse": { + "VersionResponse": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "version": { + "type": "string" }, - "data": { - "$ref": "#/components/schemas/PairCodeResponse" + "buildTime": { + "type": "string" + }, + "env": { + "type": "string" + }, + "apiVersion": { + "type": "string" + }, + "minSupportedVersion": { + "type": "string" } }, "required": [ - "success", - "data" + "version", + "buildTime", + "env", + "apiVersion", + "minSupportedVersion" ] }, - "PairCodeRequest": { + "StatusResponse": { "type": "object", "properties": { - "phone": { - "type": "string", - "minLength": 1 + "status": { + "type": "string" + }, + "uptime": { + "type": "string" + }, + "version": { + "type": "string" + }, + "env": { + "type": "string" } }, "required": [ - "phone" + "status", + "uptime", + "version", + "env" ] }, - "MessageResponse": { + "ErrorResponse": { "type": "object", "properties": { - "id": { - "type": "string", - "description": "WhatsApp message ID" - }, - "timestamp": { + "error": { "type": "string", - "description": "ISO 8601 send timestamp" + "description": "Human-readable error message" }, - "status": { + "docs": { "type": "string", - "description": "Delivery status: pending, sent, delivered, read" - }, - "mediaId": { - "type": "string" - }, - "senderLid": { - "type": "string" - }, - "senderPhoneNumber": { - "type": "string" + "description": "URL to the documentation for this error" + } + }, + "required": [ + "error" + ] + }, + "SuccessResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean" }, - "fromLid": { + "message": { "type": "string" + } + }, + "required": [ + "success" + ] + }, + "ClientToken": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "Client token with pmfa_ct_ prefix" }, - "fromPhoneNumber": { + "expiresAt": { "type": "string" } }, "required": [ - "id", - "timestamp", - "status", - "senderLid", - "fromLid" + "token", + "expiresAt" ] }, - "SendMessageResponse": { + "MintClientTokenResponse": { "type": "object", "properties": { "success": { @@ -2847,7 +3599,7 @@ ] }, "data": { - "$ref": "#/components/schemas/MessageResponse" + "$ref": "#/components/schemas/ClientToken" } }, "required": [ @@ -2855,715 +3607,2102 @@ "data" ] }, - "QuotedMessage": { + "MintClientTokenRequest": { "type": "object", "properties": { - "messageId": { - "type": "string" - }, - "participant": { - "type": "string" + "session": { + "type": "string", + "minLength": 1 }, - "type": { - "type": "string" + "ephemeralId": { + "type": "string", + "minLength": 1 }, - "text": { - "type": "string" + "ttlSeconds": { + "type": "integer" } }, "required": [ - "messageId", - "participant" + "session", + "ephemeralId" ] }, - "SendMessageRequest": { + "ClientRules": { "type": "object", "properties": { - "chatId": { - "type": "string", - "minLength": 1 - }, - "type": { - "type": "string", - "enum": [ - "text", - "image", - "file", - "voice", - "video", - "poll", - "location", - "contact", - "request_phone_number", - "product", - "product_list", - "order", - "list", - "buttons", - "address_message", - "flow" - ] - }, - "text": { - "type": "string" - }, - "url": { - "type": "string" - }, - "base64": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "caption": { - "type": "string" - }, - "ptt": { - "type": "boolean" - }, - "pollTitle": { - "type": "string" - }, - "pollOptions": { + "actions": { "type": "array", "items": { "type": "string" } }, - "pollMultiSelect": { - "type": "boolean" - }, - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - }, - "address": { - "type": "string" - }, - "vcard": { + "recipientMode": { "type": "string" }, - "isForwarded": { - "type": "boolean" - }, - "mentions": { + "verifiedJids": { "type": "array", "items": { "type": "string" } }, - "quotedMessage": { - "$ref": "#/components/schemas/QuotedMessage" - }, - "template": { + "rateLimits": { "type": "object", "properties": { - "name": { - "type": "string", - "minLength": 1 - }, - "language": { - "type": "string", - "minLength": 2 - }, - "components": { - "type": "array", - "items": { - "nullable": true - } - } - }, - "required": [ - "name", - "language" - ], - "description": "Template send (cloud_api only). When set, type is ignored. Rejected on whatsmeow sessions." - }, - "product": { - "type": "object", - "properties": { - "businessOwnerJid": { - "type": "string", - "pattern": "^\\d+@s\\.whatsapp\\.net$" - }, - "id": { - "type": "string", - "minLength": 1 - }, - "title": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 0 - }, - "currencyCode": { - "type": "string", - "pattern": "^[A-Z]{3}$" - }, - "priceAmount1000": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "salePriceAmount1000": { + "perMinute": { "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "retailerId": { - "type": "string", - "minLength": 0 - }, - "url": { - "type": "string", - "maxLength": 2048, - "format": "uri" + "minimum": 0 }, - "imageCount": { + "perDay": { "type": "integer", - "minimum": 0, - "maximum": 100 - }, - "image": { - "type": "object", - "properties": { - "url": { - "type": "string", - "maxLength": 4096, - "format": "uri" - }, - "base64": { - "type": "string", - "maxLength": 87384 - }, - "mimeType": { - "type": "string", - "minLength": 0 - } - } - }, - "body": { - "type": "string", - "minLength": 0 - }, - "footer": { - "type": "string", - "minLength": 0 + "minimum": 0 } - }, - "required": [ - "businessOwnerJid", - "id", - "title", - "currencyCode", - "priceAmount1000" + } + } + }, + "required": [ + "actions", + "recipientMode" + ] + }, + "GetClientRulesResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true ] }, - "productList": { - "type": "object", - "properties": { - "businessOwnerJid": { - "type": "string", - "pattern": "^\\d+@s\\.whatsapp\\.net$" - }, - "title": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 0 - }, - "buttonText": { - "type": "string", - "minLength": 1 - }, - "footer": { - "type": "string", - "minLength": 0 - }, - "sections": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "minLength": 0 - }, - "productIds": { - "type": "array", - "items": { - "type": "string", - "minLength": 1 - }, - "minItems": 1, - "maxItems": 30 - } - }, - "required": [ - "productIds" - ] - }, - "minItems": 1, - "maxItems": 10 - } - }, - "required": [ - "businessOwnerJid", - "title", - "buttonText", - "sections" - ] + "data": { + "$ref": "#/components/schemas/ClientRules" + } + }, + "required": [ + "success", + "data" + ] + }, + "SetClientRulesRequest": { + "type": "object", + "properties": { + "recipientMode": { + "type": "string" }, - "order": { - "type": "object", - "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "thumbnailBase64": { - "type": "string", - "maxLength": 87384 - }, - "itemCount": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "status": { - "type": "string", - "enum": [ - "inquiry", - "accepted", - "declined" - ] - }, - "message": { - "type": "string", - "minLength": 0 - }, - "title": { - "type": "string", - "minLength": 0 - }, - "sellerJid": { - "type": "string", - "pattern": "^\\d+@s\\.whatsapp\\.net$" - }, - "token": { - "type": "string", - "minLength": 0 - }, - "totalAmount1000": { - "type": "integer", - "minimum": 0, - "maximum": 9007199254740991 - }, - "totalCurrencyCode": { - "type": "string", - "pattern": "^[A-Z]{3}$" - }, - "catalogType": { - "type": "string", - "minLength": 0 - } - }, - "required": [ - "id", - "itemCount", - "status", - "sellerJid", - "totalAmount1000", - "totalCurrencyCode" - ] + "allowedActions": { + "type": "string", + "default": "" }, - "list": { - "type": "object", - "properties": { - "title": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 0 - }, - "buttonText": { - "type": "string", - "minLength": 1 - }, - "footer": { - "type": "string", - "minLength": 0 - }, - "sections": { - "type": "array", - "items": { - "type": "object", - "properties": { - "title": { - "type": "string", - "minLength": 0 - }, - "rows": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "title": { - "type": "string", - "minLength": 1 - }, - "description": { - "type": "string", - "minLength": 0 - } - }, - "required": [ - "id", - "title" - ] - }, - "minItems": 1, - "maxItems": 30 - } - }, - "required": [ - "rows" - ] - }, - "minItems": 1, - "maxItems": 10 - } - }, - "required": [ - "title", - "buttonText", - "sections" + "rateLimit": { + "type": "integer", + "default": 0 + }, + "maxDaily": { + "type": "integer", + "default": 0 + }, + "allowedOrigins": { + "type": "string", + "default": "" + }, + "enabled": { + "type": "boolean" + }, + "maxConcurrency": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "allowedNumber": { + "type": "string", + "default": "" + }, + "maxSetupsPerMinute": { + "type": "integer", + "minimum": 0, + "maximum": 600, + "default": 0 + } + }, + "required": [ + "recipientMode", + "enabled" + ] + }, + "Operation": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "kind": { + "type": "string" + }, + "resourceType": { + "type": "string" + }, + "resourceId": { + "type": "string" + }, + "projectId": { + "type": "string", + "nullable": true, + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "running", + "action_required", + "cancelling", + "succeeded", + "failed", + "cancelled" ] }, - "buttons": { + "progressCode": { + "type": "string", + "nullable": true + }, + "failureCode": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "completedAt": { + "type": "string", + "nullable": true, + "format": "date-time" + } + }, + "required": [ + "id", + "kind", + "resourceType", + "resourceId", + "projectId", + "status", + "progressCode", + "failureCode", + "createdAt", + "updatedAt", + "completedAt" + ] + }, + "Session": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Canonical per-org session identifier. Use this in /api/sessions/{session}/* paths." + }, + "name": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "connection": { + "type": "string", + "enum": [ + "linked_device", + "cloud_api" + ], + "description": "How the session connects to WhatsApp. 'linked_device' pairs as a WhatsApp linked device; 'cloud_api' uses the Meta Cloud API. Immutable after create." + }, + "testMode": { + "type": "boolean", + "description": "Whether this session was created as a simulated testing number." + }, + "status": { + "type": "string", + "description": "Current status: CONNECTING, CONNECTED, or DISCONNECTED" + }, + "statusReason": { + "type": "string" + }, + "runnerId": { + "type": "string" + }, + "proxy": { + "type": "string" + }, + "config": { + "type": "object", + "additionalProperties": {} + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "name", + "tenantId", + "connection", + "testMode", + "status", + "config", + "createdAt", + "updatedAt" + ] + }, + "ListSessionsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session" + } + } + }, + "required": [ + "success", + "data" + ] + }, + "SessionOperation": { + "allOf": [ + { + "$ref": "#/components/schemas/Session" + }, + { "type": "object", "properties": { - "title": { - "type": "string", - "minLength": 0 - }, - "body": { - "type": "string", - "minLength": 1 - }, - "footer": { + "operationId": { "type": "string", - "minLength": 0 - }, + "format": "uuid" + } + } + } + ] + }, + "CreateSessionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/SessionOperation" + } + }, + "required": [ + "success", + "data" + ] + }, + "CloudApiCredentials": { + "type": "object", + "properties": { + "phoneNumberId": { + "type": "string", + "minLength": 1 + }, + "wabaId": { + "type": "string", + "minLength": 1 + }, + "businessAccountId": { + "type": "string" + }, + "appId": { + "type": "string", + "minLength": 1 + }, + "appSecret": { + "type": "string", + "minLength": 1 + }, + "systemUserToken": { + "type": "string", + "minLength": 1 + }, + "webhookVerifyToken": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "phoneNumberId", + "wabaId", + "appId", + "appSecret", + "systemUserToken", + "webhookVerifyToken" + ], + "description": "Meta credentials. Required when connection='cloud_api'. Encrypted at rest before storage." + }, + "HistorySyncPolicy": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": [ + "metadata_only", + "deliver" + ], + "default": "metadata_only" + }, + "requestFull": { + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "CreateSessionRequest": { + "type": "object", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "description": "Project id the session belongs to. Required." + }, + "sessionId": { + "type": "string", + "minLength": 1, + "description": "Canonical per-org session identifier (1-64 alphanumeric chars, dots, hyphens, underscores). Caller-supplied or generated; returned in the response. This is the id used in all /api/sessions/{session}/* paths." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Optional human-friendly label for the session. Not an id." + }, + "start": { + "type": "boolean" + }, + "connection": { + "type": "string", + "enum": [ + "linked_device", + "cloud_api" + ], + "description": "How the session connects to WhatsApp. 'linked_device' (default) pairs as a WhatsApp linked device; 'cloud_api' uses the Meta Cloud API and requires the `cloudApi` creds block. Immutable after create." + }, + "testMode": { + "type": "boolean", + "description": "Create a simulated testing number (behaves like a real session via the simulated universe). Defaults to false." + }, + "bartenderMode": { + "type": "string", + "enum": [ + "magic", + "passthrough", + "passthrough_plus" + ], + "description": "Bartender privacy tier. Defaults to 'magic'. Mutable via the handoff ceremony at POST /api/sessions/{name}/handoff-keys + /handoff-import. 'magic' — Polymorfa holds the Signal session; full message visibility. 'passthrough' — Polymorfa holds noise creds only; neither messages nor app state are decryptable on Polymorfa's side. 'passthrough_plus' — Track P + customer has uploaded AppStateSyncKeys for selected collections (contacts, labels, mute/archive, blocklist), so Polymorfa sees metadata mutations but never message content." + }, + "cloudApi": { + "$ref": "#/components/schemas/CloudApiCredentials" + }, + "config": { + "type": "object", + "additionalProperties": true + }, + "historySync": { + "$ref": "#/components/schemas/HistorySyncPolicy" + } + }, + "required": [ + "projectId" + ] + }, + "GetSessionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/Session" + } + }, + "required": [ + "success", + "data" + ] + }, + "UpdateSessionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/Session" + } + }, + "required": [ + "success", + "data" + ] + }, + "UpdateSessionRequest": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "historySync": { + "$ref": "#/components/schemas/HistorySyncPolicy" + } + } + }, + "OperationAccepted": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "message": { + "type": "string" + }, + "operationId": { + "type": "string", + "format": "uuid" + }, + "immediate": { + "type": "boolean" + } + }, + "required": [ + "success", + "message" + ] + }, + "WhatsAppAccount": { + "type": "object", + "properties": { + "lid": { + "type": "string" + }, + "phoneNumber": { + "type": "string" + }, + "pushName": { + "type": "string" + }, + "businessName": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "profilePicUrl": { + "type": "string" + } + }, + "required": [ + "lid", + "pushName" + ] + }, + "GetSessionAccountResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/WhatsAppAccount" + } + }, + "required": [ + "success", + "data" + ] + }, + "HandoffKeysResponse": { + "type": "object", + "properties": { + "runnerPublicKeyB64": { + "type": "string", + "description": "Curve25519 public key the customer must seal their snapshot to. Base64 (URL-safe, no padding)." + }, + "handoffId": { + "type": "string" + } + }, + "required": [ + "runnerPublicKeyB64", + "handoffId" + ] + }, + "HandoffKeysSuccess": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/HandoffKeysResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "HandoffImportSuccess": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/SuccessResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "HandoffImportRequest": { + "type": "object", + "properties": { + "handoffId": { + "type": "string", + "description": "Round-trip id returned by POST /handoff-keys." + }, + "sealedEnvelopeB64": { + "type": "string", + "description": "Sealed-box envelope (ephemeral_pub(32) || ciphertext) wrapping the customer's SignalStoreSnapshot. Base64." + }, + "snapshotHashHex": { + "type": "string", + "description": "SHA-256 of the unsealed snapshot JSON, hex-encoded. The runner verifies after decrypt before flipping bartenderMode." + } + }, + "required": [ + "handoffId", + "sealedEnvelopeB64", + "snapshotHashHex" + ] + }, + "UploadAppStateKeysSuccess": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/SuccessResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "UploadAppStateKeysRequest": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "type": "object", + "properties": { + "keyId": { + "type": "string", + "description": "Base64-encoded AppStateSyncKey id (URL-safe-no-pad)." + }, + "keyB64": { + "type": "string", + "description": "Base64-encoded 32-byte symmetric key bytes." + }, + "fingerprintB64": { + "type": "string", + "description": "Base64-encoded WA fingerprint blob (optional)." + }, + "timestamp": { + "type": "number", + "description": "Unix-seconds timestamp WA stamped on the key." + } + }, + "required": [ + "keyId", + "keyB64", + "timestamp" + ] + } + }, + "upgradeTo": { + "type": "string", + "enum": [ + "passthrough_plus" + ], + "description": "When set, atomically flips sessions.config.bartenderMode → passthrough_plus after the keys persist. Eliminates the window where mode is set but no keys are staged." + } + }, + "required": [ + "keys" + ] + }, + "HandoffAckSuccess": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/SuccessResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "HandoffAckRequest": { + "type": "object", + "properties": { + "handoffId": { + "type": "string" + }, + "snapshotHashHex": { + "type": "string", + "description": "SHA-256 of the snapshot the customer received via mode.handoff_export. API verifies this matches what the runner exported before flipping bartenderMode." + } + }, + "required": [ + "handoffId", + "snapshotHashHex" + ] + }, + "QRResponse": { + "type": "object", + "properties": { + "qr": { + "type": "string", + "description": "QR code data to encode into a scannable image" + }, + "event": { + "type": "string", + "description": "QR channel event (e.g. success, timeout)" + } + } + }, + "GetQRCodeResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/QRResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "PairCodeResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Numeric pairing code for device linking" + } + }, + "required": [ + "code" + ] + }, + "RequestPairCodeResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/PairCodeResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "PairCodeRequest": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "phone" + ] + }, + "MessageResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "WhatsApp message ID" + }, + "timestamp": { + "type": "string", + "description": "ISO 8601 send timestamp" + }, + "status": { + "type": "string", + "description": "Delivery status: pending, sent, delivered, read" + }, + "mediaId": { + "type": "string" + }, + "senderLid": { + "type": "string" + }, + "senderPhoneNumber": { + "type": "string" + }, + "fromLid": { + "type": "string" + }, + "fromPhoneNumber": { + "type": "string" + } + }, + "required": [ + "id", + "timestamp", + "status", + "senderLid", + "fromLid" + ] + }, + "SendMessageResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/MessageResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "QuotedMessage": { + "type": "object", + "properties": { + "messageId": { + "type": "string" + }, + "participant": { + "type": "string" + }, + "type": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "messageId", + "participant" + ] + }, + "SendMessageRequest": { + "type": "object", + "properties": { + "chatId": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "enum": [ + "text", + "image", + "file", + "voice", + "video", + "poll", + "location", + "contact", + "request_phone_number", + "product", + "product_list", + "order", + "list", + "buttons", + "address_message", + "flow" + ] + }, + "text": { + "type": "string" + }, + "url": { + "type": "string" + }, + "base64": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "ptt": { + "type": "boolean" + }, + "pollTitle": { + "type": "string" + }, + "pollOptions": { + "type": "array", + "items": { + "type": "string" + } + }, + "pollMultiSelect": { + "type": "boolean" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "address": { + "type": "string" + }, + "vcard": { + "type": "string" + }, + "product": { + "type": "object", + "properties": { + "businessOwnerJid": { + "type": "string", + "pattern": "^\\d+@s\\.whatsapp\\.net$" + }, + "id": { + "type": "string", + "minLength": 1 + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 0 + }, + "currencyCode": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "priceAmount1000": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "salePriceAmount1000": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "retailerId": { + "type": "string", + "minLength": 0 + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "imageCount": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "image": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 4096, + "format": "uri" + }, + "base64": { + "type": "string", + "maxLength": 87384 + }, + "mimeType": { + "type": "string", + "minLength": 0 + } + } + }, + "body": { + "type": "string", + "minLength": 0 + }, + "footer": { + "type": "string", + "minLength": 0 + } + }, + "required": [ + "businessOwnerJid", + "id", + "title", + "currencyCode", + "priceAmount1000" + ] + }, + "productList": { + "type": "object", + "properties": { + "businessOwnerJid": { + "type": "string", + "pattern": "^\\d+@s\\.whatsapp\\.net$" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 0 + }, + "buttonText": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, + "sections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 0 + }, + "productIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "maxItems": 30 + } + }, + "required": [ + "productIds" + ] + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": [ + "businessOwnerJid", + "title", + "buttonText", + "sections" + ] + }, + "order": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "thumbnailBase64": { + "type": "string", + "maxLength": 87384 + }, + "itemCount": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "status": { + "type": "string", + "enum": [ + "inquiry", + "accepted", + "declined" + ] + }, + "message": { + "type": "string", + "minLength": 0 + }, + "title": { + "type": "string", + "minLength": 0 + }, + "sellerJid": { + "type": "string", + "pattern": "^\\d+@s\\.whatsapp\\.net$" + }, + "token": { + "type": "string", + "minLength": 0 + }, + "totalAmount1000": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "totalCurrencyCode": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "catalogType": { + "type": "string", + "minLength": 0 + } + }, + "required": [ + "id", + "itemCount", + "status", + "sellerJid", + "totalAmount1000", + "totalCurrencyCode" + ] + }, + "list": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 0 + }, + "buttonText": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, + "sections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 0 + }, + "rows": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 0 + } + }, + "required": [ + "id", + "title" + ] + }, + "minItems": 1, + "maxItems": 30 + } + }, + "required": [ + "rows" + ] + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": [ + "title", + "buttonText", + "sections" + ] + }, + "buttons": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 0 + }, + "body": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, "buttons": { "type": "array", "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "url" - ] - }, - "text": { - "type": "string", - "minLength": 1 - }, - "url": { - "type": "string", - "maxLength": 2048, - "format": "uri" - } - }, - "required": [ - "type", - "text", - "url" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "call" - ] - }, - "text": { - "type": "string", - "minLength": 1 - }, - "phoneNumber": { - "type": "string", - "pattern": "^\\+?\\d{6,20}$" - } - }, - "required": [ - "type", - "text", - "phoneNumber" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "reply" - ] - }, - "text": { - "type": "string", - "minLength": 1 - }, - "id": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "type", - "text", - "id" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "copy" - ] - }, - "text": { - "type": "string", - "minLength": 1 - }, - "copyCode": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "type", - "text", - "copyCode" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "catalog" - ] - }, - "text": { - "type": "string", - "minLength": 1 - }, - "businessPhoneNumber": { - "type": "string", - "pattern": "^\\+?\\d{6,20}$" - }, - "catalogProductId": { - "type": "string", - "minLength": 0 - } - }, - "required": [ - "type", - "text", - "businessPhoneNumber" - ] - } - ] - }, - "minItems": 1, - "maxItems": 3 + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "url" + ] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": [ + "type", + "text", + "url" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "call" + ] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "phoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + } + }, + "required": [ + "type", + "text", + "phoneNumber" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reply" + ] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "id": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "type", + "text", + "id" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "copy" + ] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "copyCode": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "type", + "text", + "copyCode" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "catalog" + ] + }, + "text": { + "type": "string", + "minLength": 1 + }, + "businessPhoneNumber": { + "type": "string", + "pattern": "^\\+?\\d{6,20}$" + }, + "catalogProductId": { + "type": "string", + "minLength": 0 + } + }, + "required": [ + "type", + "text", + "businessPhoneNumber" + ] + } + ] + }, + "minItems": 1, + "maxItems": 3 + } + }, + "required": [ + "body", + "buttons" + ] + }, + "addressMessage": { + "type": "object", + "properties": { + "body": { + "type": "string", + "minLength": 1 + }, + "buttonText": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, + "country": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "required": [ + "body" + ] + }, + "flow": { + "type": "object", + "properties": { + "body": { + "type": "string", + "minLength": 1 + }, + "buttonText": { + "type": "string", + "minLength": 1 + }, + "footer": { + "type": "string", + "minLength": 0 + }, + "id": { + "type": "string", + "minLength": 1 + }, + "token": { + "type": "string", + "minLength": 1 + }, + "action": { + "type": "string", + "enum": [ + "navigate", + "data_exchange" + ] + }, + "screen": { + "type": "string", + "minLength": 0 + }, + "dataJson": { + "type": "string", + "minLength": 0 + } + }, + "required": [ + "body", + "buttonText", + "id", + "token", + "action" + ], + "oneOf": [ + { + "properties": { + "action": { + "enum": [ + "navigate" + ] + } + }, + "required": [ + "screen" + ] + }, + { + "properties": { + "action": { + "enum": [ + "data_exchange" + ] + } + }, + "not": { + "anyOf": [ + { + "required": [ + "screen" + ] + }, + { + "required": [ + "dataJson" + ] + } + ] + } + } + ] + }, + "isForwarded": { + "type": "boolean" + }, + "mentions": { + "type": "array", + "items": { + "type": "string" + } + }, + "quotedMessage": { + "$ref": "#/components/schemas/QuotedMessage" + }, + "template": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "language": { + "type": "string", + "minLength": 2 + }, + "components": { + "type": "array", + "items": {} + } + }, + "required": [ + "name", + "language" + ], + "description": "Template send (cloud_api only). When set, type is ignored. Rejected on whatsmeow sessions." + } + }, + "required": [ + "chatId", + "type" + ] + }, + "SeenRequest": { + "type": "object", + "properties": { + "chatId": { + "type": "string", + "minLength": 1 + }, + "messageId": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "chatId", + "messageId" + ] + }, + "TypingRequest": { + "type": "object", + "properties": { + "chatId": { + "type": "string", + "minLength": 1 + }, + "state": { + "type": "string", + "enum": [ + "typing", + "recording", + "paused" + ] + } + }, + "required": [ + "chatId", + "state" + ] + }, + "SendReactionResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/MessageResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "ReactRequest": { + "type": "object", + "properties": { + "chatId": { + "type": "string", + "minLength": 1 + }, + "messageId": { + "type": "string", + "minLength": 1 + }, + "reaction": { + "type": "string" + } + }, + "required": [ + "chatId", + "messageId", + "reaction" + ] + }, + "StarMessageResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/MessageResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "StarRequest": { + "type": "object", + "properties": { + "chatId": { + "type": "string", + "minLength": 1 + }, + "messageId": { + "type": "string", + "minLength": 1 + }, + "star": { + "type": "boolean" + }, + "fromMe": { + "type": "boolean" + }, + "sender": { + "type": "string" + } + }, + "required": [ + "chatId", + "messageId", + "star" + ] + }, + "EditMessageRequest": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "text" + ] + }, + "DisappearingTimerRequest": { + "type": "object", + "properties": { + "durationSeconds": { + "anyOf": [ + { + "type": "number", + "enum": [ + 0 + ] + }, + { + "type": "number", + "enum": [ + 86400 + ] + }, + { + "type": "number", + "enum": [ + 604800 + ] + }, + { + "type": "number", + "enum": [ + 7776000 + ] + } + ] + } + }, + "required": [ + "durationSeconds" + ] + }, + "RejectCallRequest": { + "type": "object", + "properties": { + "from": { + "type": "string", + "minLength": 1, + "description": "JID of the incoming caller" + } + }, + "required": [ + "from" + ] + }, + "ICEServer": { + "type": "object", + "properties": { + "urls": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } } - }, - "required": [ - "body", - "buttons" ] }, - "addressMessage": { - "type": "object", - "properties": { - "body": { - "type": "string", - "minLength": 1 - }, - "buttonText": { - "type": "string", - "minLength": 1 - }, - "footer": { - "type": "string", - "minLength": 0 - }, - "country": { - "type": "string", - "pattern": "^[A-Z]{2}$" - } - }, - "required": [ - "body" + "username": { + "type": "string" + }, + "credential": { + "type": "string" + } + }, + "required": [ + "urls" + ] + }, + "SDPAnswer": { + "type": "object", + "properties": { + "sdp": { + "type": "string" + }, + "iceServers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ICEServer" + } + } + }, + "required": [ + "sdp", + "iceServers" + ] + }, + "VoipOfferResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/SDPAnswer" + } + }, + "required": [ + "success", + "data" + ] + }, + "VoipOfferRequest": { + "type": "object", + "properties": { + "sdp": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "sdp" + ] + }, + "VoipRenegotiateResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/SDPAnswer" + } + }, + "required": [ + "success", + "data" + ] + }, + "VoipRenegotiateRequest": { + "type": "object", + "properties": { + "sdp": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "sdp" + ] + }, + "VoipSocketTicket": { + "type": "object", + "properties": { + "ticket": { + "type": "string" + }, + "expiresAt": { + "type": "number" + }, + "url": { + "type": "string" + } + }, + "required": [ + "ticket", + "expiresAt", + "url" + ] + }, + "VoipSocketTicketResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/VoipSocketTicket" + } + }, + "required": [ + "success", + "data" + ] + }, + "VoipSocketTicketRequest": { + "type": "object", + "properties": { + "session": { + "type": "string", + "minLength": 1 + } + } + }, + "VoipAgentToken": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "expiresAt": { + "type": "number" + } + }, + "required": [ + "token", + "expiresAt" + ] + }, + "VoipAgentTokenResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/VoipAgentToken" + } + }, + "required": [ + "success", + "data" + ] + }, + "VoipAgentTokenRequest": { + "type": "object", + "properties": { + "ttlSeconds": { + "type": "integer", + "minimum": 1, + "maximum": 3600 + } + } + }, + "VoipCandidateRequest": { + "type": "object", + "properties": { + "candidate": { + "type": "string" + }, + "sdpMid": { + "type": "string" + }, + "sdpMLineIndex": { + "type": "integer" + } + }, + "required": [ + "candidate" + ] + }, + "TrickleCandidate": { + "type": "object", + "properties": { + "candidate": { + "type": "string" + }, + "sdpMid": { + "type": "string" + }, + "sdpMLineIndex": { + "type": "integer" + } + }, + "required": [ + "candidate" + ] + }, + "VoipCandidatesResponse": { + "type": "object", + "properties": { + "candidates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TrickleCandidate" + } + } + }, + "required": [ + "candidates" + ] + }, + "VoipCandidatesEnvelope": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true ] }, - "flow": { - "type": "object", - "properties": { - "body": { - "type": "string", - "minLength": 1 - }, - "buttonText": { - "type": "string", - "minLength": 1 - }, - "footer": { - "type": "string", - "minLength": 0 - }, - "id": { - "type": "string", - "minLength": 1 - }, - "token": { - "type": "string", - "minLength": 1 - }, - "action": { - "type": "string", - "enum": [ - "navigate", - "data_exchange" - ] - }, - "screen": { - "type": "string", - "minLength": 0 - }, - "dataJson": { - "type": "string", - "minLength": 0 - } - }, - "required": [ - "body", - "buttonText", - "id", - "token", - "action" - ], - "oneOf": [ - { - "properties": { - "action": { - "enum": [ - "navigate" - ] - } - }, - "required": [ - "screen" - ] - }, - { - "properties": { - "action": { - "enum": [ - "data_exchange" - ] - } - }, - "not": { - "anyOf": [ - { - "required": [ - "screen" - ] - }, - { - "required": [ - "dataJson" - ] - } - ] - } - } + "data": { + "$ref": "#/components/schemas/VoipCandidatesResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "VoipToken": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "expiresAt": { + "type": "number" + } + }, + "required": [ + "token", + "expiresAt" + ] + }, + "VoipTokenResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true ] + }, + "data": { + "$ref": "#/components/schemas/VoipToken" + } + }, + "required": [ + "success", + "data" + ] + }, + "VoipTokenRequest": { + "type": "object", + "properties": { + "session": { + "type": "string", + "minLength": 1 + }, + "ephemeralId": { + "type": "string", + "minLength": 1 + }, + "ttlSeconds": { + "type": "integer" + } + }, + "required": [ + "session", + "ephemeralId" + ] + }, + "Participant": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lid": { + "type": "string", + "deprecated": true + }, + "phoneNumber": { + "type": "string" + }, + "username": { + "type": "string" + }, + "isAdmin": { + "type": "boolean" + }, + "isSuperAdmin": { + "type": "boolean" } }, "required": [ - "chatId", - "type" + "id", + "lid", + "isAdmin", + "isSuperAdmin" ] }, - "SeenRequest": { + "Group": { "type": "object", "properties": { - "chatId": { + "id": { "type": "string", - "minLength": 1 + "description": "Group JID (e.g. 120363...@g.us)" }, - "messageId": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "ownerLid": { "type": "string", - "minLength": 1 + "deprecated": true + }, + "createdAt": { + "type": "number", + "description": "Unix timestamp in seconds" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Participant" + } } }, "required": [ - "chatId", - "messageId" + "id", + "name", + "description", + "ownerId", + "ownerLid", + "createdAt", + "participants" ] }, - "TypingRequest": { + "ListGroupsResponse": { "type": "object", "properties": { - "chatId": { - "type": "string", - "minLength": 1 - }, - "state": { - "type": "string", + "success": { + "type": "boolean", "enum": [ - "typing", - "recording", - "paused" + true ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Group" + } } }, "required": [ - "chatId", - "state" + "success", + "data" ] }, - "SendReactionResponse": { + "CreateGroupResponse": { "type": "object", "properties": { "success": { @@ -3573,7 +5712,7 @@ ] }, "data": { - "$ref": "#/components/schemas/MessageResponse" + "$ref": "#/components/schemas/Group" } }, "required": [ @@ -3581,28 +5720,66 @@ "data" ] }, - "ReactRequest": { + "CreateGroupRequest": { "type": "object", "properties": { - "chatId": { + "name": { "type": "string", "minLength": 1 }, - "messageId": { - "type": "string", - "minLength": 1 + "participants": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "name", + "participants" + ] + }, + "GroupInviteInfo": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "reaction": { + "subject": { + "type": "string" + }, + "creatorId": { "type": "string" + }, + "creatorLid": { + "type": "string", + "deprecated": true + }, + "createdAt": { + "type": "number" + }, + "size": { + "type": "number" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Participant" + } } }, "required": [ - "chatId", - "messageId", - "reaction" + "id", + "subject", + "creatorId", + "creatorLid", + "createdAt", + "size", + "participants" ] }, - "StarMessageResponse": { + "GetGroupJoinInfoResponse": { "type": "object", "properties": { "success": { @@ -3612,7 +5789,7 @@ ] }, "data": { - "$ref": "#/components/schemas/MessageResponse" + "$ref": "#/components/schemas/GroupInviteInfo" } }, "required": [ @@ -3620,97 +5797,185 @@ "data" ] }, - "StarRequest": { + "JoinGroupRequest": { "type": "object", "properties": { - "chatId": { + "code": { "type": "string", "minLength": 1 + } + }, + "required": [ + "code" + ] + }, + "GetGroupResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "messageId": { + "data": { + "$ref": "#/components/schemas/Group" + } + }, + "required": [ + "success", + "data" + ] + }, + "SetGroupFieldRequest": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "required": [ + "value" + ] + }, + "InviteCodeResponse": { + "type": "object", + "properties": { + "code": { "type": "string", - "minLength": 1 + "description": "Group invite code" + } + }, + "required": [ + "code" + ] + }, + "GetGroupInviteCodeResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "star": { - "type": "boolean" + "data": { + "$ref": "#/components/schemas/InviteCodeResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "RevokeGroupInviteCodeResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "fromMe": { - "type": "boolean" + "data": { + "$ref": "#/components/schemas/InviteCodeResponse" + } + }, + "required": [ + "success", + "data" + ] + }, + "GetGroupParticipantsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Participant" + } + } + }, + "required": [ + "success", + "data" + ] + }, + "GroupParticipantsRequest": { + "type": "object", + "properties": { + "participants": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "participants" + ] + }, + "SetGroupPictureRequest": { + "type": "object", + "properties": { + "url": { + "type": "string" }, - "sender": { + "base64": { "type": "string" } - }, - "required": [ - "chatId", - "messageId", - "star" - ] + } }, - "EditMessageRequest": { + "GroupAdminOnlySettingRequest": { "type": "object", "properties": { - "text": { - "type": "string", - "minLength": 1 + "adminsOnly": { + "type": "boolean" } }, "required": [ - "text" + "adminsOnly" ] }, - "DisappearingTimerRequest": { + "GroupMemberAddModeRequest": { "type": "object", "properties": { - "durationSeconds": { - "anyOf": [ - { - "type": "number", - "enum": [ - 0 - ] - }, - { - "type": "number", - "enum": [ - 86400 - ] - }, - { - "type": "number", - "enum": [ - 604800 - ] - }, - { - "type": "number", - "enum": [ - 7776000 - ] - } + "mode": { + "type": "string", + "enum": [ + "admin_add", + "all_member_add" ] } }, "required": [ - "durationSeconds" + "mode" ] }, - "RejectCallRequest": { + "GroupJoinApprovalRequest": { "type": "object", "properties": { - "from": { - "type": "string", - "minLength": 1, - "description": "JID of the incoming caller" + "required": { + "type": "boolean" } }, "required": [ - "from" + "required" ] }, - "Participant": { + "Contact": { "type": "object", "properties": { + "id": { + "type": "string" + }, "lid": { "type": "string", "deprecated": true @@ -3718,66 +5983,30 @@ "phoneNumber": { "type": "string" }, - "isAdmin": { - "type": "boolean" - }, - "isSuperAdmin": { - "type": "boolean" - }, - "id": { - "type": "string" - }, "username": { "type": "string" - } - }, - "required": [ - "lid", - "isAdmin", - "isSuperAdmin" - ] - }, - "Group": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Group JID (e.g. 120363...@g.us)" }, "name": { "type": "string" }, - "description": { + "pushName": { "type": "string" }, - "ownerLid": { - "type": "string", - "deprecated": true - }, - "createdAt": { - "type": "number", - "description": "Unix timestamp in seconds" - }, - "participants": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Participant" - } + "businessName": { + "type": "string" }, - "ownerId": { + "profileUrl": { "type": "string" } }, "required": [ "id", + "lid", "name", - "description", - "ownerLid", - "createdAt", - "participants" + "pushName" ] }, - "ListGroupsResponse": { + "ListContactsResponse": { "type": "object", "properties": { "success": { @@ -3789,7 +6018,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Group" + "$ref": "#/components/schemas/Contact" } } }, @@ -3798,83 +6027,70 @@ "data" ] }, - "CreateGroupResponse": { + "CheckContactResult": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "exists": { + "type": "boolean" }, - "data": { - "$ref": "#/components/schemas/Group" + "id": { + "type": "string" + }, + "lid": { + "type": "string", + "deprecated": true + }, + "phoneNumber": { + "type": "string" + }, + "username": { + "type": "string" } }, "required": [ - "success", - "data" + "exists" ] }, - "CreateGroupRequest": { + "CheckContactsResponse": { "type": "object", "properties": { - "name": { - "type": "string", - "minLength": 1 + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "participants": { + "data": { "type": "array", "items": { - "type": "string" - }, - "minItems": 1 + "$ref": "#/components/schemas/CheckContactResult" + } } }, "required": [ - "name", - "participants" + "success", + "data" ] }, - "GroupInviteInfo": { + "Blocklist": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "subject": { + "hash": { "type": "string" }, - "creatorLid": { - "type": "string", - "deprecated": true - }, - "createdAt": { - "type": "number" - }, - "size": { - "type": "number" - }, - "participants": { + "jids": { "type": "array", "items": { - "$ref": "#/components/schemas/Participant" + "type": "string" } - }, - "creatorId": { - "type": "string" } }, "required": [ - "id", - "subject", - "creatorLid", - "createdAt", - "size", - "participants" + "hash", + "jids" ] }, - "GetGroupJoinInfoResponse": { + "GetBlocklistResponse": { "type": "object", "properties": { "success": { @@ -3884,7 +6100,7 @@ ] }, "data": { - "$ref": "#/components/schemas/GroupInviteInfo" + "$ref": "#/components/schemas/Blocklist" } }, "required": [ @@ -3892,19 +6108,7 @@ "data" ] }, - "JoinGroupRequest": { - "type": "object", - "properties": { - "code": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "code" - ] - }, - "GetGroupResponse": { + "GetContactResponse": { "type": "object", "properties": { "success": { @@ -3914,7 +6118,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Group" + "$ref": "#/components/schemas/Contact" } }, "required": [ @@ -3922,48 +6126,83 @@ "data" ] }, - "SetGroupFieldRequest": { + "ProfilePictureResponse": { "type": "object", "properties": { - "value": { - "type": "string" + "url": { + "type": "string", + "description": "HTTPS URL of the profile picture" } }, "required": [ - "value" + "url" ] }, - "InviteCodeResponse": { + "GetContactPictureResponse": { "type": "object", "properties": { - "code": { - "type": "string", - "description": "Group invite code" + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/ProfilePictureResponse" } }, "required": [ - "code" + "success", + "data" ] }, - "GetGroupInviteCodeResponse": { + "UserInfo": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "id": { + "type": "string" }, - "data": { - "$ref": "#/components/schemas/InviteCodeResponse" + "jid": { + "type": "string", + "deprecated": true + }, + "lid": { + "type": "string", + "deprecated": true + }, + "phoneNumber": { + "type": "string" + }, + "username": { + "type": "string" + }, + "status": { + "type": "string" + }, + "pictureId": { + "type": "string" + }, + "verifiedName": { + "type": "string" + }, + "devices": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "success", - "data" + "id", + "jid", + "lid", + "status", + "pictureId", + "verifiedName", + "devices" ] }, - "RevokeGroupInviteCodeResponse": { + "GetUserInfoResponse": { "type": "object", "properties": { "success": { @@ -3973,7 +6212,7 @@ ] }, "data": { - "$ref": "#/components/schemas/InviteCodeResponse" + "$ref": "#/components/schemas/UserInfo" } }, "required": [ @@ -3981,7 +6220,7 @@ "data" ] }, - "GetGroupParticipantsResponse": { + "GetUserDevicesResponse": { "type": "object", "properties": { "success": { @@ -3993,7 +6232,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Participant" + "type": "string" } } }, @@ -4002,105 +6241,233 @@ "data" ] }, - "GroupParticipantsRequest": { + "BusinessProfile": { "type": "object", "properties": { - "participants": { + "jid": { + "type": "string" + }, + "address": { + "type": "string" + }, + "email": { + "type": "string" + }, + "description": { + "type": "string" + }, + "websites": { "type": "array", "items": { - "type": "string" - }, - "minItems": 1 - } - }, - "required": [ - "participants" - ] - }, - "SetGroupPictureRequest": { - "type": "object", - "properties": { - "url": { + "type": "string", + "format": "uri" + } + }, + "coverPhotoId": { "type": "string" }, - "base64": { + "categories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + } + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "hoursTimeZone": { "type": "string" - } - } - }, - "GroupAdminOnlySettingRequest": { - "type": "object", - "properties": { - "adminsOnly": { - "type": "boolean" + }, + "hours": { + "type": "array", + "items": { + "type": "object", + "properties": { + "dayOfWeek": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "openTime": { + "type": "string" + }, + "closeTime": { + "type": "string" + } + }, + "required": [ + "dayOfWeek", + "mode", + "openTime", + "closeTime" + ] + } } }, "required": [ - "adminsOnly" + "jid", + "address", + "email", + "description", + "websites", + "coverPhotoId", + "categories", + "options", + "hoursTimeZone", + "hours" ] }, - "GroupMemberAddModeRequest": { + "GetBusinessProfileResponse": { "type": "object", "properties": { - "mode": { - "type": "string", + "success": { + "type": "boolean", "enum": [ - "admin_add", - "all_member_add" + true ] + }, + "data": { + "$ref": "#/components/schemas/BusinessProfile" } }, "required": [ - "mode" - ] - }, - "GroupJoinApprovalRequest": { - "type": "object", - "properties": { - "required": { - "type": "boolean" - } - }, - "required": [ - "required" + "success", + "data" ] }, - "Contact": { + "BusinessLinkedAccounts": { "type": "object", "properties": { - "lid": { - "type": "string", - "deprecated": true - }, - "phoneNumber": { - "type": "string" - }, - "name": { - "type": "string" - }, - "pushName": { - "type": "string" - }, - "businessName": { - "type": "string" + "facebookPage": { + "type": "object", + "properties": { + "hasActiveCTWAAd": { + "type": "boolean" + }, + "hasCreatedAd": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "profileSync": { + "type": "string", + "enum": [ + "disable", + "import" + ] + }, + "profilePictureUrl": { + "type": "string", + "format": "uri" + }, + "showOnProfile": { + "type": "boolean" + }, + "whatsAppAsPageButton": { + "type": "boolean" + } + }, + "required": [ + "hasActiveCTWAAd", + "hasCreatedAd", + "id", + "displayName", + "profilePictureUrl", + "showOnProfile", + "whatsAppAsPageButton" + ] }, - "profileUrl": { - "type": "string" + "facebookBusiness": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "catalogId": { + "type": "string" + }, + "catalogState": { + "type": "string", + "enum": [ + "disable", + "import" + ] + } + }, + "required": [ + "id", + "displayName" + ] }, - "id": { - "type": "string" + "instagramProfessional": { + "type": "object", + "properties": { + "handle": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "profilePictureUrl": { + "type": "string", + "format": "uri" + }, + "showOnProfile": { + "type": "boolean" + } + }, + "required": [ + "handle", + "displayName", + "profilePictureUrl", + "showOnProfile" + ] }, - "username": { - "type": "string" + "whatsAppAdIdentity": { + "type": "object", + "properties": { + "hasActiveCTWAAd": { + "type": "boolean" + }, + "hasCreatedAd": { + "type": "boolean" + }, + "id": { + "type": "string" + } + }, + "required": [ + "hasActiveCTWAAd", + "hasCreatedAd", + "id" + ] } - }, - "required": [ - "lid", - "name", - "pushName" - ] + } }, - "ListContactsResponse": { + "GetBusinessLinkedAccountsResponse": { "type": "object", "properties": { "success": { @@ -4110,10 +6477,7 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Contact" - } + "$ref": "#/components/schemas/BusinessLinkedAccounts" } }, "required": [ @@ -4121,31 +6485,54 @@ "data" ] }, - "CheckContactResult": { + "BusinessEligibility": { "type": "object", "properties": { - "exists": { - "type": "boolean" - }, - "lid": { - "type": "string", - "deprecated": true - }, - "phoneNumber": { - "type": "string" - }, - "id": { - "type": "string" - }, - "username": { - "type": "string" + "features": { + "type": "array", + "items": { + "type": "object", + "properties": { + "feature": { + "type": "string", + "enum": [ + "meta_verified", + "marketing_messages", + "genai", + "genai_image", + "meta_one", + "bb_pro" + ] + }, + "status": { + "type": "string" + }, + "expiration": { + "type": "integer" + }, + "additionalParams": { + "type": "string" + }, + "showPrivacyInterstitialToNewUsers": { + "type": "boolean" + }, + "v1Enabled": { + "type": "boolean" + } + }, + "required": [ + "feature", + "status" + ] + }, + "maxItems": 6 } }, "required": [ - "exists" + "features" ] }, - "CheckContactsResponse": { + "GetBusinessEligibilityResponse": { "type": "object", "properties": { "success": { @@ -4155,10 +6542,7 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CheckContactResult" - } + "$ref": "#/components/schemas/BusinessEligibility" } }, "required": [ @@ -4166,25 +6550,25 @@ "data" ] }, - "Blocklist": { + "GetOwnBusinessProfileResponse": { "type": "object", "properties": { - "hash": { - "type": "string" + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "jids": { - "type": "array", - "items": { - "type": "string" - } + "data": { + "$ref": "#/components/schemas/BusinessProfile" } }, "required": [ - "hash", - "jids" + "success", + "data" ] }, - "GetBlocklistResponse": { + "UpdateBusinessProfileResponse": { "type": "object", "properties": { "success": { @@ -4194,7 +6578,15 @@ ] }, "data": { - "$ref": "#/components/schemas/Blocklist" + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] } }, "required": [ @@ -4202,37 +6594,103 @@ "data" ] }, - "GetContactResponse": { + "BusinessProfileUpdateRequest": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true + "address": { + "type": "string", + "maxLength": 512 + }, + "email": { + "anyOf": [ + { + "type": "string", + "enum": [ + "" + ] + }, + { + "type": "string", + "maxLength": 320, + "format": "email" + } + ] + }, + "description": { + "type": "string", + "maxLength": 1024 + }, + "websites": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "minItems": 1, + "maxItems": 2 + }, + "hours": { + "type": "object", + "properties": { + "timeZone": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "days": { + "type": "array", + "items": { + "type": "object", + "properties": { + "dayOfWeek": { + "type": "string", + "enum": [ + "sun", + "mon", + "tue", + "wed", + "thu", + "fri", + "sat" + ] + }, + "mode": { + "type": "string", + "enum": [ + "specific_hours", + "open_24h", + "appointment_only" + ] + }, + "openTime": { + "type": "integer", + "minimum": 0, + "maximum": 1439 + }, + "closeTime": { + "type": "integer", + "minimum": 0, + "maximum": 1439 + } + }, + "required": [ + "dayOfWeek", + "mode" + ] + }, + "minItems": 1, + "maxItems": 7 + } + }, + "required": [ + "timeZone", + "days" ] - }, - "data": { - "$ref": "#/components/schemas/Contact" - } - }, - "required": [ - "success", - "data" - ] - }, - "ProfilePictureResponse": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "HTTPS URL of the profile picture" } - }, - "required": [ - "url" - ] + } }, - "GetContactPictureResponse": { + "SetBusinessCoverPhotoResponse": { "type": "object", "properties": { "success": { @@ -4242,7 +6700,15 @@ ] }, "data": { - "$ref": "#/components/schemas/ProfilePictureResponse" + "type": "object", + "properties": { + "coverPhotoId": { + "type": "string" + } + }, + "required": [ + "coverPhotoId" + ] } }, "required": [ @@ -4250,52 +6716,22 @@ "data" ] }, - "UserInfo": { + "BusinessCoverPhotoRequest": { "type": "object", "properties": { - "jid": { + "url": { "type": "string", - "deprecated": true + "maxLength": 2048, + "format": "uri" }, - "lid": { + "base64": { "type": "string", - "deprecated": true - }, - "status": { - "type": "string" - }, - "pictureId": { - "type": "string" - }, - "verifiedName": { - "type": "string" - }, - "devices": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "phoneNumber": { - "type": "string" - }, - "username": { - "type": "string" + "minLength": 1, + "maxLength": 6990508 } - }, - "required": [ - "jid", - "lid", - "status", - "pictureId", - "verifiedName", - "devices" - ] + } }, - "GetUserInfoResponse": { + "DeleteBusinessCoverPhotoResponse": { "type": "object", "properties": { "success": { @@ -4305,28 +6741,15 @@ ] }, "data": { - "$ref": "#/components/schemas/UserInfo" - } - }, - "required": [ - "success", - "data" - ] - }, - "GetUserDevicesResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" ] - }, - "data": { - "type": "array", - "items": { - "type": "string" - } } }, "required": [ @@ -4334,99 +6757,94 @@ "data" ] }, - "BusinessProfile": { + "BusinessMerchantCompliance": { "type": "object", "properties": { - "jid": { - "type": "string" - }, - "address": { - "type": "string" - }, - "email": { - "type": "string" + "entityName": { + "type": "string", + "maxLength": 256 }, - "description": { - "type": "string" + "entityType": { + "type": "string", + "enum": [ + "SOLE_PROPRIETORSHIP", + "PARTNERSHIP", + "PRIVATE_COMPANY", + "PUBLIC_COMPANY", + "LIMITED_LIABILITY_PARTNERSHIP", + "OTHER" + ] }, - "websites": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - } + "isRegistered": { + "type": "boolean" }, - "coverPhotoId": { - "type": "string" + "entityTypeCustom": { + "type": "string", + "maxLength": 256 }, - "categories": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } + "customerCare": { + "type": "object", + "properties": { + "email": { + "type": "string", + "maxLength": 254 }, - "required": [ - "id", - "name" - ] - } + "landlineNumber": { + "type": "string", + "maxLength": 64 + }, + "mobileNumber": { + "type": "string", + "maxLength": 64 + } + }, + "required": [ + "email", + "landlineNumber", + "mobileNumber" + ], + "additionalProperties": false }, - "options": { + "grievanceOfficer": { "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "hoursTimeZone": { - "type": "string" - }, - "hours": { - "type": "array", - "items": { - "type": "object", - "properties": { - "dayOfWeek": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "openTime": { - "type": "string" - }, - "closeTime": { - "type": "string" - } + "properties": { + "email": { + "type": "string", + "maxLength": 254 }, - "required": [ - "dayOfWeek", - "mode", - "openTime", - "closeTime" - ] - } + "landlineNumber": { + "type": "string", + "maxLength": 64 + }, + "mobileNumber": { + "type": "string", + "maxLength": 64 + }, + "name": { + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "email", + "landlineNumber", + "mobileNumber", + "name" + ], + "additionalProperties": false } }, "required": [ - "jid", - "address", - "email", - "description", - "websites", - "coverPhotoId", - "categories", - "options", - "hoursTimeZone", - "hours" - ] + "entityName", + "entityType", + "isRegistered", + "entityTypeCustom", + "customerCare", + "grievanceOfficer" + ], + "additionalProperties": false }, - "GetOwnBusinessProfileResponse": { + "GetBusinessMerchantComplianceResponse": { "type": "object", "properties": { "success": { @@ -4436,7 +6854,7 @@ ] }, "data": { - "$ref": "#/components/schemas/BusinessProfile" + "$ref": "#/components/schemas/BusinessMerchantCompliance" } }, "required": [ @@ -4444,7 +6862,7 @@ "data" ] }, - "UpdateBusinessProfileResponse": { + "SetBusinessMerchantComplianceResponse": { "type": "object", "properties": { "success": { @@ -4454,15 +6872,7 @@ ] }, "data": { - "type": "object", - "properties": { - "status": { - "type": "string" - } - }, - "required": [ - "status" - ] + "$ref": "#/components/schemas/BusinessMerchantCompliance" } }, "required": [ @@ -4470,103 +6880,61 @@ "data" ] }, - "BusinessProfileUpdateRequest": { + "BusinessQuickReplyMutation": { "type": "object", "properties": { - "address": { + "shortcut": { "type": "string", - "maxLength": 512 - }, - "email": { - "anyOf": [ - { - "type": "string", - "enum": [ - "" - ] - }, - { - "type": "string", - "maxLength": 320, - "format": "email" - } - ] + "minLength": 1, + "maxLength": 25, + "pattern": "^[\\p{L}\\p{N}]+$" }, - "description": { + "message": { "type": "string", - "maxLength": 1024 + "minLength": 1, + "maxLength": 65536 }, - "websites": { + "keywords": { "type": "array", "items": { "type": "string", - "maxLength": 2048, - "format": "uri" + "minLength": 1, + "maxLength": 256 }, - "minItems": 1, - "maxItems": 2 + "maxItems": 100, + "default": [] }, - "hours": { + "count": { + "type": "integer", + "minimum": 0, + "maximum": 2147483647, + "default": 0 + } + }, + "required": [ + "shortcut", + "message" + ] + }, + "BusinessQuickReply": { + "allOf": [ + { + "$ref": "#/components/schemas/BusinessQuickReplyMutation" + }, + { "type": "object", "properties": { - "timeZone": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "days": { - "type": "array", - "items": { - "type": "object", - "properties": { - "dayOfWeek": { - "type": "string", - "enum": [ - "sun", - "mon", - "tue", - "wed", - "thu", - "fri", - "sat" - ] - }, - "mode": { - "type": "string", - "enum": [ - "specific_hours", - "open_24h", - "appointment_only" - ] - }, - "openTime": { - "type": "integer", - "minimum": 0, - "maximum": 1439 - }, - "closeTime": { - "type": "integer", - "minimum": 0, - "maximum": 1439 - } - }, - "required": [ - "dayOfWeek", - "mode" - ] - }, - "minItems": 1, - "maxItems": 7 + "id": { + "type": "string" } }, "required": [ - "timeZone", - "days" + "id" ] } - } + ] }, - "SetBusinessCoverPhotoResponse": { + "CreateBusinessQuickReplyResponse": { "type": "object", "properties": { "success": { @@ -4576,38 +6944,86 @@ ] }, "data": { + "$ref": "#/components/schemas/BusinessQuickReply" + } + }, + "required": [ + "success", + "data" + ] + }, + "BusinessQuickReplyObserved": { + "allOf": [ + { + "$ref": "#/components/schemas/BusinessQuickReply" + }, + { "type": "object", "properties": { - "coverPhotoId": { - "type": "string" + "associatedLabelIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "observedAt": { + "type": "string", + "format": "date-time" } }, "required": [ - "coverPhotoId" + "associatedLabelIds", + "observedAt" ] } - }, - "required": [ - "success", - "data" ] }, - "BusinessCoverPhotoRequest": { + "BusinessQuickReplyCollection": { "type": "object", "properties": { - "url": { + "policy": { "type": "string", - "maxLength": 2048, - "format": "uri" + "enum": [ + "off", + "events", + "cache" + ] }, - "base64": { + "status": { "type": "string", - "minLength": 1, - "maxLength": 6990508 + "enum": [ + "disabled", + "unknown", + "partial", + "fresh" + ] + }, + "unknownReason": { + "type": "string", + "enum": [ + "observation_disabled", + "not_retained", + "not_observed" + ] + }, + "observedAt": { + "type": "string", + "format": "date-time" + }, + "quickReplies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusinessQuickReplyObserved" + } } - } + }, + "required": [ + "policy", + "status", + "quickReplies" + ] }, - "DeleteBusinessCoverPhotoResponse": { + "ListBusinessQuickRepliesResponse": { "type": "object", "properties": { "success": { @@ -4617,15 +7033,25 @@ ] }, "data": { - "type": "object", - "properties": { - "status": { - "type": "string" - } - }, - "required": [ - "status" + "$ref": "#/components/schemas/BusinessQuickReplyCollection" + } + }, + "required": [ + "success", + "data" + ] + }, + "SetBusinessQuickReplyResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true ] + }, + "data": { + "$ref": "#/components/schemas/BusinessQuickReply" } }, "required": [ @@ -4633,7 +7059,7 @@ "data" ] }, - "GetBusinessProfileResponse": { + "DeleteBusinessQuickReplyResponse": { "type": "object", "properties": { "success": { @@ -4641,9 +7067,24 @@ "enum": [ true ] - }, - "data": { - "$ref": "#/components/schemas/BusinessProfile" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "DELETED" + ] + } + }, + "required": [ + "id", + "status" + ] } }, "required": [ @@ -4959,262 +7400,45 @@ } }, "required": [ - "availability", - "listingDetails", - "types", - "properties" - ] - } - }, - "required": [ - "id", - "name", - "price", - "currency", - "hidden", - "sanctioned", - "media", - "status" - ] - }, - "BusinessCatalogPage": { - "type": "object", - "properties": { - "next": { - "type": "string" - }, - "previous": { - "type": "string" - }, - "products": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BusinessProduct" - } - } - }, - "required": [ - "products" - ] - }, - "GetBusinessCatalogResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessCatalogPage" - } - }, - "required": [ - "success", - "data" - ] - }, - "GetBusinessProductResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessProduct" - } - }, - "required": [ - "success", - "data" - ] - }, - "BusinessCollection": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "products": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BusinessProduct" - } - }, - "status": { - "type": "object", - "properties": { - "status": { - "type": "string" - }, - "canAppeal": { - "type": "boolean" - }, - "commerceUrl": { - "type": "string", - "format": "uri" - }, - "rejectReason": { - "type": "string" - } - }, - "required": [ - "canAppeal" - ] - } - }, - "required": [ - "id", - "name", - "products", - "status" - ] - }, - "BusinessCollectionPage": { - "type": "object", - "properties": { - "next": { - "type": "string" - }, - "collections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BusinessCollection" - } - } - }, - "required": [ - "collections" - ] - }, - "GetBusinessCollectionsResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessCollectionPage" - } - }, - "required": [ - "success", - "data" - ] - }, - "GetBusinessCollectionResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessCollection" - } - }, - "required": [ - "success", - "data" - ] - }, - "BusinessOrder": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "createdAt": { - "type": "integer", - "minimum": 0 - }, - "catalogId": { - "type": "string" - }, - "price": { - "type": "object", - "properties": { - "subtotal": { - "type": "string" - }, - "total": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "priceStatus": { - "type": "string" - } - }, - "required": [ - "subtotal", - "total", - "currency" - ] - }, - "products": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "imageId": { - "type": "string" - }, - "imageUrl": { - "type": "string", - "format": "uri" - }, - "price": { - "type": "string" - }, - "currency": { - "type": "string" - }, - "name": { - "type": "string" - }, - "quantity": { - "type": "integer", - "minimum": 0 - }, - "variantProperties": { - "type": "string" - } - }, - "required": [ - "id", - "price", - "currency", - "name", - "quantity" - ] - } + "availability", + "listingDetails", + "types", + "properties" + ] } }, "required": [ "id", - "createdAt", + "name", "price", + "currency", + "hidden", + "sanctioned", + "media", + "status" + ] + }, + "BusinessCatalogPage": { + "type": "object", + "properties": { + "next": { + "type": "string" + }, + "previous": { + "type": "string" + }, + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusinessProduct" + } + } + }, + "required": [ "products" ] }, - "GetBusinessOrderResponse": { + "GetBusinessCatalogResponse": { "type": "object", "properties": { "success": { @@ -5224,7 +7448,7 @@ ] }, "data": { - "$ref": "#/components/schemas/BusinessOrder" + "$ref": "#/components/schemas/BusinessCatalogPage" } }, "required": [ @@ -5232,41 +7456,36 @@ "data" ] }, - "BusinessOrderLookupRequest": { + "CreateBusinessCatalogResponse": { "type": "object", "properties": { - "token": { - "type": "string", - "minLength": 1, - "maxLength": 8192 + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "success" + ] } }, "required": [ - "token" + "success", + "data" ] }, - "ResolveLIDResult": { - "type": "object", - "properties": { - "lid": { - "type": "string", - "deprecated": true - }, - "phoneNumber": { - "type": "string" - }, - "id": { - "type": "string" - }, - "username": { - "type": "string" - }, - "keyRequired": { - "type": "boolean" - } - } - }, - "ResolveLIDsResponse": { + "SetBusinessCartEnabledResponse": { "type": "object", "properties": { "success": { @@ -5276,7 +7495,18 @@ ] }, "data": { - "$ref": "#/components/schemas/ResolveLIDResult" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "success" + ] } }, "required": [ @@ -5284,33 +7514,19 @@ "data" ] }, - "Channel": { + "BusinessCartSettingRequest": { "type": "object", "properties": { - "lid": { - "type": "string" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "profileUrl": { - "type": "string" - }, - "followers": { - "type": "number" - }, - "muted": { - "type": "boolean" - }, - "preview": { + "enabled": { "type": "boolean" } - } + }, + "required": [ + "enabled" + ], + "additionalProperties": false }, - "ListChannelsResponse": { + "GetBusinessProductResponse": { "type": "object", "properties": { "success": { @@ -5320,10 +7536,7 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Channel" - } + "$ref": "#/components/schemas/BusinessProduct" } }, "required": [ @@ -5331,7 +7544,7 @@ "data" ] }, - "CreateChannelResponse": { + "CreateBusinessProductResponse": { "type": "object", "properties": { "success": { @@ -5341,7 +7554,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Channel" + "$ref": "#/components/schemas/BusinessProduct" } }, "required": [ @@ -5349,25 +7562,145 @@ "data" ] }, - "CreateChannelRequest": { + "BusinessProductMutationRequest": { "type": "object", "properties": { "name": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 256 + }, + "description": { + "type": "string", + "maxLength": 4096 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "price": { + "type": "string", + "pattern": "^\\d{1,18}$" + }, + "salePrice": { + "type": "string", + "pattern": "^\\d{1,18}$" + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "retailerId": { + "type": "string", + "maxLength": 256 + }, + "hidden": { + "type": "boolean" + }, + "images": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 4096, + "format": "uri", + "description": "HTTPS image source fetched through Polymorfa's SSRF-safe downloader" + }, + "base64": { + "type": "string", + "minLength": 1, + "maxLength": 22369624 + }, + "mediaUrl": { + "type": "string", + "maxLength": 4096, + "format": "uri", + "description": "Already-uploaded WhatsApp or Meta media URL reused without downloading" + } + }, + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 10 + }, + "videoUrls": { + "type": "array", + "items": { + "type": "string", + "maxLength": 4096, + "format": "uri" + }, + "maxItems": 10 + }, + "complianceCategory": { + "type": "string", + "maxLength": 128 + }, + "compliance": { + "type": "object", + "properties": { + "countryCodeOrigin": { + "type": "string", + "maxLength": 3 + }, + "importerName": { + "type": "string", + "maxLength": 256 + }, + "importerAddress": { + "type": "object", + "properties": { + "street1": { + "type": "string", + "maxLength": 512 + }, + "street2": { + "type": "string", + "maxLength": 512 + }, + "city": { + "type": "string", + "maxLength": 256 + }, + "region": { + "type": "string", + "maxLength": 256 + }, + "postalCode": { + "type": "string", + "maxLength": 64 + }, + "countryCode": { + "type": "string", + "maxLength": 3 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false }, - "description": { - "type": "string" + "width": { + "type": "integer", + "minimum": 1, + "maximum": 1024 }, - "picture": { - "type": "string" + "height": { + "type": "integer", + "minimum": 1, + "maximum": 1024 } }, "required": [ - "name" - ] + "name", + "images" + ], + "additionalProperties": false }, - "GetChannelResponse": { + "UpdateBusinessProductResponse": { "type": "object", "properties": { "success": { @@ -5377,7 +7710,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Channel" + "$ref": "#/components/schemas/BusinessProduct" } }, "required": [ @@ -5385,49 +7718,7 @@ "data" ] }, - "ChannelMessage": { - "type": "object", - "properties": { - "serverId": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true - }, - "id": { - "type": "string" - }, - "type": { - "type": "string" - }, - "timestamp": { - "type": "string", - "format": "date-time" - }, - "views": { - "type": "integer", - "minimum": 0 - }, - "reactionCounts": { - "type": "object", - "additionalProperties": { - "type": "integer", - "minimum": 0 - } - }, - "text": { - "type": "string" - } - }, - "required": [ - "serverId", - "id", - "type", - "timestamp", - "views", - "reactionCounts" - ] - }, - "ListChannelMessagesResponse": { + "DeleteBusinessProductResponse": { "type": "object", "properties": { "success": { @@ -5437,10 +7728,16 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ChannelMessage" - } + "type": "object", + "properties": { + "deletedCount": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "deletedCount" + ] } }, "required": [ @@ -5448,7 +7745,7 @@ "data" ] }, - "ListChannelMessageUpdatesResponse": { + "SetBusinessProductVisibilityResponse": { "type": "object", "properties": { "success": { @@ -5458,10 +7755,18 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ChannelMessage" - } + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "success" + ] } }, "required": [ @@ -5469,50 +7774,19 @@ "data" ] }, - "ChannelReactionRequest": { - "type": "object", - "properties": { - "reaction": { - "type": "string", - "maxLength": 32 - } - }, - "required": [ - "reaction" - ] - }, - "ChannelLiveUpdates": { - "type": "object", - "properties": { - "durationSeconds": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true - } - }, - "required": [ - "durationSeconds" - ] - }, - "ChannelLiveUpdatesResponse": { + "BusinessProductVisibilityRequest": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/ChannelLiveUpdates" + "hidden": { + "type": "boolean" } }, "required": [ - "success", - "data" - ] + "hidden" + ], + "additionalProperties": false }, - "DeleteChannelResponse": { + "AppealBusinessProductResponse": { "type": "object", "properties": { "success": { @@ -5524,15 +7798,15 @@ "data": { "type": "object", "properties": { - "status": { - "type": "string", + "success": { + "type": "boolean", "enum": [ - "DELETED" + true ] } }, "required": [ - "status" + "success" ] } }, @@ -5541,85 +7815,82 @@ "data" ] }, - "AsyncAcceptedResponse": { + "BusinessCatalogAppealRequest": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "reason": { + "type": "string", + "minLength": 1, + "maxLength": 4096 + } + }, + "required": [ + "reason" + ], + "additionalProperties": false + }, + "BusinessCollection": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "data": { + "name": { + "type": "string" + }, + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusinessProduct" + } + }, + "status": { "type": "object", "properties": { - "requestId": { + "status": { + "type": "string" + }, + "canAppeal": { + "type": "boolean" + }, + "commerceUrl": { "type": "string", - "format": "uuid" + "format": "uri" + }, + "rejectReason": { + "type": "string" } }, "required": [ - "requestId" - ] - } - }, - "required": [ - "success", - "data" - ] - }, - "SetPresenceRequest": { - "type": "object", - "properties": { - "presence": { - "type": "string", - "enum": [ - "available", - "unavailable" + "canAppeal" ] } }, "required": [ - "presence" + "id", + "name", + "products", + "status" ] }, - "PresenceData": { + "BusinessCollectionPage": { "type": "object", "properties": { - "desired": { - "type": "string", - "enum": [ - "available", - "unavailable" - ] - }, - "desiredAt": { - "type": "string", - "format": "date-time" - }, - "lastSent": { - "type": "string", - "enum": [ - "available", - "unavailable" - ] - }, - "lastSentAt": { - "type": "string", - "format": "date-time" + "next": { + "type": "string" }, - "authoritative": { - "type": "boolean", - "enum": [ - false - ] + "collections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BusinessCollection" + } } }, "required": [ - "authoritative" + "collections" ] }, - "GetPresenceResponse": { + "GetBusinessCollectionsResponse": { "type": "object", "properties": { "success": { @@ -5629,7 +7900,7 @@ ] }, "data": { - "$ref": "#/components/schemas/PresenceData" + "$ref": "#/components/schemas/BusinessCollectionPage" } }, "required": [ @@ -5637,116 +7908,62 @@ "data" ] }, - "ChatPresenceData": { + "CreateBusinessCollectionResponse": { "type": "object", "properties": { - "policy": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "status": { - "type": "string", - "enum": [ - "unknown", - "fresh", - "stale" - ] - }, - "unknownReason": { - "type": "string", - "enum": [ - "disabled", - "not_observed", - "suspended" - ] - }, - "available": { - "type": "boolean" - }, - "lastSeen": { - "type": "string", - "format": "date-time" - }, - "observedAt": { - "type": "string", - "format": "date-time" - }, - "subscriptionExpiresAt": { - "type": "string", - "format": "date-time" - }, - "stale": { - "type": "boolean" - }, - "typingPolicy": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "typingStatus": { - "type": "string", - "enum": [ - "unknown", - "fresh", - "stale" - ] - }, - "typingUnknownReason": { - "type": "string", + "success": { + "type": "boolean", "enum": [ - "disabled", - "not_observed", - "suspended" + true ] }, - "chatState": { + "data": { "type": "object", "properties": { - "sender": { + "id": { "type": "string" }, - "state": { - "type": "string", - "enum": [ - "composing", - "paused" - ] - }, - "media": { + "reviewStatus": { "type": "string" - }, - "observedAt": { - "type": "string", - "format": "date-time" - }, - "stale": { - "type": "boolean" } }, "required": [ - "sender", - "state", - "observedAt", - "stale" + "id", + "reviewStatus" ] } }, "required": [ - "policy", - "status", - "stale", - "typingPolicy", - "typingStatus" + "success", + "data" ] }, - "GetChatPresenceResponse": { + "BusinessCollectionCreateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "productIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 1, + "maxItems": 100 + } + }, + "required": [ + "name", + "productIds" + ], + "additionalProperties": false + }, + "GetBusinessCollectionResponse": { "type": "object", "properties": { "success": { @@ -5756,7 +7973,7 @@ ] }, "data": { - "$ref": "#/components/schemas/ChatPresenceData" + "$ref": "#/components/schemas/BusinessCollection" } }, "required": [ @@ -5764,26 +7981,76 @@ "data" ] }, - "PresenceSubscriptionData": { + "ReorderBusinessCollectionsResponse": { "type": "object", "properties": { - "status": { - "type": "string", + "success": { + "type": "boolean", "enum": [ - "SUBSCRIBED" + true ] }, - "expiresAt": { - "type": "string", - "format": "date-time" + "data": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "success" + ] } }, "required": [ - "status", - "expiresAt" + "success", + "data" ] }, - "SubscribePresenceResponse": { + "BusinessCollectionReorderRequest": { + "type": "object", + "properties": { + "moves": { + "type": "array", + "items": { + "type": "object", + "properties": { + "collectionId": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "fromIndex": { + "type": "integer", + "minimum": 0, + "maximum": 99 + }, + "toIndex": { + "type": "integer", + "minimum": 0, + "maximum": 99 + } + }, + "required": [ + "collectionId", + "fromIndex", + "toIndex" + ], + "additionalProperties": false + }, + "minItems": 1, + "maxItems": 100 + } + }, + "required": [ + "moves" + ], + "additionalProperties": false + }, + "UpdateBusinessCollectionResponse": { "type": "object", "properties": { "success": { @@ -5793,7 +8060,19 @@ ] }, "data": { - "$ref": "#/components/schemas/PresenceSubscriptionData" + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reviewStatus": { + "type": "string" + } + }, + "required": [ + "id", + "reviewStatus" + ] } }, "required": [ @@ -5801,55 +8080,38 @@ "data" ] }, - "ProjectObservationPolicy": { + "BusinessCollectionUpdateRequest": { "type": "object", "properties": { - "projectId": { - "type": "string", - "format": "uuid" - }, - "presenceMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "typingMode": { + "name": { "type": "string", - "enum": [ - "off", - "events", - "cache" - ] + "minLength": 1, + "maxLength": 256 }, - "labelMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache", - "project" - ] + "addProductIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 0, + "maxItems": 100 }, - "quickReplyMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] + "removeProductIds": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "minItems": 0, + "maxItems": 100 } }, - "required": [ - "projectId", - "presenceMode", - "typingMode", - "labelMode" - ] + "additionalProperties": false }, - "GetProjectObservationPolicyResponse": { + "DeleteBusinessCollectionResponse": { "type": "object", "properties": { "success": { @@ -5859,7 +8121,18 @@ ] }, "data": { - "$ref": "#/components/schemas/ProjectObservationPolicy" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "success" + ] } }, "required": [ @@ -5867,7 +8140,7 @@ "data" ] }, - "UpdateProjectObservationPolicyResponse": { + "AppealBusinessCollectionResponse": { "type": "object", "properties": { "success": { @@ -5877,7 +8150,18 @@ ] }, "data": { - "$ref": "#/components/schemas/ProjectObservationPolicy" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "success" + ] } }, "required": [ @@ -5885,159 +8169,143 @@ "data" ] }, - "SessionObservationPolicy": { + "BusinessOrder": { "type": "object", "properties": { - "sessionName": { + "id": { "type": "string" }, - "projectId": { - "type": "string", - "format": "uuid" + "createdAt": { + "type": "integer", + "minimum": 0 }, - "project": { - "type": "object", - "properties": { - "presenceMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "typingMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "labelMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache", - "project" - ] - }, - "quickReplyMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - } - }, - "required": [ - "presenceMode", - "typingMode", - "labelMode" - ] + "catalogId": { + "type": "string" }, - "override": { + "price": { "type": "object", "properties": { - "presenceMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "events", - "cache" - ] + "subtotal": { + "type": "string" }, - "typingMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "events", - "cache" - ] + "total": { + "type": "string" }, - "labelMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "events", - "cache", - "project" - ] + "currency": { + "type": "string" }, - "quickReplyMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "events", - "cache" - ] + "priceStatus": { + "type": "string" } }, "required": [ - "presenceMode", - "typingMode", - "labelMode" + "subtotal", + "total", + "currency" ] }, - "effective": { - "type": "object", - "properties": { - "presenceMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "typingMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "labelMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache", - "project" - ] + "products": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "imageId": { + "type": "string" + }, + "imageUrl": { + "type": "string", + "format": "uri" + }, + "price": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "name": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": 0 + }, + "variantProperties": { + "type": "string" + } }, - "quickReplyMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - } - }, - "required": [ - "presenceMode", - "typingMode", - "labelMode" + "required": [ + "id", + "price", + "currency", + "name", + "quantity" + ] + } + } + }, + "required": [ + "id", + "createdAt", + "price", + "products" + ] + }, + "GetBusinessOrderResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true ] + }, + "data": { + "$ref": "#/components/schemas/BusinessOrder" + } + }, + "required": [ + "success", + "data" + ] + }, + "BusinessOrderLookupRequest": { + "type": "object", + "properties": { + "token": { + "type": "string", + "minLength": 1, + "maxLength": 8192 } }, "required": [ - "sessionName", - "projectId", - "project", - "override", - "effective" + "token" ] }, - "GetSessionObservationPolicyResponse": { + "ResolveLIDResult": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lid": { + "type": "string", + "deprecated": true + }, + "phoneNumber": { + "type": "string" + }, + "username": { + "type": "string" + }, + "keyRequired": { + "type": "boolean" + } + } + }, + "ResolveLIDsResponse": { "type": "object", "properties": { "success": { @@ -6047,7 +8315,7 @@ ] }, "data": { - "$ref": "#/components/schemas/SessionObservationPolicy" + "$ref": "#/components/schemas/ResolveLIDResult" } }, "required": [ @@ -6055,7 +8323,34 @@ "data" ] }, - "UpdateSessionObservationPolicyResponse": { + "UserSecurityCode": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^\\d+@lid$" + }, + "phoneNumber": { + "type": "string" + }, + "username": { + "type": "string" + }, + "numericCode": { + "type": "string", + "pattern": "^\\d{60}$" + }, + "qrCode": { + "type": "string" + } + }, + "required": [ + "id", + "numericCode", + "qrCode" + ] + }, + "UserSecurityCodeResponse": { "type": "object", "properties": { "success": { @@ -6065,7 +8360,7 @@ ] }, "data": { - "$ref": "#/components/schemas/SessionObservationPolicy" + "$ref": "#/components/schemas/UserSecurityCode" } }, "required": [ @@ -6073,25 +8368,54 @@ "data" ] }, - "ProfileData": { + "Channel": { "type": "object", "properties": { + "lid": { + "type": "string" + }, "name": { "type": "string" }, - "status": { + "description": { "type": "string" }, - "profilePicUrl": { + "profileUrl": { "type": "string" + }, + "followers": { + "type": "number" + }, + "muted": { + "type": "boolean" + }, + "preview": { + "type": "boolean" + } + } + }, + "ListChannelsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Channel" + } } }, "required": [ - "name", - "status" + "success", + "data" ] }, - "GetProfileResponse": { + "CreateChannelResponse": { "type": "object", "properties": { "success": { @@ -6101,7 +8425,7 @@ ] }, "data": { - "$ref": "#/components/schemas/ProfileData" + "$ref": "#/components/schemas/Channel" } }, "required": [ @@ -6109,88 +8433,152 @@ "data" ] }, - "SetProfileNameRequest": { + "CreateChannelRequest": { "type": "object", "properties": { "name": { "type": "string", "minLength": 1 + }, + "description": { + "type": "string" + }, + "picture": { + "type": "string" } }, "required": [ "name" ] }, - "SetProfileStatusRequest": { + "GetChannelResponse": { "type": "object", "properties": { - "status": { - "type": "string" + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/Channel" } }, "required": [ - "status" + "success", + "data" ] }, - "SetProfilePictureRequest": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "base64": { - "type": "string" - } - } - }, - "PrivacySettings": { + "ChannelMessage": { "type": "object", "properties": { - "groupAdd": { - "type": "string" - }, - "lastSeen": { - "type": "string" + "serverId": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true }, - "status": { + "id": { "type": "string" }, - "profile": { + "type": { "type": "string" }, - "readReceipts": { - "type": "string" + "timestamp": { + "type": "string", + "format": "date-time" }, - "online": { - "type": "string" + "views": { + "type": "integer", + "minimum": 0 }, - "callAdd": { - "type": "string" + "reactionCounts": { + "type": "object", + "additionalProperties": { + "type": "integer", + "minimum": 0 + } }, - "messages": { + "text": { "type": "string" + } + }, + "required": [ + "serverId", + "id", + "type", + "timestamp", + "views", + "reactionCounts" + ] + }, + "ListChannelMessagesResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "defense": { - "type": "string" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelMessage" + } + } + }, + "required": [ + "success", + "data" + ] + }, + "ListChannelMessageUpdatesResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "stickers": { - "type": "string" + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelMessage" + } + } + }, + "required": [ + "success", + "data" + ] + }, + "ChannelReactionRequest": { + "type": "object", + "properties": { + "reaction": { + "type": "string", + "maxLength": 32 + } + }, + "required": [ + "reaction" + ] + }, + "ChannelLiveUpdates": { + "type": "object", + "properties": { + "durationSeconds": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true } }, "required": [ - "groupAdd", - "lastSeen", - "status", - "profile", - "readReceipts", - "online", - "callAdd", - "messages", - "defense", - "stickers" + "durationSeconds" ] }, - "PrivacySettingsResponse": { + "ChannelLiveUpdatesResponse": { "type": "object", "properties": { "success": { @@ -6200,7 +8588,7 @@ ] }, "data": { - "$ref": "#/components/schemas/PrivacySettings" + "$ref": "#/components/schemas/ChannelLiveUpdates" } }, "required": [ @@ -6208,43 +8596,36 @@ "data" ] }, - "DefaultDisappearingTimerRequest": { + "DeleteChannelResponse": { "type": "object", "properties": { - "durationSeconds": { - "anyOf": [ - { - "type": "number", - "enum": [ - 0 - ] - }, - { - "type": "number", - "enum": [ - 86400 - ] - }, - { - "type": "number", - "enum": [ - 604800 - ] - }, - { - "type": "number", + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "type": "object", + "properties": { + "status": { + "type": "string", "enum": [ - 7776000 + "DELETED" ] } + }, + "required": [ + "status" ] } }, "required": [ - "durationSeconds" + "success", + "data" ] }, - "UpdatedPrivacySettingsResponse": { + "AsyncAcceptedResponse": { "type": "object", "properties": { "success": { @@ -6254,7 +8635,16 @@ ] }, "data": { - "$ref": "#/components/schemas/PrivacySettings" + "type": "object", + "properties": { + "requestId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "requestId" + ] } }, "required": [ @@ -6262,58 +8652,76 @@ "data" ] }, - "SetPrivacySettingRequest": { + "SetPresenceRequest": { "type": "object", "properties": { - "value": { + "presence": { "type": "string", "enum": [ - "all", - "contacts", - "contact_allowlist", - "contact_blacklist", - "match_last_seen", - "known", - "none", - "on_standard", - "off" + "available", + "unavailable" ] } }, "required": [ - "value" + "presence" ] }, - "Label": { + "PresenceData": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "color": { - "type": "integer" + "desired": { + "type": "string", + "enum": [ + "available", + "unavailable" + ] }, - "orderIndex": { - "type": "integer" + "desiredAt": { + "type": "string", + "format": "date-time" }, - "chatCount": { - "type": "integer" + "lastSent": { + "type": "string", + "enum": [ + "available", + "unavailable" + ] }, - "observedAt": { + "lastSentAt": { "type": "string", "format": "date-time" + }, + "authoritative": { + "type": "boolean", + "enum": [ + false + ] } }, "required": [ - "id", - "name", - "color" + "authoritative" ] }, - "LabelCollection": { + "GetPresenceResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/PresenceData" + } + }, + "required": [ + "success", + "data" + ] + }, + "ChatPresenceData": { "type": "object", "properties": { "policy": { @@ -6321,81 +8729,108 @@ "enum": [ "off", "events", - "cache", - "project" + "cache" ] }, "status": { "type": "string", "enum": [ - "disabled", "unknown", - "partial", - "fresh" + "fresh", + "stale" ] }, "unknownReason": { "type": "string", "enum": [ - "observation_disabled", - "not_retained", + "disabled", "not_observed", - "expired" + "suspended" ] }, + "available": { + "type": "boolean" + }, + "lastSeen": { + "type": "string", + "format": "date-time" + }, "observedAt": { "type": "string", "format": "date-time" }, - "expiresAt": { + "subscriptionExpiresAt": { "type": "string", "format": "date-time" }, - "labels": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Label" - } - } - }, - "required": [ - "policy", - "status", - "labels" - ] - }, - "LabelReadData": { - "anyOf": [ - { - "type": "array", - "items": { - "$ref": "#/components/schemas/Label" - } + "stale": { + "type": "boolean" }, - { - "$ref": "#/components/schemas/LabelCollection" - } - ] - }, - "ListLabelsResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", + "typingPolicy": { + "type": "string", "enum": [ - true + "off", + "events", + "cache" + ] + }, + "typingStatus": { + "type": "string", + "enum": [ + "unknown", + "fresh", + "stale" + ] + }, + "typingUnknownReason": { + "type": "string", + "enum": [ + "disabled", + "not_observed", + "suspended" + ] + }, + "chatState": { + "type": "object", + "properties": { + "sender": { + "type": "string" + }, + "state": { + "type": "string", + "enum": [ + "composing", + "paused" + ] + }, + "media": { + "type": "string" + }, + "observedAt": { + "type": "string", + "format": "date-time" + }, + "stale": { + "type": "boolean" + } + }, + "required": [ + "sender", + "state", + "observedAt", + "stale" ] - }, - "data": { - "$ref": "#/components/schemas/LabelReadData" } }, "required": [ - "success", - "data" + "policy", + "status", + "stale", + "typingPolicy", + "typingStatus" ] }, - "CreateLabelResponse": { + "GetChatPresenceResponse": { "type": "object", "properties": { "success": { @@ -6405,7 +8840,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Label" + "$ref": "#/components/schemas/ChatPresenceData" } }, "required": [ @@ -6413,40 +8848,26 @@ "data" ] }, - "CreateLabelRequest": { + "PresenceSubscriptionData": { "type": "object", "properties": { - "name": { + "status": { "type": "string", - "minLength": 1, - "maxLength": 100 + "enum": [ + "SUBSCRIBED" + ] }, - "color": { - "type": "integer", - "minimum": 0, - "maximum": 19 + "expiresAt": { + "type": "string", + "format": "date-time" } }, "required": [ - "name" + "status", + "expiresAt" ] }, - "UpdateLabelRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "color": { - "type": "integer", - "minimum": 0, - "maximum": 19 - } - } - }, - "GetChatLabelsResponse": { + "SubscribePresenceResponse": { "type": "object", "properties": { "success": { @@ -6456,7 +8877,7 @@ ] }, "data": { - "$ref": "#/components/schemas/LabelReadData" + "$ref": "#/components/schemas/PresenceSubscriptionData" } }, "required": [ @@ -6464,116 +8885,247 @@ "data" ] }, - "SetChatLabelsRequest": { + "ProjectObservationPolicy": { "type": "object", "properties": { - "labels": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "maxItems": 20 + "projectId": { + "type": "string", + "format": "uuid" + }, + "presenceMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "typingMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "labelMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache", + "project" + ] + }, + "quickReplyMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] } }, "required": [ - "labels" + "projectId", + "presenceMode", + "typingMode", + "labelMode", + "quickReplyMode" ] }, - "RetryConfig": { + "GetProjectObservationPolicyResponse": { "type": "object", "properties": { - "attempts": { - "type": "integer", - "minimum": 0 - }, - "delaySeconds": { - "type": "integer", - "minimum": 0 + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "policy": { - "type": "string", - "description": "linear, exponential, or constant" + "data": { + "$ref": "#/components/schemas/ProjectObservationPolicy" } }, "required": [ - "attempts", - "delaySeconds", - "policy" + "success", + "data" ] }, - "WebhookHeader": { + "UpdateProjectObservationPolicyResponse": { "type": "object", "properties": { - "name": { - "type": "string" + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "value": { - "type": "string" + "data": { + "$ref": "#/components/schemas/ProjectObservationPolicy" } }, "required": [ - "name", - "value" + "success", + "data" ] }, - "Webhook": { + "SessionObservationPolicy": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "tenantId": { - "type": "string" - }, - "session": { - "type": "string" - }, - "url": { + "sessionName": { "type": "string" }, - "events": { - "type": "array", - "items": { - "type": "string" - } - }, - "retries": { - "$ref": "#/components/schemas/RetryConfig" - }, - "headers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookHeader" - } + "projectId": { + "type": "string", + "format": "uuid" }, - "enabled": { - "type": "boolean" + "project": { + "type": "object", + "properties": { + "presenceMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "typingMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "labelMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache", + "project" + ] + }, + "quickReplyMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + } + }, + "required": [ + "presenceMode", + "typingMode", + "labelMode", + "quickReplyMode" + ] }, - "format": { - "type": "string", - "enum": [ - "native", - "meta" + "override": { + "type": "object", + "properties": { + "presenceMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache" + ] + }, + "typingMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache" + ] + }, + "labelMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache", + "project" + ] + }, + "quickReplyMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache" + ] + } + }, + "required": [ + "presenceMode", + "typingMode", + "labelMode", + "quickReplyMode" ] }, - "createdAt": { - "type": "string" + "effective": { + "type": "object", + "properties": { + "presenceMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "typingMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "labelMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache", + "project" + ] + }, + "quickReplyMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + } + }, + "required": [ + "presenceMode", + "typingMode", + "labelMode", + "quickReplyMode" + ] } }, "required": [ - "id", - "tenantId", - "url", - "events", - "retries", - "headers", - "enabled", - "createdAt" + "sessionName", + "projectId", + "project", + "override", + "effective" ] }, - "ListWebhooksResponse": { + "GetSessionObservationPolicyResponse": { "type": "object", "properties": { "success": { @@ -6583,10 +9135,7 @@ ] }, "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Webhook" - } + "$ref": "#/components/schemas/SessionObservationPolicy" } }, "required": [ @@ -6594,7 +9143,7 @@ "data" ] }, - "CreateWebhookResponse": { + "UpdateSessionObservationPolicyResponse": { "type": "object", "properties": { "success": { @@ -6604,7 +9153,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Webhook" + "$ref": "#/components/schemas/SessionObservationPolicy" } }, "required": [ @@ -6612,48 +9161,25 @@ "data" ] }, - "CreateWebhookRequest": { + "ProfileData": { "type": "object", "properties": { - "session": { + "name": { "type": "string" }, - "url": { - "type": "string", - "format": "uri" - }, - "events": { - "type": "array", - "items": { - "type": "string" - } - }, - "hmacKey": { + "status": { "type": "string" }, - "retries": { - "$ref": "#/components/schemas/RetryConfig" - }, - "headers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookHeader" - } - }, - "format": { - "type": "string", - "enum": [ - "native", - "meta" - ], - "description": "\"native\" (default) = Polymorfa event envelope. \"meta\" = Meta Cloud API webhook shape (object/entry/changes/value), signed with X-Hub-Signature-256." + "profilePicUrl": { + "type": "string" } }, "required": [ - "url" + "name", + "status" ] }, - "GetWebhookResponse": { + "GetProfileResponse": { "type": "object", "properties": { "success": { @@ -6663,7 +9189,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Webhook" + "$ref": "#/components/schemas/ProfileData" } }, "required": [ @@ -6671,97 +9197,142 @@ "data" ] }, - "UpdateWebhookResponse": { + "SetProfileNameRequest": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Webhook" + "name": { + "type": "string", + "minLength": 1 } }, "required": [ - "success", - "data" + "name" ] }, - "UpdateWebhookRequest": { + "SetProfileStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "SetProfilePictureRequest": { "type": "object", "properties": { "url": { - "type": "string", - "format": "uri" - }, - "events": { - "type": "array", - "items": { - "type": "string" - } - }, - "hmacKey": { "type": "string" }, - "retries": { - "$ref": "#/components/schemas/RetryConfig" - }, - "headers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/WebhookHeader" - } - }, - "enabled": { - "type": "boolean" - }, - "format": { - "type": "string", - "enum": [ - "native", - "meta" - ] + "base64": { + "type": "string" } } }, - "MediaInfo": { + "PrivacySettings": { "type": "object", "properties": { - "id": { + "groupAdd": { "type": "string" }, - "session": { + "lastSeen": { "type": "string" }, - "messageId": { + "status": { "type": "string" }, - "mimeType": { + "profile": { "type": "string" }, - "fileLength": { - "type": "integer", - "minimum": 0 + "readReceipts": { + "type": "string" }, - "persisted": { - "type": "boolean" + "online": { + "type": "string" }, - "s3Url": { + "callAdd": { + "type": "string" + }, + "messages": { + "type": "string" + }, + "defense": { + "type": "string" + }, + "stickers": { "type": "string" } }, "required": [ - "id", - "session", - "messageId", - "mimeType", - "fileLength", - "persisted" + "groupAdd", + "lastSeen", + "status", + "profile", + "readReceipts", + "online", + "callAdd", + "messages", + "defense", + "stickers" + ] + }, + "PrivacySettingsResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/PrivacySettings" + } + }, + "required": [ + "success", + "data" + ] + }, + "DefaultDisappearingTimerRequest": { + "type": "object", + "properties": { + "durationSeconds": { + "anyOf": [ + { + "type": "number", + "enum": [ + 0 + ] + }, + { + "type": "number", + "enum": [ + 86400 + ] + }, + { + "type": "number", + "enum": [ + 604800 + ] + }, + { + "type": "number", + "enum": [ + 7776000 + ] + } + ] + } + }, + "required": [ + "durationSeconds" ] }, - "GetMediaInfoResponse": { + "UpdatedPrivacySettingsResponse": { "type": "object", "properties": { "success": { @@ -6771,7 +9342,7 @@ ] }, "data": { - "$ref": "#/components/schemas/MediaInfo" + "$ref": "#/components/schemas/PrivacySettings" } }, "required": [ @@ -6779,7 +9350,29 @@ "data" ] }, - "MessageTemplate": { + "SetPrivacySettingRequest": { + "type": "object", + "properties": { + "value": { + "type": "string", + "enum": [ + "all", + "contacts", + "contact_allowlist", + "contact_blacklist", + "match_last_seen", + "known", + "none", + "on_standard", + "off" + ] + } + }, + "required": [ + "value" + ] + }, + "Label": { "type": "object", "properties": { "id": { @@ -6788,89 +9381,91 @@ "name": { "type": "string" }, - "category": { - "type": "string" - }, - "language": { - "type": "string" - }, - "status": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "definition": { - "nullable": true - }, - "sampleValues": { - "nullable": true + "color": { + "type": "integer" }, - "cloudLinks": { - "type": "array", - "items": { - "nullable": true - } + "orderIndex": { + "type": "integer" }, - "createdAt": { - "type": "number" + "chatCount": { + "type": "integer" }, - "updatedAt": { - "type": "number" + "observedAt": { + "type": "string", + "format": "date-time" } }, "required": [ "id", "name", - "category", - "language", - "status", - "kind", - "cloudLinks", - "createdAt", - "updatedAt" + "color" ] }, - "ListProjectTemplatesResponse": { + "LabelCollection": { "type": "object", "properties": { - "success": { - "type": "boolean", + "policy": { + "type": "string", "enum": [ - true + "off", + "events", + "cache", + "project" ] }, - "data": { + "status": { + "type": "string", + "enum": [ + "disabled", + "unknown", + "partial", + "fresh" + ] + }, + "unknownReason": { + "type": "string", + "enum": [ + "observation_disabled", + "not_retained", + "not_observed", + "expired" + ] + }, + "observedAt": { + "type": "string", + "format": "date-time" + }, + "expiresAt": { + "type": "string", + "format": "date-time" + }, + "labels": { "type": "array", "items": { - "$ref": "#/components/schemas/MessageTemplate" + "$ref": "#/components/schemas/Label" } } }, "required": [ - "success", - "data" + "policy", + "status", + "labels" ] }, - "GetProjectTemplateResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "LabelReadData": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Label" + } }, - "data": { - "$ref": "#/components/schemas/MessageTemplate" + { + "$ref": "#/components/schemas/LabelCollection" } - }, - "required": [ - "success", - "data" ] }, - "CreateProjectTemplateResponse": { + "ListLabelsResponse": { "type": "object", "properties": { "success": { @@ -6880,7 +9475,7 @@ ] }, "data": { - "$ref": "#/components/schemas/MessageTemplate" + "$ref": "#/components/schemas/LabelReadData" } }, "required": [ @@ -6888,33 +9483,7 @@ "data" ] }, - "CreateMessageTemplateRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "pattern": "^[a-z0-9_]+$" - }, - "definition": { - "type": "object", - "additionalProperties": { - "nullable": true - } - }, - "sampleValues": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "name", - "definition" - ] - }, - "UpdateProjectTemplateResponse": { + "CreateLabelResponse": { "type": "object", "properties": { "success": { @@ -6924,7 +9493,7 @@ ] }, "data": { - "$ref": "#/components/schemas/MessageTemplate" + "$ref": "#/components/schemas/Label" } }, "required": [ @@ -6932,71 +9501,40 @@ "data" ] }, - "UpdateMessageTemplateRequest": { + "CreateLabelRequest": { "type": "object", "properties": { "name": { "type": "string", - "minLength": 1 - }, - "status": { - "type": "string" - }, - "definition": { - "type": "object", - "additionalProperties": { - "nullable": true - } - }, - "sampleValues": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - }, - "PreviewProjectTemplateResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "minLength": 1, + "maxLength": 100 }, - "data": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "color": { + "type": "integer", + "minimum": 0, + "maximum": 19 } }, "required": [ - "success", - "data" + "name" ] }, - "PreviewMessageTemplateRequest": { + "UpdateLabelRequest": { "type": "object", "properties": { - "values": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "surface": { + "name": { "type": "string", - "enum": [ - "cloud", - "whatsmeow", - "sandbox" - ] + "minLength": 1, + "maxLength": 100 + }, + "color": { + "type": "integer", + "minimum": 0, + "maximum": 19 } } }, - "SubmitProjectTemplateResponse": { + "GetChatLabelsResponse": { "type": "object", "properties": { "success": { @@ -7006,10 +9544,7 @@ ] }, "data": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/LabelReadData" } }, "required": [ @@ -7017,134 +9552,116 @@ "data" ] }, - "SubmitMessageTemplateRequest": { + "SetChatLabelsRequest": { "type": "object", "properties": { - "session": { - "type": "string", - "minLength": 1 + "labels": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "maxItems": 20 } }, "required": [ - "session" + "labels" ] }, - "Campaign": { + "RetryConfig": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "status": { - "type": "string" + "attempts": { + "type": "integer", + "minimum": 0 }, - "templateId": { - "type": "string", - "nullable": true + "delaySeconds": { + "type": "integer", + "minimum": 0 }, - "recipientListId": { + "policy": { "type": "string", - "nullable": true - }, - "recipientCount": { - "type": "number" - }, - "sentCount": { - "type": "number" - }, - "deliveredCount": { - "type": "number" - }, - "readCount": { - "type": "number" - }, - "failedCount": { - "type": "number" - }, - "skippedCount": { - "type": "number" - }, - "scheduledAt": { - "type": "number", - "nullable": true - }, - "launchedAt": { - "type": "number", - "nullable": true - }, - "completedAt": { - "type": "number", - "nullable": true - }, - "createdAt": { - "type": "number" - }, - "updatedAt": { - "type": "number" + "description": "linear, exponential, or constant" } }, "required": [ - "id", - "name", - "status", - "templateId", - "recipientListId", - "recipientCount", - "sentCount", - "deliveredCount", - "readCount", - "failedCount", - "skippedCount", - "scheduledAt", - "launchedAt", - "completedAt", - "createdAt", - "updatedAt" + "attempts", + "delaySeconds", + "policy" ] }, - "ListCampaignsResponse": { + "WebhookHeader": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "name": { + "type": "string" }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Campaign" - } + "value": { + "type": "string" } }, "required": [ - "success", - "data" + "name", + "value" ] }, - "GetCampaignResponse": { + "Webhook": { "type": "object", "properties": { - "success": { - "type": "boolean", + "id": { + "type": "string" + }, + "tenantId": { + "type": "string" + }, + "session": { + "type": "string" + }, + "url": { + "type": "string" + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "retries": { + "$ref": "#/components/schemas/RetryConfig" + }, + "headers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookHeader" + } + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", "enum": [ - true + "native", + "meta" ] }, - "data": { - "$ref": "#/components/schemas/Campaign" + "createdAt": { + "type": "string" } }, "required": [ - "success", - "data" + "id", + "tenantId", + "url", + "events", + "retries", + "headers", + "enabled", + "createdAt" ] }, - "CampaignAnalyticsResponse": { + "ListWebhooksResponse": { "type": "object", "properties": { "success": { @@ -7154,9 +9671,9 @@ ] }, "data": { - "type": "object", - "additionalProperties": { - "nullable": true + "type": "array", + "items": { + "$ref": "#/components/schemas/Webhook" } } }, @@ -7165,7 +9682,7 @@ "data" ] }, - "CreateCampaignResponse": { + "CreateWebhookResponse": { "type": "object", "properties": { "success": { @@ -7175,7 +9692,7 @@ ] }, "data": { - "$ref": "#/components/schemas/Campaign" + "$ref": "#/components/schemas/Webhook" } }, "required": [ @@ -7183,79 +9700,48 @@ "data" ] }, - "CreateCampaignRequest": { + "CreateWebhookRequest": { "type": "object", "properties": { - "name": { + "session": { + "type": "string" + }, + "url": { "type": "string", - "minLength": 1 + "format": "uri" }, - "templateId": { - "type": "string" + "events": { + "type": "array", + "items": { + "type": "string" + } }, - "recipientListId": { + "hmacKey": { "type": "string" }, - "senderConfig": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "retries": { + "$ref": "#/components/schemas/RetryConfig" }, - "scheduledAt": { - "type": "number" - } - }, - "required": [ - "name" - ] - }, - "CampaignOperation": { - "allOf": [ - { - "$ref": "#/components/schemas/Campaign" + "headers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookHeader" + } }, - { - "type": "object", - "properties": { - "operationId": { - "type": "string", - "format": "uuid" - } - }, - "required": [ - "operationId" - ] - } - ] - }, - "LaunchCampaignResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", + "format": { + "type": "string", "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/CampaignOperation" + "native", + "meta" + ], + "description": "\"native\" (default) = Polymorfa event envelope. \"meta\" = Meta Cloud API webhook shape (object/entry/changes/value), signed with X-Hub-Signature-256." } }, "required": [ - "success", - "data" + "url" ] }, - "LaunchCampaignRequest": { - "type": "object", - "properties": { - "scheduledAt": { - "type": "number" - } - } - }, - "PauseCampaignResponse": { + "GetWebhookResponse": { "type": "object", "properties": { "success": { @@ -7265,7 +9751,7 @@ ] }, "data": { - "$ref": "#/components/schemas/CampaignOperation" + "$ref": "#/components/schemas/Webhook" } }, "required": [ @@ -7273,7 +9759,7 @@ "data" ] }, - "ResumeCampaignResponse": { + "UpdateWebhookResponse": { "type": "object", "properties": { "success": { @@ -7283,7 +9769,7 @@ ] }, "data": { - "$ref": "#/components/schemas/CampaignOperation" + "$ref": "#/components/schemas/Webhook" } }, "required": [ @@ -7291,25 +9777,79 @@ "data" ] }, - "StopCampaignResponse": { + "UpdateWebhookRequest": { "type": "object", "properties": { - "success": { - "type": "boolean", + "url": { + "type": "string", + "format": "uri" + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "hmacKey": { + "type": "string" + }, + "retries": { + "$ref": "#/components/schemas/RetryConfig" + }, + "headers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebhookHeader" + } + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", "enum": [ - true + "native", + "meta" ] + } + } + }, + "MediaInfo": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "data": { - "$ref": "#/components/schemas/CampaignOperation" + "session": { + "type": "string" + }, + "messageId": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "fileLength": { + "type": "integer", + "minimum": 0 + }, + "persisted": { + "type": "boolean" + }, + "s3Url": { + "type": "string" } }, "required": [ - "success", - "data" + "id", + "session", + "messageId", + "mimeType", + "fileLength", + "persisted" ] }, - "RequeueCampaignResponse": { + "GetMediaInfoResponse": { "type": "object", "properties": { "success": { @@ -7319,10 +9859,7 @@ ] }, "data": { - "type": "object", - "additionalProperties": { - "nullable": true - } + "$ref": "#/components/schemas/MediaInfo" } }, "required": [ @@ -7330,38 +9867,53 @@ "data" ] }, - "RequeueCampaignRequest": { - "type": "object", - "properties": { - "includeSkippedError": { - "type": "boolean" - } - } - }, - "WidgetSessionResponse": { + "MessageTemplate": { "type": "object", "properties": { - "widgetUrl": { + "id": { "type": "string" }, - "redirectUrl": { + "name": { "type": "string" }, - "clientToken": { + "category": { "type": "string" }, - "session": { + "language": { + "type": "string" + }, + "status": { + "type": "string" + }, + "kind": { "type": "string" + }, + "definition": {}, + "sampleValues": {}, + "cloudLinks": { + "type": "array", + "items": {} + }, + "createdAt": { + "type": "number" + }, + "updatedAt": { + "type": "number" } }, "required": [ - "widgetUrl", - "redirectUrl", - "clientToken", - "session" + "id", + "name", + "category", + "language", + "status", + "kind", + "cloudLinks", + "createdAt", + "updatedAt" ] }, - "WidgetSessionResponseWrapper": { + "ListProjectTemplatesResponse": { "type": "object", "properties": { "success": { @@ -7371,7 +9923,10 @@ ] }, "data": { - "$ref": "#/components/schemas/WidgetSessionResponse" + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageTemplate" + } } }, "required": [ @@ -7379,160 +9934,67 @@ "data" ] }, - "CreateWidgetSessionRequest": { + "GetProjectTemplateResponse": { "type": "object", "properties": { - "mode": { - "type": "string", - "enum": [ - "embedded", - "redirect" - ] - }, - "projectId": { - "type": "string", - "format": "uuid" - }, - "methods": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "qr", - "pairing", - "cloud-api" - ] - } - }, - "businessName": { - "type": "string", - "minLength": 1, - "maxLength": 120 - }, - "historySync": { - "type": "string", - "enum": [ - "force_on", - "force_off", - "ask" - ] - }, - "sessionName": { - "type": "string" - }, - "callbackUrl": { - "type": "string", - "maxLength": 2048, - "format": "uri", - "pattern": "^[Hh][Tt][Tt][Pp][Ss]://", - "description": "Exact HTTPS URL without credentials." - }, - "allowedOrigins": { - "type": "array", - "items": { - "type": "string", - "maxLength": 2048, - "format": "uri", - "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[^/?#@,]+/?$", - "description": "Exact HTTPS origin without credentials, path, query, fragment, or comma. A trailing root slash is removed." - }, - "maxItems": 50 - }, - "theme": { - "type": "string", + "success": { + "type": "boolean", "enum": [ - "light", - "dark", - "system" - ] - }, - "accent": { - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$" - }, - "prefillPhone": { - "type": "string", - "pattern": "^\\+?\\d{6,15}$" - }, - "autoSubmitPhone": { - "type": "boolean" - }, - "analytics": { - "type": "object", - "properties": { - "posthogKey": { - "type": "string" - }, - "posthogHost": { - "type": "string" - } - }, - "required": [ - "posthogKey" - ] - }, - "metaApp": { - "type": "object", - "properties": { - "appId": { - "type": "string", - "minLength": 1 - }, - "appSecret": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "appId", - "appSecret" + true ] + }, + "data": { + "$ref": "#/components/schemas/MessageTemplate" } - } + }, + "required": [ + "success", + "data" + ] }, - "WidgetHandoffResponse": { + "CreateProjectTemplateResponse": { "type": "object", "properties": { - "handoffUrl": { - "type": "string" - }, - "metaQrSvg": { - "type": "string" + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "expiresAt": { - "type": "string" + "data": { + "$ref": "#/components/schemas/MessageTemplate" } }, "required": [ - "handoffUrl", - "metaQrSvg", - "expiresAt" + "success", + "data" ] }, - "WidgetHandoffState": { + "CreateMessageTemplateRequest": { "type": "object", "properties": { - "qr": { + "name": { "type": "string", - "nullable": true - }, - "status": { - "type": "string" + "minLength": 1, + "pattern": "^[a-z0-9_]+$" }, - "sessionName": { - "type": "string" + "definition": { + "type": "object", + "additionalProperties": {} }, - "businessName": { - "type": "string" + "sampleValues": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "required": [ - "qr", - "status", - "sessionName" + "name", + "definition" ] }, - "CreateBusinessProductResponse": { + "UpdateProjectTemplateResponse": { "type": "object", "properties": { "success": { @@ -7542,7 +10004,7 @@ ] }, "data": { - "$ref": "#/components/schemas/BusinessProduct" + "$ref": "#/components/schemas/MessageTemplate" } }, "required": [ @@ -7550,145 +10012,174 @@ "data" ] }, - "BusinessProductMutationRequest": { + "UpdateMessageTemplateRequest": { "type": "object", "properties": { "name": { "type": "string", - "minLength": 1, - "maxLength": 256 + "minLength": 1 }, - "description": { - "type": "string", - "maxLength": 4096 + "status": { + "type": "string" }, - "currency": { - "type": "string", - "pattern": "^[A-Z]{3}$" + "definition": { + "type": "object", + "additionalProperties": {} }, - "price": { - "type": "string", - "pattern": "^\\d{1,18}$" + "sampleValues": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "PreviewProjectTemplateResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "salePrice": { - "type": "string", - "pattern": "^\\d{1,18}$" + "data": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "success", + "data" + ] + }, + "PreviewMessageTemplateRequest": { + "type": "object", + "properties": { + "values": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "url": { + "surface": { "type": "string", - "maxLength": 2048, - "format": "uri" + "enum": [ + "cloud", + "whatsmeow", + "sandbox" + ] + } + } + }, + "SubmitProjectTemplateResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] }, - "retailerId": { + "data": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "success", + "data" + ] + }, + "SubmitMessageTemplateRequest": { + "type": "object", + "properties": { + "session": { "type": "string", - "maxLength": 256 - }, - "hidden": { - "type": "boolean" - }, - "images": { - "type": "array", - "items": { - "type": "object", - "properties": { - "url": { - "type": "string", - "maxLength": 4096, - "format": "uri", - "description": "HTTPS image source fetched through Polymorfa's SSRF-safe downloader" - }, - "base64": { - "type": "string", - "minLength": 1, - "maxLength": 22369624 - }, - "mediaUrl": { - "type": "string", - "maxLength": 4096, - "format": "uri", - "description": "Already-uploaded WhatsApp or Meta media URL reused without downloading" - } - }, - "additionalProperties": false - }, - "minItems": 1, - "maxItems": 10 - }, - "videoUrls": { - "type": "array", - "items": { - "type": "string", - "maxLength": 4096, - "format": "uri" - }, - "maxItems": 10 + "minLength": 1 + } + }, + "required": [ + "session" + ] + }, + "Campaign": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "complianceCategory": { - "type": "string", - "maxLength": 128 + "name": { + "type": "string" }, - "compliance": { - "type": "object", - "properties": { - "countryCodeOrigin": { - "type": "string", - "maxLength": 3 - }, - "importerName": { - "type": "string", - "maxLength": 256 - }, - "importerAddress": { - "type": "object", - "properties": { - "street1": { - "type": "string", - "maxLength": 512 - }, - "street2": { - "type": "string", - "maxLength": 512 - }, - "city": { - "type": "string", - "maxLength": 256 - }, - "region": { - "type": "string", - "maxLength": 256 - }, - "postalCode": { - "type": "string", - "maxLength": 64 - }, - "countryCode": { - "type": "string", - "maxLength": 3 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "status": { + "type": "string" }, - "width": { - "type": "integer", - "minimum": 1, - "maximum": 1024 + "templateId": { + "type": "string", + "nullable": true }, - "height": { - "type": "integer", - "minimum": 1, - "maximum": 1024 + "recipientListId": { + "type": "string", + "nullable": true + }, + "recipientCount": { + "type": "number" + }, + "sentCount": { + "type": "number" + }, + "deliveredCount": { + "type": "number" + }, + "readCount": { + "type": "number" + }, + "failedCount": { + "type": "number" + }, + "skippedCount": { + "type": "number" + }, + "scheduledAt": { + "type": "number", + "nullable": true + }, + "launchedAt": { + "type": "number", + "nullable": true + }, + "completedAt": { + "type": "number", + "nullable": true + }, + "createdAt": { + "type": "number" + }, + "updatedAt": { + "type": "number" } }, "required": [ + "id", "name", - "images" - ], - "additionalProperties": false + "status", + "templateId", + "recipientListId", + "recipientCount", + "sentCount", + "deliveredCount", + "readCount", + "failedCount", + "skippedCount", + "scheduledAt", + "launchedAt", + "completedAt", + "createdAt", + "updatedAt" + ] }, - "UpdateBusinessProductResponse": { + "ListCampaignsResponse": { "type": "object", "properties": { "success": { @@ -7698,7 +10189,10 @@ ] }, "data": { - "$ref": "#/components/schemas/BusinessProduct" + "type": "array", + "items": { + "$ref": "#/components/schemas/Campaign" + } } }, "required": [ @@ -7706,7 +10200,7 @@ "data" ] }, - "DeleteBusinessProductResponse": { + "GetCampaignResponse": { "type": "object", "properties": { "success": { @@ -7716,16 +10210,7 @@ ] }, "data": { - "type": "object", - "properties": { - "deletedCount": { - "type": "integer", - "minimum": 0 - } - }, - "required": [ - "deletedCount" - ] + "$ref": "#/components/schemas/Campaign" } }, "required": [ @@ -7733,7 +10218,7 @@ "data" ] }, - "CreateBusinessCollectionResponse": { + "CampaignAnalyticsResponse": { "type": "object", "properties": { "success": { @@ -7744,18 +10229,7 @@ }, "data": { "type": "object", - "properties": { - "id": { - "type": "string" - }, - "reviewStatus": { - "type": "string" - } - }, - "required": [ - "id", - "reviewStatus" - ] + "additionalProperties": {} } }, "required": [ @@ -7763,32 +10237,7 @@ "data" ] }, - "BusinessCollectionCreateRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "productIds": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "minItems": 1, - "maxItems": 100 - } - }, - "required": [ - "name", - "productIds" - ], - "additionalProperties": false - }, - "ReorderBusinessCollectionsResponse": { + "CreateCampaignResponse": { "type": "object", "properties": { "success": { @@ -7798,18 +10247,7 @@ ] }, "data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "success" - ] + "$ref": "#/components/schemas/Campaign" } }, "required": [ @@ -7817,108 +10255,51 @@ "data" ] }, - "BusinessCollectionReorderRequest": { + "CreateCampaignRequest": { "type": "object", "properties": { - "moves": { - "type": "array", - "items": { - "type": "object", - "properties": { - "collectionId": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "fromIndex": { - "type": "integer", - "minimum": 0, - "maximum": 99 - }, - "toIndex": { - "type": "integer", - "minimum": 0, - "maximum": 99 - } - }, - "required": [ - "collectionId", - "fromIndex", - "toIndex" - ], - "additionalProperties": false - }, - "minItems": 1, - "maxItems": 100 + "name": { + "type": "string", + "minLength": 1 + }, + "templateId": { + "type": "string" + }, + "recipientListId": { + "type": "string" + }, + "senderConfig": { + "type": "object", + "additionalProperties": {} + }, + "scheduledAt": { + "type": "number" } }, "required": [ - "moves" - ], - "additionalProperties": false + "name" + ] }, - "UpdateBusinessCollectionResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "CampaignOperation": { + "allOf": [ + { + "$ref": "#/components/schemas/Campaign" }, - "data": { + { "type": "object", "properties": { - "id": { - "type": "string" - }, - "reviewStatus": { - "type": "string" + "operationId": { + "type": "string", + "format": "uuid" } }, "required": [ - "id", - "reviewStatus" + "operationId" ] } - }, - "required": [ - "success", - "data" - ] - }, - "BusinessCollectionUpdateRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "addProductIds": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "minItems": 0, - "maxItems": 100 - }, - "removeProductIds": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "minItems": 0, - "maxItems": 100 - } - }, - "additionalProperties": false + ] }, - "DeleteBusinessCollectionResponse": { + "LaunchCampaignResponse": { "type": "object", "properties": { "success": { @@ -7928,18 +10309,7 @@ ] }, "data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "success" - ] + "$ref": "#/components/schemas/CampaignOperation" } }, "required": [ @@ -7947,7 +10317,15 @@ "data" ] }, - "CreateBusinessCatalogResponse": { + "LaunchCampaignRequest": { + "type": "object", + "properties": { + "scheduledAt": { + "type": "number" + } + } + }, + "PauseCampaignResponse": { "type": "object", "properties": { "success": { @@ -7957,18 +10335,7 @@ ] }, "data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "success" - ] + "$ref": "#/components/schemas/CampaignOperation" } }, "required": [ @@ -7976,7 +10343,7 @@ "data" ] }, - "SetBusinessCartEnabledResponse": { + "ResumeCampaignResponse": { "type": "object", "properties": { "success": { @@ -7986,18 +10353,7 @@ ] }, "data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "success" - ] + "$ref": "#/components/schemas/CampaignOperation" } }, "required": [ @@ -8005,19 +10361,7 @@ "data" ] }, - "BusinessCartSettingRequest": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "additionalProperties": false - }, - "SetBusinessProductVisibilityResponse": { + "StopCampaignResponse": { "type": "object", "properties": { "success": { @@ -8027,18 +10371,7 @@ ] }, "data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "success" - ] + "$ref": "#/components/schemas/CampaignOperation" } }, "required": [ @@ -8046,19 +10379,7 @@ "data" ] }, - "BusinessProductVisibilityRequest": { - "type": "object", - "properties": { - "hidden": { - "type": "boolean" - } - }, - "required": [ - "hidden" - ], - "additionalProperties": false - }, - "AppealBusinessProductResponse": { + "RequeueCampaignResponse": { "type": "object", "properties": { "success": { @@ -8069,17 +10390,7 @@ }, "data": { "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "success" - ] + "additionalProperties": {} } }, "required": [ @@ -8087,21 +10398,38 @@ "data" ] }, - "BusinessCatalogAppealRequest": { + "RequeueCampaignRequest": { "type": "object", "properties": { - "reason": { - "type": "string", - "minLength": 1, - "maxLength": 4096 + "includeSkippedError": { + "type": "boolean" + } + } + }, + "WidgetSessionResponse": { + "type": "object", + "properties": { + "widgetUrl": { + "type": "string" + }, + "redirectUrl": { + "type": "string" + }, + "clientToken": { + "type": "string" + }, + "session": { + "type": "string" } }, "required": [ - "reason" - ], - "additionalProperties": false + "widgetUrl", + "redirectUrl", + "clientToken", + "session" + ] }, - "AppealBusinessCollectionResponse": { + "WidgetSessionResponseWrapper": { "type": "object", "properties": { "success": { @@ -8111,18 +10439,7 @@ ] }, "data": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - } - }, - "required": [ - "success" - ] + "$ref": "#/components/schemas/WidgetSessionResponse" } }, "required": [ @@ -8130,985 +10447,1175 @@ "data" ] }, - "BusinessMerchantCompliance": { + "CreateWidgetSessionRequest": { "type": "object", "properties": { - "entityName": { + "mode": { "type": "string", - "maxLength": 256 + "enum": [ + "embedded", + "redirect" + ] }, - "entityType": { + "projectId": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "type": "string", + "format": "uuid" + }, + "methods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "qr", + "pairing", + "cloud-api" + ] + } + }, + "businessName": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "historySync": { "type": "string", "enum": [ - "SOLE_PROPRIETORSHIP", - "PARTNERSHIP", - "PRIVATE_COMPANY", - "PUBLIC_COMPANY", - "LIMITED_LIABILITY_PARTNERSHIP", - "OTHER" + "force_on", + "force_off", + "ask" ] }, - "isRegistered": { - "type": "boolean" + "sessionName": { + "type": "string" }, - "entityTypeCustom": { + "callbackUrl": { "type": "string", - "maxLength": 256 + "maxLength": 2048, + "format": "uri", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://", + "description": "Exact HTTPS URL without credentials." }, - "customerCare": { + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "format": "uri", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[^/?#@,]+/?$", + "description": "Exact HTTPS origin without credentials, path, query, fragment, or comma. A trailing root slash is removed." + }, + "maxItems": 50 + }, + "theme": { + "type": "string", + "enum": [ + "light", + "dark", + "system" + ] + }, + "accent": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "prefillPhone": { + "type": "string", + "pattern": "^\\+?\\d{6,15}$" + }, + "autoSubmitPhone": { + "type": "boolean" + }, + "analytics": { "type": "object", "properties": { - "email": { - "type": "string", - "maxLength": 254 - }, - "landlineNumber": { - "type": "string", - "maxLength": 64 + "posthogKey": { + "type": "string" }, - "mobileNumber": { - "type": "string", - "maxLength": 64 + "posthogHost": { + "type": "string" } }, "required": [ - "email", - "landlineNumber", - "mobileNumber" - ], - "additionalProperties": false + "posthogKey" + ] }, - "grievanceOfficer": { + "metaApp": { "type": "object", "properties": { - "email": { - "type": "string", - "maxLength": 254 - }, - "landlineNumber": { - "type": "string", - "maxLength": 64 - }, - "mobileNumber": { + "appId": { "type": "string", - "maxLength": 64 + "minLength": 1 }, - "name": { + "appSecret": { "type": "string", - "maxLength": 256 + "minLength": 1 } }, "required": [ - "email", - "landlineNumber", - "mobileNumber", - "name" - ], - "additionalProperties": false - } - }, - "required": [ - "entityName", - "entityType", - "isRegistered", - "entityTypeCustom", - "customerCare", - "grievanceOfficer" - ], - "additionalProperties": false - }, - "GetBusinessMerchantComplianceResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true + "appId", + "appSecret" ] - }, - "data": { - "$ref": "#/components/schemas/BusinessMerchantCompliance" } - }, - "required": [ - "success", - "data" - ] + } }, - "SetBusinessMerchantComplianceResponse": { + "WidgetHandoffResponse": { "type": "object", "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "handoffUrl": { + "type": "string" }, - "data": { - "$ref": "#/components/schemas/BusinessMerchantCompliance" + "metaQrSvg": { + "type": "string" + }, + "expiresAt": { + "type": "string" } }, "required": [ - "success", - "data" + "handoffUrl", + "metaQrSvg", + "expiresAt" ] }, - "BusinessQuickReplyUpdatePayload": { + "WidgetHandoffState": { "type": "object", "properties": { - "id": { + "qr": { + "type": "string", + "nullable": true + }, + "status": { "type": "string" }, - "shortcut": { + "sessionName": { "type": "string" }, - "message": { + "businessName": { "type": "string" + } + }, + "required": [ + "qr", + "status", + "sessionName" + ] + } + }, + "securitySchemes": { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "Polymorfa API key (pmfa_ prefix) or client token (pmfa_ct_ prefix)." + } + } + }, + "paths": { + "/ping": { + "get": { + "operationId": "ping", + "tags": [ + "Health" + ], + "summary": "Liveness probe", + "security": [], + "responses": { + "200": { + "description": "Pong", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PingResponse" + } + } + } + } + } + } + }, + "/health": { + "get": { + "operationId": "getHealth", + "tags": [ + "Health" + ], + "summary": "Full service health check", + "security": [], + "responses": { + "200": { + "description": "All services healthy", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } }, - "keywords": { - "type": "array", - "items": { - "type": "string" + "503": { + "description": "One or more services unhealthy or draining", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + } + } + } + }, + "/api/info/version": { + "get": { + "operationId": "getVersion", + "tags": [ + "Info" + ], + "summary": "Get API version info", + "security": [], + "responses": { + "200": { + "description": "Version info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionResponse" + } + } + } + } + } + } + }, + "/api/info/status": { + "get": { + "operationId": "getStatus", + "tags": [ + "Info" + ], + "summary": "Get API runtime status", + "security": [], + "responses": { + "200": { + "description": "Status info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StatusResponse" + } + } + } + } + } + } + }, + "/v1/bridge/route": { + "get": { + "operationId": "getBridgeRoute", + "x-required-scope": "bridge:connect", + "tags": [ + "Bridge" + ], + "summary": "Verify a project token, resolve live project authority, and return the regional WSS endpoint.", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "wsUrl": { + "type": "string", + "format": "uri" + }, + "region": { + "type": "string", + "enum": [ + "BR", + "US", + "IN", + "Auto" + ] + }, + "kind": { + "type": "string", + "enum": [ + "sandbox", + "production" + ] + }, + "signal": { + "type": "string", + "enum": [ + "customer", + "bartender" + ] + }, + "tokenKind": { + "type": "string", + "enum": [ + "project" + ] + }, + "expiresAt": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "wsUrl", + "region", + "kind", + "signal", + "tokenKind", + "expiresAt" + ] + } + } } }, - "count": { - "type": "integer", - "minimum": 0 - }, - "deleted": { - "type": "boolean" - }, - "associatedLabelIds": { - "type": "array", - "items": { - "type": "string" + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "error", + "message" + ] + } + } } }, - "observedAt": { - "type": "integer" - }, - "fromFullSync": { - "type": "boolean" - } - }, - "required": [ - "id", - "shortcut", - "message", - "keywords", - "count", - "deleted", - "associatedLabelIds", - "observedAt", - "fromFullSync" - ] - }, - "BusinessQuickReplyUpdateEvent": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique event identifier" - }, - "session": { - "type": "string", - "description": "Session that produced this event" - }, - "timestamp": { - "type": "string", - "description": "ISO 8601 timestamp" - }, - "event": { - "type": "string", - "description": "Event type" - }, - "payload": { - "$ref": "#/components/schemas/BusinessQuickReplyUpdatePayload" + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "error", + "message" + ] + } + } + } } }, - "required": [ - "id", - "session", - "timestamp", - "event", - "payload" - ] - }, - "BusinessQuickReplyMutation": { - "type": "object", - "properties": { - "shortcut": { - "type": "string", - "minLength": 1, - "maxLength": 25, - "pattern": "^[\\p{L}\\p{N}]+$" - }, - "message": { - "type": "string", - "minLength": 1, - "maxLength": 65536 - }, - "keywords": { - "type": "array", - "items": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "maxItems": 100, - "default": [] - }, - "count": { - "type": "integer", - "minimum": 0, - "maximum": 2147483647, - "default": 0 - } + "x-mint": { + "content": "bridge:connect" }, - "required": [ - "shortcut", - "message" - ] - }, - "BusinessQuickReply": { - "allOf": [ - { - "$ref": "#/components/schemas/BusinessQuickReplyMutation" - }, + "description": "**Required scope:** `bridge:connect`" + } + }, + "/api/client-tokens": { + "post": { + "operationId": "mintClientToken", + "tags": [ + "Client Tokens" + ], + "summary": "Mint a short-lived browser-safe client token", + "security": [ { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ] - } - ] - }, - "CreateBusinessQuickReplyResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessQuickReply" - } - }, - "required": [ - "success", - "data" - ] - }, - "SetBusinessQuickReplyResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessQuickReply" + "BearerAuth": [] } - }, - "required": [ - "success", - "data" - ] - }, - "DeleteBusinessQuickReplyResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "DELETED" - ] - } - }, - "required": [ - "id", - "status" - ] + ], + "x-required-scope": "sessions:manage", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MintClientTokenRequest" + } + } } }, - "required": [ - "success", - "data" - ] - }, - "BusinessQuickReplyObserved": { - "allOf": [ - { - "$ref": "#/components/schemas/BusinessQuickReply" + "responses": { + "200": { + "description": "Token minted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MintClientTokenResponse" + } + } + } }, - { - "type": "object", - "properties": { - "associatedLabelIds": { - "type": "array", - "items": { - "type": "string" + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } - }, - "observedAt": { - "type": "string", - "format": "date-time" } - }, - "required": [ - "associatedLabelIds", - "observedAt" - ] - } - ] - }, - "BusinessQuickReplyCollection": { - "type": "object", - "properties": { - "policy": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] + } }, - "status": { - "type": "string", - "enum": [ - "disabled", - "unknown", - "partial", - "fresh" - ] + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "unknownReason": { - "type": "string", - "enum": [ - "observation_disabled", - "not_retained", - "not_observed" - ] + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "observedAt": { - "type": "string", - "format": "date-time" + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "quickReplies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BusinessQuickReplyObserved" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } } }, - "required": [ - "policy", - "status", - "quickReplies" - ] - }, - "ListBusinessQuickRepliesResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessQuickReplyCollection" - } + "x-mint": { + "content": "sessions:manage" }, - "required": [ - "success", - "data" - ] - }, - "BusinessLinkedAccounts": { - "type": "object", - "properties": { - "facebookPage": { - "type": "object", - "properties": { - "hasActiveCTWAAd": { - "type": "boolean" - }, - "hasCreatedAd": { - "type": "boolean" - }, - "id": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "profileSync": { - "type": "string", - "enum": [ - "disable", - "import" - ] - }, - "profilePictureUrl": { - "type": "string", - "format": "uri" - }, - "showOnProfile": { - "type": "boolean" - }, - "whatsAppAsPageButton": { - "type": "boolean" - } + "description": "**Required scope:** `sessions:manage`" + } + }, + "/api/sessions/{session}/client-rules": { + "get": { + "operationId": "getClientRules", + "tags": [ + "Client Tokens" + ], + "summary": "Get client-token rules for a session", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "sessions:manage", + "parameters": [ + { + "schema": { + "type": "string" }, - "required": [ - "hasActiveCTWAAd", - "hasCreatedAd", - "id", - "displayName", - "profilePictureUrl", - "showOnProfile", - "whatsAppAsPageButton" - ] + "required": true, + "name": "session", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Client rules", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetClientRulesResponse" + } + } + } }, - "facebookBusiness": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "catalogId": { - "type": "string" - }, - "catalogState": { - "type": "string", - "enum": [ - "disable", - "import" - ] + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - }, - "required": [ - "id", - "displayName" - ] + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "instagramProfessional": { - "type": "object", - "properties": { - "handle": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "profilePictureUrl": { - "type": "string", - "format": "uri" - }, - "showOnProfile": { - "type": "boolean" + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - }, - "required": [ - "handle", - "displayName", - "profilePictureUrl", - "showOnProfile" - ] + } }, - "whatsAppAdIdentity": { - "type": "object", - "properties": { - "hasActiveCTWAAd": { - "type": "boolean" - }, - "hasCreatedAd": { - "type": "boolean" - }, - "id": { - "type": "string" + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - }, - "required": [ - "hasActiveCTWAAd", - "hasCreatedAd", - "id" - ] - } - } - }, - "GetBusinessLinkedAccountsResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + } }, - "data": { - "$ref": "#/components/schemas/BusinessLinkedAccounts" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "required": [ - "success", - "data" - ] + "x-mint": { + "content": "sessions:manage" + }, + "description": "**Required scope:** `sessions:manage`" }, - "BusinessEligibility": { - "type": "object", - "properties": { - "features": { - "type": "array", - "items": { - "type": "object", - "properties": { - "feature": { - "type": "string", - "enum": [ - "meta_verified", - "marketing_messages", - "genai", - "genai_image", - "meta_one", - "bb_pro" - ] - }, - "status": { - "type": "string" - }, - "expiration": { - "type": "integer" - }, - "additionalParams": { - "type": "string" - }, - "showPrivacyInterstitialToNewUsers": { - "type": "boolean" - }, - "v1Enabled": { - "type": "boolean" - } - }, - "required": [ - "feature", - "status" - ] + "put": { + "operationId": "setClientRules", + "tags": [ + "Client Tokens" + ], + "summary": "Set client-token rules for a session", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "sessions:manage", + "parameters": [ + { + "schema": { + "type": "string" }, - "maxItems": 6 + "required": true, + "name": "session", + "in": "path" } - }, - "required": [ - "features" - ] - }, - "GetBusinessEligibilityResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/BusinessEligibility" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetClientRulesRequest" + } + } } }, - "required": [ - "success", - "data" - ] - }, - "UserSecurityCode": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^\\d+@lid$" + "responses": { + "200": { + "description": "Rules saved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } }, - "phoneNumber": { - "type": "string" + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "username": { - "type": "string" + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "numericCode": { - "type": "string", - "pattern": "^\\d{60}$" + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "qrCode": { - "type": "string" - } - }, - "required": [ - "id", - "numericCode", - "qrCode" - ] - }, - "UserSecurityCodeResponse": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "data": { - "$ref": "#/components/schemas/UserSecurityCode" + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "required": [ - "success", - "data" - ] + "x-mint": { + "content": "sessions:manage" + }, + "description": "**Required scope:** `sessions:manage`" }, - "CloudMessagePayload": { - "type": "object", - "description": "Cloud API native webhook payload. Meta supplies a phone-backed wa_id, not a LID.", - "properties": { - "messageId": { - "type": "string" - }, - "from": { - "type": "string", - "description": "Meta wa_id. This is not a LID-backed user ID." + "delete": { + "operationId": "deleteClientRules", + "tags": [ + "Client Tokens" + ], + "summary": "Delete client-token rules for a session", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "sessions:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Rules deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } }, - "timestamp": { - "type": "string" + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "type": { - "type": "string" + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "senderName": { - "type": "string" + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - "nativeFlowResponse": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 64 - }, - "paramsJson": { - "type": "string", - "minLength": 1, - "maxLength": 262144 - }, - "version": { - "type": "integer", - "minimum": 0 + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } - }, - "required": [ - "name", - "paramsJson" - ] + } }, - "interactive": { - "type": "object", - "additionalProperties": true + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "required": [ - "messageId", - "from", - "timestamp", - "type" - ], - "additionalProperties": true - }, - "MessageReceivedPayload": { - "oneOf": [ - { - "$ref": "#/components/schemas/MessagePayload" - }, - { - "$ref": "#/components/schemas/CloudMessagePayload" - } - ] + "x-mint": { + "content": "sessions:manage" + }, + "description": "**Required scope:** `sessions:manage`" } }, - "securitySchemes": { - "BearerAuth": { - "type": "http", - "scheme": "bearer", - "description": "Polymorfa API key (pmfa_ prefix) or client token (pmfa_ct_ prefix)." - } - } - }, - "paths": { - "/ping": { + "/api/operations/{operation}": { "get": { - "operationId": "ping", + "operationId": "getOperation", "tags": [ - "Health" + "Operations" + ], + "summary": "Get a durable operation", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scopes": [ + "sessions:read", + "campaigns:read", + "webhooks:manage" + ], + "x-required-scope-mode": "any", + "parameters": [ + { + "schema": { + "type": "string", + "format": "uuid" + }, + "required": true, + "name": "operation", + "in": "path" + } ], - "summary": "Liveness probe", - "security": [], "responses": { "200": { - "description": "Pong", + "description": "Operation state", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/Operation" + } + }, + "required": [ + "success", + "data" + ] + } + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PingResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } } - } + }, + "x-mint": { + "content": "sessions:read campaigns:read webhooks:manage" + }, + "description": "**Required scopes (any):** `sessions:read` · `campaigns:read` · `webhooks:manage`" } }, - "/health": { + "/api/sessions": { "get": { - "operationId": "getHealth", + "operationId": "listSessions", "tags": [ - "Health" + "Sessions" ], - "summary": "Full service health check", - "security": [], + "summary": "List all sessions for the tenant", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "sessions:read", "responses": { "200": { - "description": "All services healthy", + "description": "Session list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HealthResponse" + "$ref": "#/components/schemas/ListSessionsResponse" } } } }, - "503": { - "description": "One or more services unhealthy or draining", + "400": { + "description": "Bad request — request body or params failed validation", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HealthResponse" + "$ref": "#/components/schemas/ErrorResponse" } } } - } - } - } - }, - "/api/info/version": { - "get": { - "operationId": "getVersion", - "tags": [ - "Info" - ], - "summary": "Get API version info", - "security": [], - "responses": { - "200": { - "description": "Version info", + }, + "401": { + "description": "Missing or invalid authentication token", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VersionResponse" + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } - } - } - }, - "/api/info/status": { - "get": { - "operationId": "getStatus", + }, + "x-mint": { + "content": "sessions:read" + }, + "description": "**Required scope:** `sessions:read`" + }, + "post": { + "operationId": "createSession", "tags": [ - "Info" + "Sessions" ], - "summary": "Get API runtime status", - "security": [], + "summary": "Create a new session", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "sessions:manage", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSessionRequest" + } + } + } + }, "responses": { - "200": { - "description": "Status info", + "201": { + "description": "Session created; a testing session is connected and command-ready before this response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StatusResponse" + "$ref": "#/components/schemas/CreateSessionResponse" + } + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Session with this name already exists", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } - } + }, + "x-mint": { + "content": "sessions:manage" + }, + "description": "**Required scope:** `sessions:manage`" } }, - "/v1/bridge/route": { + "/api/sessions/{session}": { "get": { - "operationId": "getBridgeRoute", + "operationId": "getSession", "tags": [ - "Bridge" + "Sessions" ], - "summary": "Verify a project token, resolve live project authority, and return the regional WSS endpoint.", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "wsUrl": { - "type": "string", - "format": "uri" - }, - "region": { - "type": "string", - "enum": [ - "BR", - "US", - "IN", - "Auto" - ] - }, - "kind": { - "type": "string", - "enum": [ - "sandbox", - "production" - ] - }, - "signal": { - "type": "string", - "enum": [ - "customer", - "bartender" - ] - }, - "tokenKind": { - "type": "string", - "enum": [ - "project" - ] - }, - "expiresAt": { - "type": "integer", - "minimum": 0 - } - }, - "required": [ - "wsUrl", - "region", - "kind", - "signal", - "tokenKind", - "expiresAt" - ] + "summary": "Get session details", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "sessions:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Session details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSessionResponse" + } + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { - "description": "Unauthorized", + "description": "Missing or invalid authentication token", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "error", - "message" - ] + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "503": { - "description": "Service unavailable", + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "error", - "message" - ] + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" } } } } }, - "x-required-scope": "bridge:connect", "x-mint": { - "content": "bridge:connect" + "content": "sessions:read" }, - "description": "**Required scope:** `bridge:connect`" - } - }, - "/api/client-tokens": { - "post": { - "operationId": "mintClientToken", + "description": "**Required scope:** `sessions:read`" + }, + "put": { + "operationId": "updateSession", "tags": [ - "Client Tokens" + "Sessions" ], - "summary": "Mint a short-lived browser-safe client token", + "summary": "Update session configuration", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MintClientTokenRequest" + "$ref": "#/components/schemas/UpdateSessionRequest" } } } }, "responses": { "200": { - "description": "Token minted", + "description": "Session updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MintClientTokenResponse" + "$ref": "#/components/schemas/UpdateSessionResponse" } } } @@ -9162,27 +11669,35 @@ } } } + }, + "503": { + "description": "Session configuration was stored but the connected runner did not acknowledge it", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "sessions:manage", "x-mint": { "content": "sessions:manage" }, "description": "**Required scope:** `sessions:manage`" - } - }, - "/api/sessions/{session}/client-rules": { - "get": { - "operationId": "getClientRules", + }, + "delete": { + "operationId": "deleteSession", "tags": [ - "Client Tokens" + "Sessions" ], - "summary": "Get client-token rules for a session", + "summary": "Delete a session", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -9195,11 +11710,21 @@ ], "responses": { "200": { - "description": "Client rules", + "description": "Testing session deleted immediately after its simulated runtime closes", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetClientRulesResponse" + "$ref": "#/components/schemas/OperationAccepted" + } + } + } + }, + "202": { + "description": "Session deletion scheduled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationAccepted" } } } @@ -9255,23 +11780,25 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { "content": "sessions:manage" }, "description": "**Required scope:** `sessions:manage`" - }, - "put": { - "operationId": "setClientRules", + } + }, + "/api/sessions/{session}/start": { + "post": { + "operationId": "startSession", "tags": [ - "Client Tokens" + "Sessions" ], - "summary": "Set client-token rules for a session", + "summary": "Start a session (connect to WhatsApp)", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -9282,22 +11809,13 @@ "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetClientRulesRequest" - } - } - } - }, "responses": { - "200": { - "description": "Rules saved", + "202": { + "description": "Testing-session state changed immediately, or production-session start scheduled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/OperationAccepted" } } } @@ -9353,23 +11871,25 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { "content": "sessions:manage" }, "description": "**Required scope:** `sessions:manage`" - }, - "delete": { - "operationId": "deleteClientRules", + } + }, + "/api/sessions/{session}/stop": { + "post": { + "operationId": "stopSession", "tags": [ - "Client Tokens" + "Sessions" ], - "summary": "Delete client-token rules for a session", + "summary": "Stop a session", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -9381,12 +11901,12 @@ } ], "responses": { - "200": { - "description": "Rules deleted", + "202": { + "description": "Testing session stopped after backend teardown, or production-session stop scheduled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/OperationAccepted" } } } @@ -9442,58 +11962,42 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { "content": "sessions:manage" }, "description": "**Required scope:** `sessions:manage`" } }, - "/api/operations/{operation}": { - "get": { - "operationId": "getOperation", + "/api/sessions/{session}/restart": { + "post": { + "operationId": "restartSession", "tags": [ - "Operations" + "Sessions" ], - "summary": "Get a durable operation", + "summary": "Restart a session", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, "required": true, - "name": "operation", + "name": "session", "in": "path" } ], "responses": { - "200": { - "description": "Operation state", + "202": { + "description": "Testing session reconnected and ready, or production-session restart scheduled", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/Operation" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/OperationAccepted" } } } @@ -9549,37 +12053,42 @@ } } }, - "x-required-scopes": [ - "sessions:read", - "campaigns:read", - "webhooks:manage" - ], - "x-required-scope-mode": "any", "x-mint": { - "content": "sessions:read campaigns:read webhooks:manage" + "content": "sessions:manage" }, - "description": "**Required scopes (any):** `sessions:read` · `campaigns:read` · `webhooks:manage`" + "description": "**Required scope:** `sessions:manage`" } }, - "/api/sessions": { - "get": { - "operationId": "listSessions", + "/api/sessions/{session}/logout": { + "post": { + "operationId": "logoutSession", "tags": [ "Sessions" ], - "summary": "List all sessions for the tenant", + "summary": "Log the session out of WhatsApp", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + } + ], "responses": { - "200": { - "description": "Session list", + "202": { + "description": "Session logout scheduled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListSessionsResponse" + "$ref": "#/components/schemas/OperationAccepted" } } } @@ -9635,39 +12144,42 @@ } } }, - "x-required-scope": "sessions:read", "x-mint": { - "content": "sessions:read" + "content": "sessions:manage" }, - "description": "**Required scope:** `sessions:read`" - }, - "post": { - "operationId": "createSession", + "description": "**Required scope:** `sessions:manage`" + } + }, + "/api/sessions/{session}/me": { + "get": { + "operationId": "getSessionAccount", "tags": [ "Sessions" ], - "summary": "Create a new session", + "summary": "Get the connected WhatsApp account info", "security": [ { "BearerAuth": [] } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateSessionRequest" - } - } + "x-required-scope": "sessions:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" } - }, + ], "responses": { - "201": { - "description": "Session created", + "200": { + "description": "Account info", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSessionResponse" + "$ref": "#/components/schemas/GetSessionAccountResponse" } } } @@ -9712,16 +12224,6 @@ } } }, - "409": { - "description": "Session with this name already exists", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -9733,25 +12235,25 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "sessions:read" }, - "description": "**Required scope:** `sessions:manage`" + "description": "**Required scope:** `sessions:read`" } }, - "/api/sessions/{session}": { - "get": { - "operationId": "getSession", + "/api/sessions/{session}/handoff-keys": { + "post": { + "operationId": "sessionHandoffKeys", "tags": [ "Sessions" ], - "summary": "Get session details", + "summary": "Initiate M↔P toggle: fetch the destination Curve25519 pubkey", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -9764,11 +12266,11 @@ ], "responses": { "200": { - "description": "Session details", + "description": "Destination pubkey + correlation id for the handoff", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetSessionResponse" + "$ref": "#/components/schemas/HandoffKeysSuccess" } } } @@ -9824,23 +12326,25 @@ } } }, - "x-required-scope": "sessions:read", "x-mint": { - "content": "sessions:read" + "content": "sessions:manage" }, - "description": "**Required scope:** `sessions:read`" - }, - "put": { - "operationId": "updateSession", + "description": "**Required scope:** `sessions:manage`" + } + }, + "/api/sessions/{session}/handoff-import": { + "post": { + "operationId": "sessionHandoffImport", "tags": [ "Sessions" ], - "summary": "Update session configuration", + "summary": "Ship a sealed SignalStoreSnapshot from customer Baileys to the runner", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -9855,18 +12359,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSessionRequest" + "$ref": "#/components/schemas/HandoffImportRequest" } } } }, "responses": { "200": { - "description": "Session updated", + "description": "Import accepted; bartenderMode will flip after the runner acks", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSessionResponse" + "$ref": "#/components/schemas/HandoffImportSuccess" } } } @@ -9920,35 +12424,27 @@ } } } - }, - "503": { - "description": "Session configuration was stored but the connected runner did not acknowledge it", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, - "x-required-scope": "sessions:manage", "x-mint": { "content": "sessions:manage" }, "description": "**Required scope:** `sessions:manage`" - }, - "delete": { - "operationId": "deleteSession", + } + }, + "/api/sessions/{session}/upload-appstate-keys": { + "post": { + "operationId": "sessionUploadAppStateKeys", "tags": [ "Sessions" ], - "summary": "Delete a session", + "summary": "Upload selected AppStateSyncKeys (Track P+ tier)", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -9959,13 +12455,22 @@ "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadAppStateKeysRequest" + } + } + } + }, "responses": { - "202": { - "description": "Session deletion scheduled", + "200": { + "description": "Keys persisted; mode may have been flipped per upgradeTo", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OperationAccepted" + "$ref": "#/components/schemas/UploadAppStateKeysSuccess" } } } @@ -10021,25 +12526,25 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { "content": "sessions:manage" }, "description": "**Required scope:** `sessions:manage`" } }, - "/api/sessions/{session}/start": { + "/api/sessions/{session}/handoff-ack": { "post": { - "operationId": "startSession", + "operationId": "sessionHandoffAck", "tags": [ "Sessions" ], - "summary": "Start a session (connect to WhatsApp)", + "summary": "Acknowledge receipt of an M→P export; API flips bartenderMode after hash verify", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -10050,13 +12555,22 @@ "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HandoffAckRequest" + } + } + } + }, "responses": { - "202": { - "description": "Session start scheduled", + "200": { + "description": "Ack accepted; bartenderMode flipped from magic → passthrough", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OperationAccepted" + "$ref": "#/components/schemas/HandoffAckSuccess" } } } @@ -10112,25 +12626,25 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { "content": "sessions:manage" }, "description": "**Required scope:** `sessions:manage`" } }, - "/api/sessions/{session}/stop": { - "post": { - "operationId": "stopSession", + "/api/{session}/pair/qr": { + "get": { + "operationId": "getQRCode", "tags": [ - "Sessions" + "Pairing" ], - "summary": "Stop a session", + "summary": "Get QR code for device pairing", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -10139,15 +12653,34 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "json", + "image" + ], + "default": "json" + }, + "required": false, + "name": "format", + "in": "query" } ], "responses": { - "202": { - "description": "Session stop scheduled", + "200": { + "description": "QR code data", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OperationAccepted" + "$ref": "#/components/schemas/GetQRCodeResponse" + } + }, + "image/png": { + "schema": { + "type": "string", + "format": "binary" } } } @@ -10203,25 +12736,28 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "Linked Device API sessions:manage" }, - "description": "**Required scope:** `sessions:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `sessions:manage`" } }, - "/api/sessions/{session}/restart": { + "/api/{session}/pair/code": { "post": { - "operationId": "restartSession", + "operationId": "requestPairCode", "tags": [ - "Sessions" + "Pairing" ], - "summary": "Restart a session", + "summary": "Request a pairing code for a phone number", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { @@ -10232,13 +12768,22 @@ "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PairCodeRequest" + } + } + } + }, "responses": { - "202": { - "description": "Session restart scheduled", + "200": { + "description": "Pairing code issued", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OperationAccepted" + "$ref": "#/components/schemas/RequestPairCodeResponse" } } } @@ -10294,25 +12839,29 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "Linked Device API sessions:manage" }, - "description": "**Required scope:** `sessions:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `sessions:manage`" } }, - "/api/sessions/{session}/logout": { + "/api/{session}/messages/send": { "post": { - "operationId": "logoutSession", + "operationId": "sendMessage", "tags": [ - "Sessions" + "Messages" ], - "summary": "Log the session out of WhatsApp", + "summary": "Send a message", + "description": "**Protocols:** Linked Device API · Meta Cloud API · Polymorfa Hybrid Link\n\n**Required scope:** `messages:write`\n\nTesting sessions accept a recipient testing number in E.164 format or as a phone-number JID, and can send only to active testing numbers in the same project. Malformed, unknown, stopped, deleted, and cross-project testing recipients all return 404 without creating recipient state.", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "messages:write", "parameters": [ { "schema": { @@ -10322,14 +12871,23 @@ "name": "session", "in": "path" } - ], + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SendMessageRequest" + } + } + } + }, "responses": { - "202": { - "description": "Session logout scheduled", + "200": { + "description": "Message sent successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OperationAccepted" + "$ref": "#/components/schemas/SendMessageResponse" } } } @@ -10385,25 +12943,29 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "Linked Device API Meta Cloud API Polymorfa Hybrid Link messages:write" }, - "description": "**Required scope:** `sessions:manage`" + "x-protocols": [ + "Linked Device API", + "Meta Cloud API", + "Polymorfa Hybrid Link" + ] } }, - "/api/sessions/{session}/me": { - "get": { - "operationId": "getSessionAccount", + "/api/{session}/messages/seen": { + "post": { + "operationId": "sendSeen", "tags": [ - "Sessions" + "Messages" ], - "summary": "Get the connected WhatsApp account info", + "summary": "Mark messages as seen", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "messages:write", "parameters": [ { "schema": { @@ -10414,13 +12976,22 @@ "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SeenRequest" + } + } + } + }, "responses": { "200": { - "description": "Account info", + "description": "Seen marker queued", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetSessionAccountResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -10476,25 +13047,30 @@ } } }, - "x-required-scope": "sessions:read", "x-mint": { - "content": "sessions:read" + "content": "Linked Device API Meta Cloud API Polymorfa Hybrid Link messages:write" }, - "description": "**Required scope:** `sessions:read`" + "x-protocols": [ + "Linked Device API", + "Meta Cloud API", + "Polymorfa Hybrid Link" + ], + "description": "**Protocols:** Linked Device API · Meta Cloud API · Polymorfa Hybrid Link\n\n**Required scope:** `messages:write`" } }, - "/api/sessions/{session}/handoff-keys": { + "/api/{session}/messages/typing": { "post": { - "operationId": "sessionHandoffKeys", + "operationId": "setTyping", "tags": [ - "Sessions" + "Messages" ], - "summary": "Initiate M↔P toggle: fetch the destination Curve25519 pubkey", + "summary": "Set typing indicator for a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "messages:write", "parameters": [ { "schema": { @@ -10505,13 +13081,22 @@ "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypingRequest" + } + } + } + }, "responses": { "200": { - "description": "Destination pubkey + correlation id for the handoff", + "description": "Typing state queued", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HandoffKeysSuccess" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -10567,25 +13152,28 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "Linked Device API messages:write" }, - "description": "**Required scope:** `sessions:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `messages:write`" } }, - "/api/sessions/{session}/handoff-import": { + "/api/{session}/messages/react": { "post": { - "operationId": "sessionHandoffImport", + "operationId": "sendReaction", "tags": [ - "Sessions" + "Messages" ], - "summary": "Ship a sealed SignalStoreSnapshot from customer Baileys to the runner", + "summary": "React to a message", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "messages:write", "parameters": [ { "schema": { @@ -10600,18 +13188,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HandoffImportRequest" + "$ref": "#/components/schemas/ReactRequest" } } } }, "responses": { "200": { - "description": "Import accepted; bartenderMode will flip after the runner acks", + "description": "Reaction sent", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HandoffImportSuccess" + "$ref": "#/components/schemas/SendReactionResponse" } } } @@ -10667,25 +13255,30 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "Linked Device API Meta Cloud API Polymorfa Hybrid Link messages:write" }, - "description": "**Required scope:** `sessions:manage`" + "x-protocols": [ + "Linked Device API", + "Meta Cloud API", + "Polymorfa Hybrid Link" + ], + "description": "**Protocols:** Linked Device API · Meta Cloud API · Polymorfa Hybrid Link\n\n**Required scope:** `messages:write`" } }, - "/api/sessions/{session}/upload-appstate-keys": { + "/api/{session}/messages/star": { "post": { - "operationId": "sessionUploadAppStateKeys", + "operationId": "starMessage", "tags": [ - "Sessions" + "Messages" ], - "summary": "Upload selected AppStateSyncKeys (Track P+ tier)", + "summary": "Star or unstar a message", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "messages:write", "parameters": [ { "schema": { @@ -10700,18 +13293,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UploadAppStateKeysRequest" + "$ref": "#/components/schemas/StarRequest" } } } }, "responses": { "200": { - "description": "Keys persisted; mode may have been flipped per upgradeTo", + "description": "Star status updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UploadAppStateKeysSuccess" + "$ref": "#/components/schemas/StarMessageResponse" } } } @@ -10767,25 +13360,28 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "Linked Device API messages:write" }, - "description": "**Required scope:** `sessions:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `messages:write`" } }, - "/api/sessions/{session}/handoff-ack": { - "post": { - "operationId": "sessionHandoffAck", + "/api/{session}/chats/{chatId}/messages/{messageId}": { + "put": { + "operationId": "editMessage", "tags": [ - "Sessions" + "Chats" ], - "summary": "Acknowledge receipt of an M→P export; API flips bartenderMode after hash verify", + "summary": "Edit a previously sent message", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "chats:manage", "parameters": [ { "schema": { @@ -10794,24 +13390,40 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "chatId", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "messageId", + "in": "path" } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HandoffAckRequest" + "$ref": "#/components/schemas/EditMessageRequest" } } } }, "responses": { "200": { - "description": "Ack accepted; bartenderMode flipped from magic → passthrough", + "description": "Message edited", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HandoffAckSuccess" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -10867,25 +13479,26 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "sessions:manage" + "content": "Linked Device API chats:manage" }, - "description": "**Required scope:** `sessions:manage`" - } - }, - "/api/{session}/pair/qr": { - "get": { - "operationId": "getQRCode", + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" + }, + "delete": { + "operationId": "deleteMessage", "tags": [ - "Pairing" + "Chats" ], - "summary": "Get QR code for device pairing", + "summary": "Delete a previously sent message", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "chats:manage", "parameters": [ { "schema": { @@ -10897,31 +13510,28 @@ }, { "schema": { - "type": "string", - "enum": [ - "json", - "image" - ], - "default": "json" + "type": "string" }, - "required": false, - "name": "format", - "in": "query" + "required": true, + "name": "chatId", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "messageId", + "in": "path" } ], "responses": { "200": { - "description": "QR code data", + "description": "Message deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetQRCodeResponse" - } - }, - "image/png": { - "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -10977,28 +13587,28 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "Linked Device API sessions:manage" + "content": "Linked Device API chats:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `sessions:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" } }, - "/api/{session}/pair/code": { + "/api/{session}/chats/{chatId}/archive": { "post": { - "operationId": "requestPairCode", + "operationId": "archiveChat", "tags": [ - "Pairing" + "Chats" ], - "summary": "Request a pairing code for a phone number", + "summary": "Archive a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "chats:manage", "parameters": [ { "schema": { @@ -11007,24 +13617,23 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "chatId", + "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PairCodeRequest" - } - } - } - }, "responses": { "200": { - "description": "Pairing code issued", + "description": "Chat archived", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RequestPairCodeResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -11080,28 +13689,28 @@ } } }, - "x-required-scope": "sessions:manage", "x-mint": { - "content": "Linked Device API sessions:manage" + "content": "Linked Device API chats:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `sessions:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" } }, - "/api/{session}/messages/send": { + "/api/{session}/chats/{chatId}/unarchive": { "post": { - "operationId": "sendMessage", + "operationId": "unarchiveChat", "tags": [ - "Messages" + "Chats" ], - "summary": "Send a message", + "summary": "Unarchive a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "chats:manage", "parameters": [ { "schema": { @@ -11110,24 +13719,23 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "chatId", + "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SendMessageRequest" - } - } - } - }, "responses": { "200": { - "description": "Message sent successfully", + "description": "Chat unarchived", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SendMessageResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -11183,30 +13791,28 @@ } } }, - "x-required-scope": "messages:write", "x-mint": { - "content": "Linked Device API Meta Cloud API Polymorfa Hybrid Link messages:write" + "content": "Linked Device API chats:manage" }, "x-protocols": [ - "Linked Device API", - "Meta Cloud API", - "Polymorfa Hybrid Link" + "Linked Device API" ], - "description": "**Protocols:** Linked Device API · Meta Cloud API · Polymorfa Hybrid Link\n\n**Required scope:** `messages:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" } }, - "/api/{session}/messages/seen": { - "post": { - "operationId": "sendSeen", + "/api/{session}/chats/{chatId}/disappearing": { + "put": { + "operationId": "setChatDisappearingTimer", "tags": [ - "Messages" + "Chats" ], - "summary": "Mark messages as seen", + "summary": "Set the disappearing-message timer for a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "chats:manage", "parameters": [ { "schema": { @@ -11215,20 +13821,28 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "chatId", + "in": "path" } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SeenRequest" + "$ref": "#/components/schemas/DisappearingTimerRequest" } } } }, "responses": { "200": { - "description": "Seen marker queued", + "description": "Disappearing-message timer updated", "content": { "application/json": { "schema": { @@ -11288,30 +13902,28 @@ } } }, - "x-required-scope": "messages:write", "x-mint": { - "content": "Linked Device API Meta Cloud API Polymorfa Hybrid Link messages:write" + "content": "Linked Device API chats:manage" }, "x-protocols": [ - "Linked Device API", - "Meta Cloud API", - "Polymorfa Hybrid Link" + "Linked Device API" ], - "description": "**Protocols:** Linked Device API · Meta Cloud API · Polymorfa Hybrid Link\n\n**Required scope:** `messages:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" } }, - "/api/{session}/messages/typing": { + "/api/{session}/calls/{callId}/reject": { "post": { - "operationId": "setTyping", + "operationId": "rejectCall", "tags": [ - "Messages" + "Calls" ], - "summary": "Set typing indicator for a chat", + "summary": "Reject an incoming WhatsApp call", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "chats:manage", "parameters": [ { "schema": { @@ -11320,20 +13932,30 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "required": true, + "name": "callId", + "in": "path" } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TypingRequest" + "$ref": "#/components/schemas/RejectCallRequest" } } } }, "responses": { "200": { - "description": "Typing state queued", + "description": "Call rejected", "content": { "application/json": { "schema": { @@ -11393,35 +14015,37 @@ } } }, - "x-required-scope": "messages:write", "x-mint": { - "content": "Linked Device API messages:write" + "content": "Linked Device API chats:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `messages:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" } }, - "/api/{session}/messages/react": { + "/api/voip/calls/{id}/offer": { "post": { - "operationId": "sendReaction", + "operationId": "voipOffer", "tags": [ - "Messages" + "VoIP" ], - "summary": "React to a message", + "summary": "Submit an SDP offer and receive the pod's answer + ICE servers", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "example": "call-123" }, "required": true, - "name": "session", + "name": "id", "in": "path" } ], @@ -11429,18 +14053,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReactRequest" + "$ref": "#/components/schemas/VoipOfferRequest" } } } }, "responses": { "200": { - "description": "Reaction sent", + "description": "SDP answer with STUN/TURN ICE servers", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SendReactionResponse" + "$ref": "#/components/schemas/VoipOfferResponse" } } } @@ -11485,6 +14109,36 @@ } } }, + "409": { + "description": "Call not ready — no media session on the owning pod yet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "410": { + "description": "Call ended — the pod that owned its media is gone (pod_lost)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Call setup rate exceeded for this client token, or its concurrency limit is reached", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -11494,39 +14148,46 @@ } } } + }, + "503": { + "description": "Call refused — the owning pod is at capacity or draining", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "messages:write", "x-mint": { - "content": "Linked Device API Meta Cloud API Polymorfa Hybrid Link messages:write" + "content": "sessions:manage" }, - "x-protocols": [ - "Linked Device API", - "Meta Cloud API", - "Polymorfa Hybrid Link" - ], - "description": "**Protocols:** Linked Device API · Meta Cloud API · Polymorfa Hybrid Link\n\n**Required scope:** `messages:write`" + "description": "**Required scope:** `sessions:manage`" } }, - "/api/{session}/messages/star": { + "/api/voip/calls/{id}/renegotiate": { "post": { - "operationId": "starMessage", + "operationId": "voipRenegotiate", "tags": [ - "Messages" + "VoIP" ], - "summary": "Star or unstar a message", + "summary": "Re-offer on an established call (add video, restart ICE) and receive the pod's answer", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "example": "call-123" }, "required": true, - "name": "session", + "name": "id", "in": "path" } ], @@ -11534,18 +14195,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StarRequest" + "$ref": "#/components/schemas/VoipRenegotiateRequest" } } } }, "responses": { "200": { - "description": "Star status updated", + "description": "SDP answer for the re-offer, on the same peer connection", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StarMessageResponse" + "$ref": "#/components/schemas/VoipRenegotiateResponse" } } } @@ -11590,6 +14251,26 @@ } } }, + "409": { + "description": "Call not ready — no media session on the owning pod yet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "410": { + "description": "Call ended — the pod that owned its media is gone (pod_lost)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -11599,72 +14280,54 @@ } } } + }, + "503": { + "description": "Call refused — the owning pod is at capacity or draining", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "messages:write", "x-mint": { - "content": "Linked Device API messages:write" + "content": "sessions:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `messages:write`" + "description": "**Required scope:** `sessions:manage`" } }, - "/api/{session}/chats/{chatId}/messages/{messageId}": { - "put": { - "operationId": "editMessage", + "/api/voip/ws-ticket": { + "post": { + "operationId": "voipSocketTicket", "tags": [ - "Chats" + "VoIP" ], - "summary": "Edit a previously sent message", + "summary": "Mint a single-use ticket for the calls WebSocket", + "description": "**Required scope:** `sessions:manage`\n\nBrowsers cannot send an Authorization header on a WebSocket upgrade, so the calls socket (`GET /voip/ws?ticket=…`) is opened with a ticket obtained here. Tickets are single-use and expire after 60 seconds. Client tokens are bound to their session; server keys must name one.", "security": [ { "BearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "chatId", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "messageId", - "in": "path" - } - ], + "x-required-scope": "sessions:manage", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EditMessageRequest" + "$ref": "#/components/schemas/VoipSocketTicketRequest" } } } }, "responses": { "200": { - "description": "Message edited", + "description": "Ticket minted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/VoipSocketTicketResponse" } } } @@ -11720,59 +14383,53 @@ } } }, - "x-required-scope": "chats:manage", "x-mint": { - "content": "Linked Device API chats:manage" - }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" - }, - "delete": { - "operationId": "deleteMessage", + "content": "sessions:manage" + } + } + }, + "/api/voip/calls/{id}/agent-token": { + "post": { + "operationId": "voipAgentToken", "tags": [ - "Chats" + "VoIP" ], - "summary": "Delete a previously sent message", + "summary": "Mint a per-call ticket for a voice agent's media WebSocket", + "description": "**Required scope:** `sessions:manage`\n\nServer keys only. The ticket is bound to this call, expires after `ttlSeconds` (default 300, max 3600) and is presented by the agent to the voip pod as `Authorization: Bearer `. It grants nothing else.", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "chatId", - "in": "path" - }, - { - "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "example": "call-123" }, "required": true, - "name": "messageId", + "name": "id", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoipAgentTokenRequest" + } + } + } + }, "responses": { "200": { - "description": "Message deleted", + "description": "Agent ticket minted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/VoipAgentTokenResponse" } } } @@ -11828,49 +14485,48 @@ } } }, - "x-required-scope": "chats:manage", "x-mint": { - "content": "Linked Device API chats:manage" - }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" + "content": "sessions:manage" + } } }, - "/api/{session}/chats/{chatId}/archive": { + "/api/voip/calls/{id}/candidate": { "post": { - "operationId": "archiveChat", + "operationId": "voipCandidate", "tags": [ - "Chats" + "VoIP" ], - "summary": "Archive a chat", + "summary": "Trickle a local ICE candidate to the owning pod", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "example": "call-123" }, "required": true, - "name": "chatId", + "name": "id", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoipCandidateRequest" + } + } + } + }, "responses": { - "200": { - "description": "Chat archived", + "202": { + "description": "Candidate accepted", "content": { "application/json": { "schema": { @@ -11919,6 +14575,26 @@ } } }, + "409": { + "description": "Call not ready — no media session on the owning pod yet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "410": { + "description": "Call ended — the pod that owned its media is gone (pod_lost)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -11928,55 +14604,56 @@ } } } + }, + "503": { + "description": "Call refused — the owning pod is at capacity or draining", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "chats:manage", "x-mint": { - "content": "Linked Device API chats:manage" + "content": "sessions:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" + "description": "**Required scope:** `sessions:manage`" } }, - "/api/{session}/chats/{chatId}/unarchive": { - "post": { - "operationId": "unarchiveChat", + "/api/voip/calls/{id}/candidates": { + "get": { + "operationId": "voipCandidates", "tags": [ - "Chats" + "VoIP" ], - "summary": "Unarchive a chat", + "summary": "Poll remote ICE candidates queued for the browser", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "example": "call-123" }, "required": true, - "name": "chatId", + "name": "id", "in": "path" } ], "responses": { "200": { - "description": "Chat unarchived", + "description": "Queued remote candidates (drained on read)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/VoipCandidatesEnvelope" } } } @@ -12032,58 +14709,40 @@ } } }, - "x-required-scope": "chats:manage", "x-mint": { - "content": "Linked Device API chats:manage" + "content": "sessions:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" + "description": "**Required scope:** `sessions:manage`" } }, - "/api/{session}/chats/{chatId}/disappearing": { - "put": { - "operationId": "setChatDisappearingTimer", + "/api/voip/calls/{id}": { + "delete": { + "operationId": "voipTeardown", "tags": [ - "Chats" + "VoIP" ], - "summary": "Set the disappearing-message timer for a chat", + "summary": "Tear down a call's browser media session", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "sessions:manage", "parameters": [ { "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "example": "call-123" }, "required": true, - "name": "chatId", + "name": "id", "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DisappearingTimerRequest" - } - } - } - }, "responses": { "200": { - "description": "Disappearing-message timer updated", + "description": "Call torn down", "content": { "application/json": { "schema": { @@ -12132,6 +14791,26 @@ } } }, + "409": { + "description": "Call not ready — no media session on the owning pod yet", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "410": { + "description": "Call ended — the pod that owned its media is gone (pod_lost)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -12141,66 +14820,53 @@ } } } + }, + "503": { + "description": "Call refused — the owning pod is at capacity or draining", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "chats:manage", "x-mint": { - "content": "Linked Device API chats:manage" + "content": "sessions:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" + "description": "**Required scope:** `sessions:manage`" } }, - "/api/{session}/calls/{callId}/reject": { + "/api/voip/token": { "post": { - "operationId": "rejectCall", + "operationId": "voipToken", "tags": [ - "Calls" + "VoIP" ], - "summary": "Reject an incoming WhatsApp call", + "summary": "Mint a short-lived browser client token for the VoIP SDK", "security": [ { "BearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "required": true, - "name": "callId", - "in": "path" - } - ], + "x-required-scope": "sessions:manage", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RejectCallRequest" + "$ref": "#/components/schemas/VoipTokenRequest" } } } }, "responses": { "200": { - "description": "Call rejected", + "description": "Client token minted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/VoipTokenResponse" } } } @@ -12256,14 +14922,10 @@ } } }, - "x-required-scope": "chats:manage", "x-mint": { - "content": "Linked Device API chats:manage" + "content": "sessions:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `chats:manage`" + "description": "**Required scope:** `sessions:manage`" } }, "/api/{session}/groups": { @@ -12278,6 +14940,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:read", "parameters": [ { "schema": { @@ -12350,7 +15013,6 @@ } } }, - "x-required-scope": "groups:read", "x-mint": { "content": "Linked Device API groups:read" }, @@ -12370,6 +15032,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -12451,7 +15114,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -12473,6 +15135,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:read", "parameters": [ { "schema": { @@ -12554,7 +15217,6 @@ } } }, - "x-required-scope": "groups:read", "x-mint": { "content": "Linked Device API groups:read" }, @@ -12576,6 +15238,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -12657,7 +15320,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -12679,6 +15341,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:read", "parameters": [ { "schema": { @@ -12759,7 +15422,6 @@ } } }, - "x-required-scope": "groups:read", "x-mint": { "content": "Linked Device API groups:read" }, @@ -12779,6 +15441,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -12859,7 +15522,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -12881,6 +15543,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -12961,7 +15624,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -12983,6 +15645,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -13072,7 +15735,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -13094,6 +15756,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -13183,7 +15846,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -13205,6 +15867,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:read", "parameters": [ { "schema": { @@ -13285,7 +15948,6 @@ } } }, - "x-required-scope": "groups:read", "x-mint": { "content": "Linked Device API groups:read" }, @@ -13307,6 +15969,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -13387,7 +16050,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -13409,6 +16071,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:read", "parameters": [ { "schema": { @@ -13489,7 +16152,6 @@ } } }, - "x-required-scope": "groups:read", "x-mint": { "content": "Linked Device API groups:read" }, @@ -13511,6 +16173,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -13600,7 +16263,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -13622,6 +16284,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -13711,7 +16374,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -13733,6 +16395,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -13822,7 +16485,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -13844,6 +16506,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -13933,7 +16596,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -13955,6 +16617,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -14044,7 +16707,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -14066,6 +16728,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -14155,7 +16818,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -14177,6 +16839,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -14266,7 +16929,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -14288,6 +16950,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -14377,7 +17040,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -14399,6 +17061,7 @@ "BearerAuth": [] } ], + "x-required-scope": "groups:manage", "parameters": [ { "schema": { @@ -14488,7 +17151,6 @@ } } }, - "x-required-scope": "groups:manage", "x-mint": { "content": "Linked Device API groups:manage" }, @@ -14510,6 +17172,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -14582,7 +17245,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -14604,6 +17266,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -14696,7 +17359,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -14718,6 +17380,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -14790,7 +17453,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -14806,12 +17468,13 @@ "tags": [ "Contacts" ], - "summary": "Get a contact by LID", + "summary": "Get a contact by stable user ID", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -14892,7 +17555,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -14914,6 +17576,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -14994,7 +17657,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -15016,6 +17678,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -15096,7 +17759,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -15118,6 +17780,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -15198,7 +17861,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -15220,6 +17882,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -15300,7 +17963,6 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { "content": "Linked Device API contacts:read" }, @@ -15322,6 +17984,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:manage", "parameters": [ { "schema": { @@ -15402,7 +18065,6 @@ } } }, - "x-required-scope": "contacts:manage", "x-mint": { "content": "Linked Device API contacts:manage" }, @@ -15424,6 +18086,7 @@ "BearerAuth": [] } ], + "x-required-scope": "contacts:manage", "parameters": [ { "schema": { @@ -15504,7 +18167,6 @@ } } }, - "x-required-scope": "contacts:manage", "x-mint": { "content": "Linked Device API contacts:manage" }, @@ -15514,18 +18176,19 @@ "description": "**Protocols:** Linked Device API\n\n**Required scope:** `contacts:manage`" } }, - "/api/{session}/business/profile": { + "/api/{session}/business/linked-accounts": { "get": { - "operationId": "getOwnBusinessProfile", + "operationId": "getBusinessLinkedAccounts", "tags": [ "Business App" ], - "summary": "Read the connected WhatsApp Business App profile", + "summary": "Read live accounts linked to the connected WhatsApp Business App", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "business:read", "parameters": [ { "schema": { @@ -15538,11 +18201,11 @@ ], "responses": { "200": { - "description": "Live business profile", + "description": "Live linked-account state", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetOwnBusinessProfileResponse" + "$ref": "#/components/schemas/GetBusinessLinkedAccountsResponse" } } } @@ -15598,26 +18261,28 @@ } } }, - "x-required-scope": "profile:read", "x-mint": { - "content": "Linked Device API profile:read" + "content": "Linked Device API business:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" - }, - "patch": { - "operationId": "updateBusinessProfile", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" + } + }, + "/api/{session}/business/eligibility": { + "get": { + "operationId": "getBusinessEligibility", "tags": [ "Business App" ], - "summary": "Update the connected WhatsApp Business App profile", + "summary": "Read live feature eligibility for the connected WhatsApp Business App", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "business:read", "parameters": [ { "schema": { @@ -15628,23 +18293,13 @@ "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessProfileUpdateRequest" - } - } - } - }, "responses": { "200": { - "description": "Profile updated", + "description": "Live business feature eligibility", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateBusinessProfileResponse" + "$ref": "#/components/schemas/GetBusinessEligibilityResponse" } } } @@ -15700,28 +18355,28 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API business:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" } }, - "/api/{session}/business/profile/cover-photo": { - "put": { - "operationId": "setBusinessCoverPhoto", + "/api/{session}/business/profile": { + "get": { + "operationId": "getOwnBusinessProfile", "tags": [ "Business App" ], - "summary": "Set the connected WhatsApp Business App cover photo", + "summary": "Read the connected WhatsApp Business App profile", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:read", "parameters": [ { "schema": { @@ -15732,23 +18387,13 @@ "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessCoverPhotoRequest" - } - } - } - }, "responses": { "200": { - "description": "Cover photo set", + "description": "Live business profile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetBusinessCoverPhotoResponse" + "$ref": "#/components/schemas/GetOwnBusinessProfileResponse" } } } @@ -15804,28 +18449,26 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API profile:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - } - }, - "/api/{session}/business/profile/cover-photo/{coverPhotoId}": { - "delete": { - "operationId": "deleteBusinessCoverPhoto", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" + }, + "patch": { + "operationId": "updateBusinessProfile", "tags": [ "Business App" ], - "summary": "Delete the connected WhatsApp Business App cover photo", + "summary": "Update the connected WhatsApp Business App profile", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -15834,25 +18477,25 @@ "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "required": true, - "name": "coverPhotoId", - "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessProfileUpdateRequest" + } + } + } + }, "responses": { "200": { - "description": "Cover photo removed", + "description": "Profile updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteBusinessCoverPhotoResponse" + "$ref": "#/components/schemas/UpdateBusinessProfileResponse" } } } @@ -15908,7 +18551,6 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { "content": "Linked Device API profile:write" }, @@ -15918,18 +18560,19 @@ "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/business/catalog": { - "get": { - "operationId": "getBusinessCatalog", + "/api/{session}/business/profile/cover-photo": { + "put": { + "operationId": "setBusinessCoverPhoto", "tags": [ "Business App" ], - "summary": "Read a WhatsApp Business App catalog", + "summary": "Set the connected WhatsApp Business App cover photo", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -15938,64 +18581,129 @@ "required": true, "name": "session", "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessCoverPhotoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Cover photo set", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetBusinessCoverPhotoResponse" + } + } + } }, - { - "schema": { - "type": "string", - "maxLength": 256, - "pattern": "^\\d{1,32}@(s\\.whatsapp\\.net|lid)$" - }, - "required": true, - "name": "jid", - "in": "query" + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, - { - "schema": { - "type": "string", - "maxLength": 2048 - }, - "required": false, - "name": "after", - "in": "query" + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-mint": { + "content": "Linked Device API profile:write" + }, + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/business/profile/cover-photo/{coverPhotoId}": { + "delete": { + "operationId": "deleteBusinessCoverPhoto", + "tags": [ + "Business App" + ], + "summary": "Delete the connected WhatsApp Business App cover photo", + "security": [ { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "required": false, - "name": "limit", - "in": "query" - }, + "BearerAuth": [] + } + ], + "x-required-scope": "profile:write", + "parameters": [ { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 1024 + "type": "string" }, - "required": false, - "name": "width", - "in": "query" + "required": true, + "name": "session", + "in": "path" }, { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 1024 + "type": "string", + "minLength": 1, + "maxLength": 256 }, - "required": false, - "name": "height", - "in": "query" + "required": true, + "name": "coverPhotoId", + "in": "path" } ], "responses": { "200": { - "description": "Live catalog page", + "description": "Cover photo removed", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBusinessCatalogResponse" + "$ref": "#/components/schemas/DeleteBusinessCoverPhotoResponse" } } } @@ -16049,38 +18757,30 @@ } } } - }, - "502": { - "description": "Upstream WhatsApp request failed or its response could not be mapped", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, - "x-required-scope": "business:read", "x-mint": { - "content": "Linked Device API business:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" - }, - "post": { - "operationId": "createBusinessCatalog", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/business/compliance": { + "get": { + "operationId": "getBusinessMerchantCompliance", "tags": [ "Business App" ], - "summary": "Create a catalog for the connected WhatsApp Business App account", + "summary": "Read merchant compliance details for the connected WhatsApp Business App account", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:read", "parameters": [ { "schema": { @@ -16093,11 +18793,11 @@ ], "responses": { "200": { - "description": "Catalog created", + "description": "Live merchant compliance details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBusinessCatalogResponse" + "$ref": "#/components/schemas/GetBusinessMerchantComplianceResponse" } } } @@ -16153,28 +18853,26 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API profile:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - } - }, - "/api/{session}/business/products/{productId}": { - "get": { - "operationId": "getBusinessProduct", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" + }, + "put": { + "operationId": "setBusinessMerchantCompliance", "tags": [ "Business App" ], - "summary": "Read a WhatsApp Business App product", + "summary": "Replace merchant compliance details for the connected WhatsApp Business App account", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -16183,34 +18881,25 @@ "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string", - "maxLength": 256 - }, - "required": true, - "name": "productId", - "in": "path" - }, - { - "schema": { - "type": "string", - "maxLength": 256, - "pattern": "^\\d{1,32}@(s\\.whatsapp\\.net|lid)$" - }, - "required": true, - "name": "jid", - "in": "query" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessMerchantCompliance" + } + } + } + }, "responses": { "200": { - "description": "Live product", + "description": "Merchant compliance details updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBusinessProductResponse" + "$ref": "#/components/schemas/SetBusinessMerchantComplianceResponse" } } } @@ -16264,38 +18953,30 @@ } } } - }, - "502": { - "description": "Upstream WhatsApp request failed or its response could not be mapped", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, - "x-required-scope": "business:read", "x-mint": { - "content": "Linked Device API business:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" - }, - "put": { - "operationId": "updateBusinessProduct", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/business/quick-replies": { + "post": { + "operationId": "createBusinessQuickReply", "tags": [ "Business App" ], - "summary": "Replace a product in the connected WhatsApp Business App catalog", + "summary": "Create a WhatsApp Business App quick reply", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -16304,16 +18985,6 @@ "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "required": true, - "name": "productId", - "in": "path" } ], "requestBody": { @@ -16321,18 +18992,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessProductMutationRequest" + "$ref": "#/components/schemas/BusinessQuickReplyMutation" } } } }, "responses": { "200": { - "description": "Product updated", + "description": "Quick reply created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateBusinessProductResponse" + "$ref": "#/components/schemas/CreateBusinessQuickReplyResponse" } } } @@ -16388,7 +19059,6 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { "content": "Linked Device API profile:write" }, @@ -16397,17 +19067,18 @@ ], "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" }, - "delete": { - "operationId": "deleteBusinessProduct", + "get": { + "operationId": "listBusinessQuickReplies", "tags": [ "Business App" ], - "summary": "Delete a product from the connected WhatsApp Business App catalog", + "summary": "List remembered WhatsApp Business App quick replies", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:read", "parameters": [ { "schema": { @@ -16416,25 +19087,15 @@ "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256 - }, - "required": true, - "name": "productId", - "in": "path" } ], "responses": { "200": { - "description": "Product deleted", + "description": "Best-effort quick reply list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteBusinessProductResponse" + "$ref": "#/components/schemas/ListBusinessQuickRepliesResponse" } } } @@ -16490,104 +19151,66 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API profile:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" } }, - "/api/{session}/business/collections": { - "get": { - "operationId": "getBusinessCollections", + "/api/{session}/business/quick-replies/{quickReplyId}": { + "put": { + "operationId": "setBusinessQuickReply", "tags": [ "Business App" ], - "summary": "Read WhatsApp Business App collections", + "summary": "Replace a WhatsApp Business App quick reply", "security": [ { - "BearerAuth": [] - } - ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string", - "maxLength": 256, - "pattern": "^\\d{1,32}@(s\\.whatsapp\\.net|lid)$" - }, - "required": true, - "name": "jid", - "in": "query" - }, - { - "schema": { - "type": "string", - "maxLength": 2048 - }, - "required": false, - "name": "after", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 20 - }, - "required": false, - "name": "collectionLimit", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100 - }, - "required": false, - "name": "itemLimit", - "in": "query" - }, - { + "BearerAuth": [] + } + ], + "x-required-scope": "profile:write", + "parameters": [ + { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 1024 + "type": "string" }, - "required": false, - "name": "width", - "in": "query" + "required": true, + "name": "session", + "in": "path" }, { "schema": { - "type": "integer", - "minimum": 1, - "maximum": 1024 + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^[0-9]+$" }, - "required": false, - "name": "height", - "in": "query" + "required": true, + "name": "quickReplyId", + "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessQuickReplyMutation" + } + } + } + }, "responses": { "200": { - "description": "Live collection page", + "description": "Quick reply replaced", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBusinessCollectionsResponse" + "$ref": "#/components/schemas/SetBusinessQuickReplyResponse" } } } @@ -16641,38 +19264,28 @@ } } } - }, - "502": { - "description": "Upstream WhatsApp request failed or its response could not be mapped", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, - "x-required-scope": "business:read", "x-mint": { - "content": "Linked Device API business:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" }, - "post": { - "operationId": "createBusinessCollection", + "delete": { + "operationId": "deleteBusinessQuickReply", "tags": [ "Business App" ], - "summary": "Create a collection in the connected WhatsApp Business App catalog", + "summary": "Delete a WhatsApp Business App quick reply", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -16681,25 +19294,26 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^[0-9]+$" + }, + "required": true, + "name": "quickReplyId", + "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessCollectionCreateRequest" - } - } - } - }, "responses": { "200": { - "description": "Collection created", + "description": "Quick reply deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBusinessCollectionResponse" + "$ref": "#/components/schemas/DeleteBusinessQuickReplyResponse" } } } @@ -16755,7 +19369,6 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { "content": "Linked Device API profile:write" }, @@ -16765,18 +19378,19 @@ "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/business/collections/{collectionId}": { + "/api/{session}/business/catalog": { "get": { - "operationId": "getBusinessCollection", + "operationId": "getBusinessCatalog", "tags": [ "Business App" ], - "summary": "Read a WhatsApp Business App collection", + "summary": "Read a WhatsApp Business App catalog", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "business:read", "parameters": [ { "schema": { @@ -16786,15 +19400,6 @@ "name": "session", "in": "path" }, - { - "schema": { - "type": "string", - "maxLength": 256 - }, - "required": true, - "name": "collectionId", - "in": "path" - }, { "schema": { "type": "string", @@ -16847,11 +19452,11 @@ ], "responses": { "200": { - "description": "Live collection", + "description": "Live catalog page", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBusinessCollectionResponse" + "$ref": "#/components/schemas/GetBusinessCatalogResponse" } } } @@ -16917,7 +19522,6 @@ } } }, - "x-required-scope": "business:read", "x-mint": { "content": "Linked Device API business:read" }, @@ -16926,17 +19530,18 @@ ], "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" }, - "patch": { - "operationId": "updateBusinessCollection", + "post": { + "operationId": "createBusinessCatalog", "tags": [ "Business App" ], - "summary": "Update a collection and its product membership", + "summary": "Create a catalog for the connected WhatsApp Business App account", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -16945,15 +19550,99 @@ "required": true, "name": "session", "in": "path" + } + ], + "responses": { + "200": { + "description": "Catalog created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBusinessCatalogResponse" + } + } + } + }, + "400": { + "description": "Bad request — request body or params failed validation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Missing or invalid authentication token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Authenticated but not permitted (missing scope or rule violation)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "x-mint": { + "content": "Linked Device API profile:write" + }, + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/business/catalog/cart": { + "patch": { + "operationId": "setBusinessCartEnabled", + "tags": [ + "Business App" + ], + "summary": "Enable or disable the connected WhatsApp Business App catalog cart", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "profile:write", + "parameters": [ { "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256 + "type": "string" }, "required": true, - "name": "collectionId", + "name": "session", "in": "path" } ], @@ -16962,18 +19651,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessCollectionUpdateRequest" + "$ref": "#/components/schemas/BusinessCartSettingRequest" } } } }, "responses": { "200": { - "description": "Collection updated", + "description": "Cart setting updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateBusinessCollectionResponse" + "$ref": "#/components/schemas/SetBusinessCartEnabledResponse" } } } @@ -17029,7 +19718,6 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { "content": "Linked Device API profile:write" }, @@ -17037,18 +19725,21 @@ "Linked Device API" ], "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - }, - "delete": { - "operationId": "deleteBusinessCollection", + } + }, + "/api/{session}/business/products/{productId}": { + "get": { + "operationId": "getBusinessProduct", "tags": [ "Business App" ], - "summary": "Delete a collection from the connected WhatsApp Business App catalog", + "summary": "Read a WhatsApp Business App product", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "business:read", "parameters": [ { "schema": { @@ -17061,21 +19752,30 @@ { "schema": { "type": "string", - "minLength": 1, "maxLength": 256 }, "required": true, - "name": "collectionId", + "name": "productId", "in": "path" + }, + { + "schema": { + "type": "string", + "maxLength": 256, + "pattern": "^\\d{1,32}@(s\\.whatsapp\\.net|lid)$" + }, + "required": true, + "name": "jid", + "in": "query" } ], "responses": { "200": { - "description": "Collection deleted", + "description": "Live product", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteBusinessCollectionResponse" + "$ref": "#/components/schemas/GetBusinessProductResponse" } } } @@ -17129,30 +19829,38 @@ } } } + }, + "502": { + "description": "Upstream WhatsApp request failed or its response could not be mapped", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API business:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - } - }, - "/api/{session}/business/orders/{orderId}/lookup": { - "post": { - "operationId": "getBusinessOrder", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" + }, + "put": { + "operationId": "updateBusinessProduct", "tags": [ "Business App" ], - "summary": "Read WhatsApp Business App order details", + "summary": "Replace a product in the connected WhatsApp Business App catalog", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -17168,7 +19876,7 @@ "maxLength": 256 }, "required": true, - "name": "orderId", + "name": "productId", "in": "path" } ], @@ -17177,18 +19885,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessOrderLookupRequest" + "$ref": "#/components/schemas/BusinessProductMutationRequest" } } } }, "responses": { "200": { - "description": "Order details", + "description": "Product updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBusinessOrderResponse" + "$ref": "#/components/schemas/UpdateBusinessProductResponse" } } } @@ -17242,40 +19950,28 @@ } } } - }, - "502": { - "description": "Upstream WhatsApp request failed or its response could not be mapped", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, - "x-required-scope": "business:read", "x-mint": { - "content": "Linked Device API business:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" - } - }, - "/api/{session}/lids/resolve": { - "get": { - "operationId": "resolveLIDs", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + }, + "delete": { + "operationId": "deleteBusinessProduct", "tags": [ - "LIDs" + "Business App" ], - "summary": "Resolve a phone number to a LID (or vice versa)", + "summary": "Delete a product from the connected WhatsApp Business App catalog", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -17285,58 +19981,23 @@ "name": "session", "in": "path" }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "phoneNumber", - "in": "query" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "id", - "in": "query" - }, - { - "schema": { - "type": "string" - }, - "required": false, - "name": "lid", - "in": "query", - "deprecated": true - }, - { - "schema": { - "type": "string", - "minLength": 3, - "maxLength": 35 - }, - "required": false, - "name": "username", - "in": "query" - }, { "schema": { "type": "string", - "pattern": "^\\d{4}$" + "maxLength": 256 }, - "required": false, - "name": "usernameKey", - "in": "query" + "required": true, + "name": "productId", + "in": "path" } ], "responses": { "200": { - "description": "Resolved LID / phone mapping", + "description": "Product deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResolveLIDsResponse" + "$ref": "#/components/schemas/DeleteBusinessProductResponse" } } } @@ -17392,28 +20053,28 @@ } } }, - "x-required-scope": "contacts:read", "x-mint": { - "content": "Linked Device API contacts:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `contacts:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/channels": { - "get": { - "operationId": "listChannels", + "/api/{session}/business/products": { + "post": { + "operationId": "createBusinessProduct", "tags": [ - "Channels" + "Business App" ], - "summary": "List channels the session follows", + "summary": "Create a product in the connected WhatsApp Business App catalog", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -17424,13 +20085,23 @@ "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessProductMutationRequest" + } + } + } + }, "responses": { "200": { - "description": "Channel list", + "description": "Product created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListChannelsResponse" + "$ref": "#/components/schemas/CreateBusinessProductResponse" } } } @@ -17486,26 +20157,28 @@ } } }, - "x-required-scope": "channels:read", "x-mint": { - "content": "Linked Device API channels:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" - }, - "post": { - "operationId": "createChannel", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/business/products/{productId}/visibility": { + "patch": { + "operationId": "setBusinessProductVisibility", "tags": [ - "Channels" + "Business App" ], - "summary": "Create a new channel", + "summary": "Hide or show a product in the connected WhatsApp Business App catalog", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -17514,24 +20187,34 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string", + "maxLength": 256 + }, + "required": true, + "name": "productId", + "in": "path" } ], "requestBody": { + "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateChannelRequest" + "$ref": "#/components/schemas/BusinessProductVisibilityRequest" } } } }, "responses": { "200": { - "description": "Channel created", + "description": "Product visibility updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateChannelResponse" + "$ref": "#/components/schemas/SetBusinessProductVisibilityResponse" } } } @@ -17587,28 +20270,28 @@ } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/channels/{id}": { - "get": { - "operationId": "getChannel", + "/api/{session}/business/products/{productId}/appeal": { + "post": { + "operationId": "appealBusinessProduct", "tags": [ - "Channels" + "Business App" ], - "summary": "Get channel details", + "summary": "Request a review of a rejected WhatsApp Business App catalog product", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -17620,20 +20303,31 @@ }, { "schema": { - "type": "string" + "type": "string", + "maxLength": 256 }, "required": true, - "name": "id", + "name": "productId", "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessCatalogAppealRequest" + } + } + } + }, "responses": { "200": { - "description": "Channel details", + "description": "Product appeal submitted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetChannelResponse" + "$ref": "#/components/schemas/AppealBusinessProductResponse" } } } @@ -17689,51 +20383,104 @@ } } }, - "x-required-scope": "channels:read", "x-mint": { - "content": "Linked Device API channels:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" - }, - "delete": { - "operationId": "deleteChannel", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/business/collections": { + "get": { + "operationId": "getBusinessCollections", "tags": [ - "Channels" + "Business App" ], - "summary": "Delete a channel", + "summary": "Read WhatsApp Business App collections", "security": [ { - "BearerAuth": [] - } - ], - "parameters": [ + "BearerAuth": [] + } + ], + "x-required-scope": "business:read", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "maxLength": 256, + "pattern": "^\\d{1,32}@(s\\.whatsapp\\.net|lid)$" + }, + "required": true, + "name": "jid", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 2048 + }, + "required": false, + "name": "after", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 20 + }, + "required": false, + "name": "collectionLimit", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + }, + "required": false, + "name": "itemLimit", + "in": "query" + }, { "schema": { - "type": "string" + "type": "integer", + "minimum": 1, + "maximum": 1024 }, - "required": true, - "name": "session", - "in": "path" + "required": false, + "name": "width", + "in": "query" }, { "schema": { - "type": "string" + "type": "integer", + "minimum": 1, + "maximum": 1024 }, - "required": true, - "name": "id", - "in": "path" + "required": false, + "name": "height", + "in": "query" } ], "responses": { "200": { - "description": "Channel deleted", + "description": "Live collection page", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteChannelResponse" + "$ref": "#/components/schemas/GetBusinessCollectionsResponse" } } } @@ -17799,28 +20546,26 @@ } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API business:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" - } - }, - "/api/{session}/channels/{id}/messages": { - "get": { - "operationId": "listChannelMessages", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" + }, + "post": { + "operationId": "createBusinessCollection", "tags": [ - "Channels" + "Business App" ], - "summary": "List channel messages", + "summary": "Create a collection in the connected WhatsApp Business App catalog", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -17829,44 +20574,25 @@ "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 50 - }, - "required": false, - "name": "count", - "in": "query" - }, - { - "schema": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true - }, - "required": false, - "name": "before", - "in": "query" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessCollectionCreateRequest" + } + } + } + }, "responses": { "200": { - "description": "Channel messages", + "description": "Collection created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListChannelMessagesResponse" + "$ref": "#/components/schemas/CreateBusinessCollectionResponse" } } } @@ -17922,28 +20648,28 @@ } } }, - "x-required-scope": "channels:read", "x-mint": { - "content": "Linked Device API channels:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/channels/{id}/message-updates": { + "/api/{session}/business/collections/{collectionId}": { "get": { - "operationId": "listChannelMessageUpdates", + "operationId": "getBusinessCollection", "tags": [ - "Channels" + "Business App" ], - "summary": "List channel view and reaction updates", + "summary": "Read a WhatsApp Business App collection", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "business:read", "parameters": [ { "schema": { @@ -17955,51 +20681,70 @@ }, { "schema": { - "type": "string" + "type": "string", + "maxLength": 256 }, "required": true, - "name": "id", + "name": "collectionId", "in": "path" }, + { + "schema": { + "type": "string", + "maxLength": 256, + "pattern": "^\\d{1,32}@(s\\.whatsapp\\.net|lid)$" + }, + "required": true, + "name": "jid", + "in": "query" + }, + { + "schema": { + "type": "string", + "maxLength": 2048 + }, + "required": false, + "name": "after", + "in": "query" + }, { "schema": { "type": "integer", "minimum": 1, - "maximum": 100, - "default": 50 + "maximum": 100 }, "required": false, - "name": "count", + "name": "limit", "in": "query" }, { "schema": { "type": "integer", - "nullable": true, - "minimum": 0 + "minimum": 1, + "maximum": 1024 }, "required": false, - "name": "since", + "name": "width", "in": "query" }, { "schema": { "type": "integer", - "minimum": 0, - "exclusiveMinimum": true + "minimum": 1, + "maximum": 1024 }, "required": false, - "name": "after", + "name": "height", "in": "query" } ], "responses": { "200": { - "description": "Channel message updates", + "description": "Live collection", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListChannelMessageUpdatesResponse" + "$ref": "#/components/schemas/GetBusinessCollectionResponse" } } } @@ -18053,30 +20798,38 @@ } } } + }, + "502": { + "description": "Upstream WhatsApp request failed or its response could not be mapped", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "channels:read", "x-mint": { - "content": "Linked Device API channels:read" + "content": "Linked Device API business:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" - } - }, - "/api/{session}/channels/{id}/messages/{serverId}/viewed": { - "post": { - "operationId": "markChannelMessageViewed", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" + }, + "patch": { + "operationId": "updateBusinessCollection", "tags": [ - "Channels" + "Business App" ], - "summary": "Mark a channel message as viewed", + "summary": "Update a collection and its product membership", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -18088,30 +20841,31 @@ }, { "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true + "type": "string", + "maxLength": 256 }, "required": true, - "name": "serverId", + "name": "collectionId", "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessCollectionUpdateRequest" + } + } + } + }, "responses": { "200": { - "description": "Message marked viewed", + "description": "Collection updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/UpdateBusinessCollectionResponse" } } } @@ -18167,28 +20921,26 @@ } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" - } - }, - "/api/{session}/channels/{id}/messages/{serverId}/reaction": { - "post": { - "operationId": "reactToChannelMessage", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + }, + "delete": { + "operationId": "deleteBusinessCollection", "tags": [ - "Channels" + "Business App" ], - "summary": "React to a channel message", + "summary": "Delete a collection from the connected WhatsApp Business App catalog", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -18200,39 +20952,21 @@ }, { "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" - }, - { - "schema": { - "type": "integer", - "minimum": 0, - "exclusiveMinimum": true + "type": "string", + "maxLength": 256 }, "required": true, - "name": "serverId", + "name": "collectionId", "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChannelReactionRequest" - } - } - } - }, "responses": { "200": { - "description": "Reaction updated", + "description": "Collection deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/DeleteBusinessCollectionResponse" } } } @@ -18288,28 +21022,28 @@ } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/channels/{id}/live-updates": { + "/api/{session}/business/collections/reorder": { "post": { - "operationId": "subscribeChannelLiveUpdates", + "operationId": "reorderBusinessCollections", "tags": [ - "Channels" + "Business App" ], - "summary": "Subscribe temporarily to channel live updates", + "summary": "Reorder connected WhatsApp Business App catalog collections", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -18318,23 +21052,25 @@ "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessCollectionReorderRequest" + } + } + } + }, "responses": { "200": { - "description": "Live-update subscription duration", + "description": "Collections reordered", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChannelLiveUpdatesResponse" + "$ref": "#/components/schemas/ReorderBusinessCollectionsResponse" } } } @@ -18390,28 +21126,28 @@ } } }, - "x-required-scope": "channels:read", "x-mint": { - "content": "Linked Device API channels:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/channels/{id}/follow": { + "/api/{session}/business/collections/{collectionId}/appeal": { "post": { - "operationId": "followChannel", + "operationId": "appealBusinessCollection", "tags": [ - "Channels" + "Business App" ], - "summary": "Follow a channel", + "summary": "Request a review of a rejected WhatsApp Business App catalog collection", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { @@ -18423,20 +21159,31 @@ }, { "schema": { - "type": "string" + "type": "string", + "maxLength": 256 }, "required": true, - "name": "id", + "name": "collectionId", "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessCatalogAppealRequest" + } + } + } + }, "responses": { "200": { - "description": "Channel followed", + "description": "Collection appeal submitted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/AppealBusinessCollectionResponse" } } } @@ -18492,28 +21239,28 @@ } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/{session}/channels/{id}/unfollow": { + "/api/{session}/business/orders/{orderId}/lookup": { "post": { - "operationId": "unfollowChannel", + "operationId": "getBusinessOrder", "tags": [ - "Channels" + "Business App" ], - "summary": "Unfollow a channel", + "summary": "Read WhatsApp Business App order details", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "business:read", "parameters": [ { "schema": { @@ -18525,20 +21272,31 @@ }, { "schema": { - "type": "string" + "type": "string", + "maxLength": 256 }, "required": true, - "name": "id", + "name": "orderId", "in": "path" } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BusinessOrderLookupRequest" + } + } + } + }, "responses": { "200": { - "description": "Channel unfollowed", + "description": "Order details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/GetBusinessOrderResponse" } } } @@ -18592,30 +21350,40 @@ } } } + }, + "502": { + "description": "Upstream WhatsApp request failed or its response could not be mapped", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API business:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" } }, - "/api/{session}/channels/{id}/mute": { - "post": { - "operationId": "muteChannel", + "/api/{session}/lids/resolve": { + "get": { + "operationId": "resolveLIDs", "tags": [ - "Channels" + "LIDs" ], - "summary": "Mute a channel", + "summary": "Resolve a user alias to a stable user ID", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -18629,18 +21397,54 @@ "schema": { "type": "string" }, - "required": true, + "required": false, + "name": "phoneNumber", + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, "name": "id", - "in": "path" + "in": "query" + }, + { + "schema": { + "type": "string" + }, + "required": false, + "name": "lid", + "in": "query", + "deprecated": true + }, + { + "schema": { + "type": "string", + "minLength": 3, + "maxLength": 35 + }, + "required": false, + "name": "username", + "in": "query" + }, + { + "schema": { + "type": "string", + "pattern": "^\\d{4}$" + }, + "required": false, + "name": "usernameKey", + "in": "query" } ], "responses": { "200": { - "description": "Channel muted", + "description": "Stable user ID and known aliases", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/ResolveLIDsResponse" } } } @@ -18696,28 +21500,28 @@ } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API contacts:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `contacts:read`" } }, - "/api/{session}/channels/{id}/unmute": { - "post": { - "operationId": "unmuteChannel", + "/api/{session}/users/{id}/security-code": { + "get": { + "operationId": "getUserSecurityCode", "tags": [ - "Channels" + "Users" ], - "summary": "Unmute a channel", + "summary": "Get an identity verification code for a user", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "contacts:read", "parameters": [ { "schema": { @@ -18729,7 +21533,8 @@ }, { "schema": { - "type": "string" + "type": "string", + "pattern": "^\\d+@lid$" }, "required": true, "name": "id", @@ -18738,11 +21543,11 @@ ], "responses": { "200": { - "description": "Channel unmuted", + "description": "Identity verification code and display QR", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/UserSecurityCodeResponse" } } } @@ -18798,28 +21603,28 @@ } } }, - "x-required-scope": "channels:manage", "x-mint": { - "content": "Linked Device API channels:manage" + "content": "Linked Device API contacts:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `contacts:read`" } }, - "/api/{session}/presence": { - "post": { - "operationId": "setPresence", + "/api/{session}/channels": { + "get": { + "operationId": "listChannels", "tags": [ - "Presence" + "Channels" ], - "summary": "Set the session's own presence (available / unavailable)", + "summary": "List channels the session follows", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:read", "parameters": [ { "schema": { @@ -18830,32 +21635,13 @@ "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetPresenceRequest" - } - } - } - }, "responses": { "200": { - "description": "Presence updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "202": { - "description": "Accepted for asynchronous processing in response to `Prefer: respond-async`; the result is not included", + "description": "Channel list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AsyncAcceptedResponse" + "$ref": "#/components/schemas/ListChannelsResponse" } } } @@ -18911,26 +21697,26 @@ } } }, - "x-required-scope": "presence:write", "x-mint": { - "content": "Linked Device API presence:write" + "content": "Linked Device API channels:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" }, - "get": { - "operationId": "getPresence", + "post": { + "operationId": "createChannel", "tags": [ - "Presence" + "Channels" ], - "summary": "Get the session's own presence", + "summary": "Create a new channel", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { @@ -18940,14 +21726,23 @@ "name": "session", "in": "path" } - ], + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateChannelRequest" + } + } + } + }, "responses": { "200": { - "description": "Presence info", + "description": "Channel created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetPresenceResponse" + "$ref": "#/components/schemas/CreateChannelResponse" } } } @@ -19003,28 +21798,28 @@ } } }, - "x-required-scope": "presence:read", "x-mint": { - "content": "Linked Device API presence:read" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" } }, - "/api/{session}/presence/{chatId}": { + "/api/{session}/channels/{id}": { "get": { - "operationId": "getChatPresence", + "operationId": "getChannel", "tags": [ - "Presence" + "Channels" ], - "summary": "Get presence for a specific chat", + "summary": "Get channel details", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:read", "parameters": [ { "schema": { @@ -19039,17 +21834,17 @@ "type": "string" }, "required": true, - "name": "chatId", + "name": "id", "in": "path" } ], "responses": { "200": { - "description": "Chat presence", + "description": "Channel details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetChatPresenceResponse" + "$ref": "#/components/schemas/GetChannelResponse" } } } @@ -19105,28 +21900,26 @@ } } }, - "x-required-scope": "presence:read", "x-mint": { - "content": "Linked Device API presence:read" + "content": "Linked Device API channels:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:read`" - } - }, - "/api/{session}/presence/{chatId}/subscribe": { - "post": { - "operationId": "subscribePresence", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" + }, + "delete": { + "operationId": "deleteChannel", "tags": [ - "Presence" + "Channels" ], - "summary": "Subscribe to presence updates for a chat", + "summary": "Delete a channel", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { @@ -19141,27 +21934,17 @@ "type": "string" }, "required": true, - "name": "chatId", + "name": "id", "in": "path" } ], "responses": { "200": { - "description": "Subscribed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SubscribePresenceResponse" - } - } - } - }, - "202": { - "description": "Accepted for asynchronous processing in response to `Prefer: respond-async`; the result is not included", + "description": "Channel deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AsyncAcceptedResponse" + "$ref": "#/components/schemas/DeleteChannelResponse" } } } @@ -19206,8 +21989,8 @@ } } }, - "429": { - "description": "Observation limit exceeded, or the session is inside the suspension window that follows", + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -19216,8 +21999,8 @@ } } }, - "500": { - "description": "Internal server error", + "502": { + "description": "Upstream WhatsApp request failed or its response could not be mapped", "content": { "application/json": { "schema": { @@ -19227,46 +22010,74 @@ } } }, - "x-required-scope": "presence:observe", "x-mint": { - "content": "Linked Device API presence:observe" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:observe`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" } }, - "/api/projects/{projectId}/observation-policy": { + "/api/{session}/channels/{id}/messages": { "get": { - "operationId": "getProjectObservationPolicy", + "operationId": "listChannelMessages", "tags": [ - "Presence", - "Labels" + "Channels" ], + "summary": "List channel messages", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:read", "parameters": [ { "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, "required": true, - "name": "projectId", + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "required": false, + "name": "count", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "required": false, + "name": "before", + "in": "query" } ], "responses": { "200": { - "description": "Project observation ceiling", + "description": "Channel messages", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetProjectObservationPolicyResponse" + "$ref": "#/components/schemas/ListChannelMessagesResponse" } } } @@ -19322,82 +22133,84 @@ } } }, - "x-required-scope": "presence:read", "x-mint": { - "content": "presence:read" + "content": "Linked Device API channels:read" }, - "description": "**Required scope:** `presence:read`" - }, - "put": { - "operationId": "updateProjectObservationPolicy", + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" + } + }, + "/api/{session}/channels/{id}/message-updates": { + "get": { + "operationId": "listChannelMessageUpdates", "tags": [ - "Presence", - "Labels" + "Channels" ], + "summary": "List channel view and reaction updates", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:read", "parameters": [ { "schema": { - "type": "string", - "format": "uuid" + "type": "string" }, "required": true, - "name": "projectId", + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "required": false, + "name": "count", + "in": "query" + }, + { + "schema": { + "type": "integer", + "nullable": true, + "minimum": 0 + }, + "required": false, + "name": "since", + "in": "query" + }, + { + "schema": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "required": false, + "name": "after", + "in": "query" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "presenceMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "typingMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache" - ] - }, - "labelMode": { - "type": "string", - "enum": [ - "off", - "events", - "cache", - "project" - ] - } - }, - "required": [ - "presenceMode", - "typingMode" - ], - "additionalProperties": false - } - } - } - }, "responses": { "200": { - "description": "Project observation ceiling updated", + "description": "Channel message updates", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateProjectObservationPolicyResponse" + "$ref": "#/components/schemas/ListChannelMessageUpdatesResponse" } } } @@ -19441,29 +22254,9 @@ } } } - }, - "409": { - "description": "Label projection cleanup is active or failed; resolve failed cleanup before re-enabling project retention", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "503": { - "description": "Policy was stored but a connected runner did not acknowledge it; retry the request", + }, + "500": { + "description": "Internal server error", "content": { "application/json": { "schema": { @@ -19473,43 +22266,63 @@ } } }, - "x-required-scope": "presence:observe", "x-mint": { - "content": "presence:observe" + "content": "Linked Device API channels:read" }, - "description": "**Required scope:** `presence:observe`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" } }, - "/api/{session}/observation-policy": { - "get": { - "operationId": "getSessionObservationPolicy", + "/api/{session}/channels/{id}/messages/{serverId}/viewed": { + "post": { + "operationId": "markChannelMessageViewed", "tags": [ - "Presence", - "Labels" + "Channels" ], + "summary": "Mark a channel message as viewed", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { - "type": "string", - "minLength": 1 + "type": "string" }, "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "required": true, + "name": "serverId", + "in": "path" } ], "responses": { "200": { - "description": "Effective session observation policy", + "description": "Message marked viewed", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetSessionObservationPolicyResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -19565,88 +22378,72 @@ } } }, - "x-required-scope": "presence:read", "x-mint": { - "content": "Linked Device API presence:read" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:read`" - }, - "put": { - "operationId": "updateSessionObservationPolicy", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + } + }, + "/api/{session}/channels/{id}/messages/{serverId}/reaction": { + "post": { + "operationId": "reactToChannelMessage", "tags": [ - "Presence", - "Labels" + "Channels" ], + "summary": "React to a channel message", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { - "type": "string", - "minLength": 1 + "type": "string" }, "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + }, + { + "schema": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true + }, + "required": true, + "name": "serverId", + "in": "path" } ], "requestBody": { "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "presenceMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "events", - "cache" - ] - }, - "typingMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "events", - "cache" - ] - }, - "labelMode": { - "type": "string", - "enum": [ - "inherit", - "off", - "events", - "cache", - "project" - ] - } - }, - "required": [ - "presenceMode", - "typingMode" - ], - "additionalProperties": false + "$ref": "#/components/schemas/ChannelReactionRequest" } } } }, "responses": { "200": { - "description": "Session observation override updated", + "description": "Reaction updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateSessionObservationPolicyResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -19691,16 +22488,6 @@ } } }, - "409": { - "description": "Label projection cleanup is active or failed; resolve failed cleanup before re-enabling project retention", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -19710,40 +22497,30 @@ } } } - }, - "503": { - "description": "Policy was stored but a connected runner did not acknowledge it; retry the request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } } }, - "x-required-scope": "presence:observe", "x-mint": { - "content": "Linked Device API presence:observe" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:observe`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" } }, - "/api/{session}/profile": { - "get": { - "operationId": "getProfile", + "/api/{session}/channels/{id}/live-updates": { + "post": { + "operationId": "subscribeChannelLiveUpdates", "tags": [ - "Profile" + "Channels" ], - "summary": "Get the session's own profile", + "summary": "Subscribe temporarily to channel live updates", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:read", "parameters": [ { "schema": { @@ -19752,15 +22529,23 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" } ], "responses": { "200": { - "description": "Profile info", + "description": "Live-update subscription duration", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetProfileResponse" + "$ref": "#/components/schemas/ChannelLiveUpdatesResponse" } } } @@ -19816,28 +22601,28 @@ } } }, - "x-required-scope": "profile:read", "x-mint": { - "content": "Linked Device API profile:read" + "content": "Linked Device API channels:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:read`" } }, - "/api/{session}/profile/name": { - "put": { - "operationId": "setProfileName", + "/api/{session}/channels/{id}/follow": { + "post": { + "operationId": "followChannel", "tags": [ - "Profile" + "Channels" ], - "summary": "Set profile display name (push name)", + "summary": "Follow a channel", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { @@ -19846,20 +22631,19 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetProfileNameRequest" - } - } - } - }, "responses": { "200": { - "description": "Name updated", + "description": "Channel followed", "content": { "application/json": { "schema": { @@ -19919,28 +22703,28 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" } }, - "/api/{session}/profile/status": { - "put": { - "operationId": "setProfileStatus", + "/api/{session}/channels/{id}/unfollow": { + "post": { + "operationId": "unfollowChannel", "tags": [ - "Profile" + "Channels" ], - "summary": "Set profile status (about) text", + "summary": "Unfollow a channel", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { @@ -19949,20 +22733,19 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetProfileStatusRequest" - } - } - } - }, "responses": { "200": { - "description": "Status updated", + "description": "Channel unfollowed", "content": { "application/json": { "schema": { @@ -20022,28 +22805,28 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" } }, - "/api/{session}/profile/picture": { - "put": { - "operationId": "setProfilePicture", + "/api/{session}/channels/{id}/mute": { + "post": { + "operationId": "muteChannel", "tags": [ - "Profile" + "Channels" ], - "summary": "Set profile picture", + "summary": "Mute a channel", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { @@ -20052,20 +22835,19 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetProfilePictureRequest" - } - } - } - }, "responses": { "200": { - "description": "Picture updated", + "description": "Channel muted", "content": { "application/json": { "schema": { @@ -20125,26 +22907,28 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - }, - "delete": { - "operationId": "deleteProfilePicture", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" + } + }, + "/api/{session}/channels/{id}/unmute": { + "post": { + "operationId": "unmuteChannel", "tags": [ - "Profile" + "Channels" ], - "summary": "Remove the profile picture", + "summary": "Unmute a channel", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "channels:manage", "parameters": [ { "schema": { @@ -20153,11 +22937,19 @@ "required": true, "name": "session", "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" } ], "responses": { "200": { - "description": "Picture deleted", + "description": "Channel unmuted", "content": { "application/json": { "schema": { @@ -20217,28 +23009,28 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API channels:manage" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `channels:manage`" } }, - "/api/{session}/privacy": { - "get": { - "operationId": "getPrivacySettings", + "/api/{session}/presence": { + "post": { + "operationId": "setPresence", "tags": [ - "Privacy" + "Presence" ], - "summary": "Get privacy settings", + "summary": "Set the session's own presence (available / unavailable)", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:write", "parameters": [ { "schema": { @@ -20249,13 +23041,32 @@ "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetPresenceRequest" + } + } + } + }, "responses": { "200": { - "description": "Privacy settings", + "description": "Presence updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PrivacySettingsResponse" + "$ref": "#/components/schemas/SuccessResponse" + } + } + } + }, + "202": { + "description": "Accepted for asynchronous processing in response to `Prefer: respond-async`; the result is not included", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AsyncAcceptedResponse" } } } @@ -20311,28 +23122,26 @@ } } }, - "x-required-scope": "profile:read", "x-mint": { - "content": "Linked Device API profile:read" + "content": "Linked Device API presence:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" - } - }, - "/api/{session}/privacy/disappearing/default": { - "put": { - "operationId": "setDefaultDisappearingTimer", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:write`" + }, + "get": { + "operationId": "getPresence", "tags": [ - "Privacy" + "Presence" ], - "summary": "Set the default disappearing-message timer", + "summary": "Get the session's own presence", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:read", "parameters": [ { "schema": { @@ -20343,22 +23152,13 @@ "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DefaultDisappearingTimerRequest" - } - } - } - }, "responses": { "200": { - "description": "Default disappearing-message timer updated", + "description": "Presence info", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/GetPresenceResponse" } } } @@ -20414,28 +23214,28 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API presence:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:read`" } }, - "/api/{session}/privacy/{setting}": { - "put": { - "operationId": "setPrivacySetting", + "/api/{session}/presence/{chatId}": { + "get": { + "operationId": "getChatPresence", "tags": [ - "Privacy" + "Presence" ], - "summary": "Update a privacy setting", + "summary": "Get presence for a specific chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:read", "parameters": [ { "schema": { @@ -20447,41 +23247,20 @@ }, { "schema": { - "type": "string", - "enum": [ - "groupadd", - "last", - "status", - "profile", - "readreceipts", - "online", - "calladd", - "messages", - "defense", - "stickers" - ] + "type": "string" }, "required": true, - "name": "setting", + "name": "chatId", "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetPrivacySettingRequest" - } - } - } - }, "responses": { "200": { - "description": "Updated privacy settings", + "description": "Chat presence", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdatedPrivacySettingsResponse" + "$ref": "#/components/schemas/GetChatPresenceResponse" } } } @@ -20537,28 +23316,28 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "Linked Device API presence:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:read`" } }, - "/api/{session}/labels": { - "get": { - "operationId": "listLabels", + "/api/{session}/presence/{chatId}/subscribe": { + "post": { + "operationId": "subscribePresence", "tags": [ - "Labels" + "Presence" ], - "summary": "List labels defined for the session", + "summary": "Subscribe to presence updates for a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:observe", "parameters": [ { "schema": { @@ -20570,24 +23349,30 @@ }, { "schema": { - "type": "string", - "enum": [ - "true", - "false" - ] + "type": "string" }, - "required": false, - "name": "includeObservation", - "in": "query" + "required": true, + "name": "chatId", + "in": "path" } ], "responses": { "200": { - "description": "Label list", + "description": "Subscribed", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListLabelsResponse" + "$ref": "#/components/schemas/SubscribePresenceResponse" + } + } + } + }, + "202": { + "description": "Accepted for asynchronous processing in response to `Prefer: respond-async`; the result is not included", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AsyncAcceptedResponse" } } } @@ -20632,6 +23417,16 @@ } } }, + "429": { + "description": "Observation limit exceeded, or the session is inside the suspension window that follows", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -20643,52 +23438,47 @@ } } }, - "x-required-scope": "labels:read", "x-mint": { - "content": "Linked Device API labels:read" + "content": "Linked Device API presence:observe" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:read`" - }, - "post": { - "operationId": "createLabel", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:observe`" + } + }, + "/api/projects/{projectId}/observation-policy": { + "get": { + "operationId": "getProjectObservationPolicy", "tags": [ - "Labels" + "Presence", + "Labels", + "Business App" ], - "summary": "Create a label", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:read", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "format": "uuid" }, "required": true, - "name": "session", + "name": "projectId", "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateLabelRequest" - } - } - } - }, "responses": { "200": { - "description": "Label created", + "description": "Project observation ceiling", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateLabelResponse" + "$ref": "#/components/schemas/GetProjectObservationPolicyResponse" } } } @@ -20744,43 +23534,32 @@ } } }, - "x-required-scope": "labels:manage", "x-mint": { - "content": "Linked Device API labels:manage" + "content": "presence:read" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" - } - }, - "/api/{session}/labels/{labelId}": { + "description": "**Required scope:** `presence:read`" + }, "put": { - "operationId": "updateLabel", + "operationId": "updateProjectObservationPolicy", "tags": [ - "Labels" + "Presence", + "Labels", + "Business App" ], - "summary": "Update a label", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:observe", "parameters": [ { "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string" + "type": "string", + "format": "uuid" }, "required": true, - "name": "labelId", + "name": "projectId", "in": "path" } ], @@ -20788,18 +23567,58 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateLabelRequest" + "type": "object", + "properties": { + "presenceMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "typingMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + }, + "labelMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache", + "project" + ] + }, + "quickReplyMode": { + "type": "string", + "enum": [ + "off", + "events", + "cache" + ] + } + }, + "required": [ + "presenceMode", + "typingMode" + ], + "additionalProperties": false } } } }, "responses": { "200": { - "description": "Label updated", + "description": "Project observation ceiling updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/UpdateProjectObservationPolicyResponse" } } } @@ -20845,7 +23664,7 @@ } }, "409": { - "description": "The session has no authoritative view of its labels yet. Retry once a cached or project-retained snapshot is complete.", + "description": "Label projection cleanup is active or failed; resolve failed cleanup before re-enabling project retention", "content": { "application/json": { "schema": { @@ -20863,53 +23682,56 @@ } } } + }, + "503": { + "description": "Policy was stored but a connected runner did not acknowledge it; retry the request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "labels:manage", "x-mint": { - "content": "Linked Device API labels:manage" + "content": "presence:observe" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" - }, - "delete": { - "operationId": "deleteLabel", + "description": "**Required scope:** `presence:observe`" + } + }, + "/api/{session}/observation-policy": { + "get": { + "operationId": "getSessionObservationPolicy", "tags": [ - "Labels" + "Presence", + "Labels", + "Business App" ], - "summary": "Delete a label", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:read", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "minLength": 1 }, "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "labelId", - "in": "path" } ], "responses": { "200": { - "description": "Label deleted", + "description": "Effective session observation policy", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/GetSessionObservationPolicyResponse" } } } @@ -20965,65 +23787,98 @@ } } }, - "x-required-scope": "labels:manage", "x-mint": { - "content": "Linked Device API labels:manage" + "content": "Linked Device API presence:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" - } - }, - "/api/{session}/labels/chats/{chatId}": { - "get": { - "operationId": "getChatLabels", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:read`" + }, + "put": { + "operationId": "updateSessionObservationPolicy", "tags": [ - "Labels" + "Presence", + "Labels", + "Business App" ], - "summary": "Get labels attached to a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "presence:observe", "parameters": [ { "schema": { - "type": "string" + "type": "string", + "minLength": 1 }, "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "chatId", - "in": "path" - }, - { - "schema": { - "type": "string", - "enum": [ - "true", - "false" - ] - }, - "required": false, - "name": "includeObservation", - "in": "query" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "presenceMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache" + ] + }, + "typingMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache" + ] + }, + "labelMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache", + "project" + ] + }, + "quickReplyMode": { + "type": "string", + "enum": [ + "inherit", + "off", + "events", + "cache" + ] + } + }, + "required": [ + "presenceMode", + "typingMode" + ], + "additionalProperties": false + } + } + } + }, "responses": { "200": { - "description": "Chat labels", + "description": "Session observation override updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetChatLabelsResponse" + "$ref": "#/components/schemas/UpdateSessionObservationPolicyResponse" } } } @@ -21068,6 +23923,16 @@ } } }, + "409": { + "description": "Label projection cleanup is active or failed; resolve failed cleanup before re-enabling project retention", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -21077,28 +23942,40 @@ } } } + }, + "503": { + "description": "Policy was stored but a connected runner did not acknowledge it; retry the request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } }, - "x-required-scope": "labels:read", "x-mint": { - "content": "Linked Device API labels:read" + "content": "Linked Device API presence:observe" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:read`" - }, - "put": { - "operationId": "setChatLabels", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `presence:observe`" + } + }, + "/api/{session}/profile": { + "get": { + "operationId": "getProfile", "tags": [ - "Labels" + "Profile" ], - "summary": "Replace the set of labels attached to a chat", + "summary": "Get the session's own profile", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:read", "parameters": [ { "schema": { @@ -21107,32 +23984,15 @@ "required": true, "name": "session", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "chatId", - "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetChatLabelsRequest" - } - } - } - }, "responses": { "200": { - "description": "Chat labels updated", + "description": "Profile info", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/GetProfileResponse" } } } @@ -21177,16 +24037,6 @@ } } }, - "409": { - "description": "The session has no authoritative view of its labels yet. Retry once a cached or project-retained snapshot is complete.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "500": { "description": "Internal server error", "content": { @@ -21198,35 +24048,54 @@ } } }, - "x-required-scope": "labels:manage", "x-mint": { - "content": "Linked Device API labels:manage" + "content": "Linked Device API profile:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" } }, - "/api/webhooks": { - "get": { - "operationId": "listWebhooks", + "/api/{session}/profile/name": { + "put": { + "operationId": "setProfileName", "tags": [ - "Webhooks" + "Profile" ], - "summary": "List webhooks for the tenant", + "summary": "Set profile display name (push name)", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetProfileNameRequest" + } + } + } + }, "responses": { "200": { - "description": "Webhook list", + "description": "Name updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListWebhooksResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -21282,39 +24151,54 @@ } } }, - "x-required-scope": "webhooks:manage", "x-mint": { - "content": "webhooks:manage" + "content": "Linked Device API profile:write" }, - "description": "**Required scope:** `webhooks:manage`" - }, - "post": { - "operationId": "createWebhook", + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/profile/status": { + "put": { + "operationId": "setProfileStatus", "tags": [ - "Webhooks" + "Profile" ], - "summary": "Register a new webhook", + "summary": "Set profile status (about) text", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "session", + "in": "path" + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWebhookRequest" + "$ref": "#/components/schemas/SetProfileStatusRequest" } } } }, "responses": { - "201": { - "description": "Webhook created", + "200": { + "description": "Status updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWebhookResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -21370,42 +24254,54 @@ } } }, - "x-required-scope": "webhooks:manage", "x-mint": { - "content": "webhooks:manage" + "content": "Linked Device API profile:write" }, - "description": "**Required scope:** `webhooks:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/webhooks/{id}": { - "get": { - "operationId": "getWebhook", + "/api/{session}/profile/picture": { + "put": { + "operationId": "setProfilePicture", "tags": [ - "Webhooks" + "Profile" ], - "summary": "Get a webhook by id", + "summary": "Set profile picture", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "session", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetProfilePictureRequest" + } + } + } + }, "responses": { "200": { - "description": "Webhook details", + "description": "Picture updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetWebhookResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -21461,49 +24357,43 @@ } } }, - "x-required-scope": "webhooks:manage", "x-mint": { - "content": "webhooks:manage" + "content": "Linked Device API profile:write" }, - "description": "**Required scope:** `webhooks:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" }, - "put": { - "operationId": "updateWebhook", + "delete": { + "operationId": "deleteProfilePicture", "tags": [ - "Webhooks" + "Profile" ], - "summary": "Update a webhook", + "summary": "Remove the profile picture", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "session", "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateWebhookRequest" - } - } - } - }, "responses": { "200": { - "description": "Webhook updated", + "description": "Picture deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateWebhookResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -21559,40 +24449,45 @@ } } }, - "x-required-scope": "webhooks:manage", "x-mint": { - "content": "webhooks:manage" + "content": "Linked Device API profile:write" }, - "description": "**Required scope:** `webhooks:manage`" - }, - "delete": { - "operationId": "deleteWebhook", + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + } + }, + "/api/{session}/privacy": { + "get": { + "operationId": "getPrivacySettings", "tags": [ - "Webhooks" + "Privacy" ], - "summary": "Delete a webhook", + "summary": "Get privacy settings", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:read", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "session", "in": "path" } ], "responses": { "200": { - "description": "Webhook deleted", + "description": "Privacy settings", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/PrivacySettingsResponse" } } } @@ -21648,41 +24543,57 @@ } } }, - "x-required-scope": "webhooks:manage", "x-mint": { - "content": "webhooks:manage" + "content": "Linked Device API profile:read" }, - "description": "**Required scope:** `webhooks:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" } }, - "/api/media/{id}": { - "get": { - "operationId": "downloadMedia", + "/api/{session}/privacy/disappearing/default": { + "put": { + "operationId": "setDefaultDisappearingTimer", "tags": [ - "Media" + "Privacy" ], - "summary": "Download media (binary stream or redirect to S3)", + "summary": "Set the default disappearing-message timer", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "session", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DefaultDisappearingTimerRequest" + } + } + } + }, "responses": { "200": { - "description": "Media binary stream" - }, - "302": { - "description": "Redirect to S3" + "description": "Default disappearing-message timer updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse" + } + } + } }, "400": { "description": "Bad request — request body or params failed validation", @@ -21735,45 +24646,74 @@ } } }, - "x-required-scope": "media:read", "x-mint": { - "content": "Linked Device API media:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `media:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/media/{id}/info": { - "get": { - "operationId": "getMediaInfo", + "/api/{session}/privacy/{setting}": { + "put": { + "operationId": "setPrivacySetting", "tags": [ - "Media" + "Privacy" ], - "summary": "Get media metadata", + "summary": "Update a privacy setting", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "profile:write", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "groupadd", + "last", + "status", + "profile", + "readreceipts", + "online", + "calladd", + "messages", + "defense", + "stickers" + ] + }, + "required": true, + "name": "setting", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetPrivacySettingRequest" + } + } + } + }, "responses": { "200": { - "description": "Media info", + "description": "Updated privacy settings", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetMediaInfoResponse" + "$ref": "#/components/schemas/UpdatedPrivacySettingsResponse" } } } @@ -21829,45 +24769,57 @@ } } }, - "x-required-scope": "media:read", "x-mint": { - "content": "Linked Device API media:read" + "content": "Linked Device API profile:write" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `media:read`" + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" } }, - "/api/media/{id}/download-and-save": { - "post": { - "operationId": "downloadAndSaveMedia", + "/api/{session}/labels": { + "get": { + "operationId": "listLabels", "tags": [ - "Media" + "Labels" ], - "summary": "Download media and persist it to the tenant's S3 bucket", + "summary": "List labels defined for the session", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "labels:read", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "id", + "name": "session", "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": false, + "name": "includeObservation", + "in": "query" } ], "responses": { "200": { - "description": "Persistence queued or already persisted", + "description": "Label list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuccessResponse" + "$ref": "#/components/schemas/ListLabelsResponse" } } } @@ -21923,45 +24875,52 @@ } } }, - "x-required-scope": "media:manage", "x-mint": { - "content": "Linked Device API media:manage" + "content": "Linked Device API labels:read" }, "x-protocols": [ "Linked Device API" ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `media:manage`" - } - }, - "/api/projects/{projectSlug}/templates": { - "get": { - "operationId": "listProjectTemplates", + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:read`" + }, + "post": { + "operationId": "createLabel", "tags": [ - "Templates" + "Labels" ], - "summary": "List project message templates", + "summary": "Create a label", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "labels:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "projectSlug", + "name": "session", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateLabelRequest" + } + } + } + }, "responses": { "200": { - "description": "Templates", + "description": "Label created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListProjectTemplatesResponse" + "$ref": "#/components/schemas/CreateLabelResponse" } } } @@ -22017,30 +24976,43 @@ } } }, - "x-required-scope": "templates:read", "x-mint": { - "content": "templates:read" + "content": "Linked Device API labels:manage" }, - "description": "**Required scope:** `templates:read`" - }, - "post": { - "operationId": "createProjectTemplate", + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" + } + }, + "/api/{session}/labels/{labelId}": { + "put": { + "operationId": "updateLabel", "tags": [ - "Templates" + "Labels" ], - "summary": "Create a project message template", + "summary": "Update a label", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "labels:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "projectSlug", + "name": "session", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "labelId", "in": "path" } ], @@ -22048,18 +25020,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateMessageTemplateRequest" + "$ref": "#/components/schemas/UpdateLabelRequest" } } } }, "responses": { - "201": { - "description": "Created", + "200": { + "description": "Label updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateProjectTemplateResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -22104,6 +25076,16 @@ } } }, + "409": { + "description": "The session has no authoritative view of its labels yet. Retry once a cached or project-retained snapshot is complete.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -22115,32 +25097,33 @@ } } }, - "x-required-scope": "templates:manage", "x-mint": { - "content": "templates:manage" + "content": "Linked Device API labels:manage" }, - "description": "**Required scope:** `templates:manage`" - } - }, - "/api/projects/{projectSlug}/templates/{id}": { - "get": { - "operationId": "getProjectTemplate", + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" + }, + "delete": { + "operationId": "deleteLabel", "tags": [ - "Templates" + "Labels" ], - "summary": "Get a project message template", + "summary": "Delete a label", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "labels:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "projectSlug", + "name": "session", "in": "path" }, { @@ -22148,17 +25131,17 @@ "type": "string" }, "required": true, - "name": "id", + "name": "labelId", "in": "path" } ], "responses": { "200": { - "description": "Template", + "description": "Label deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetProjectTemplateResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -22214,30 +25197,35 @@ } } }, - "x-required-scope": "templates:read", "x-mint": { - "content": "templates:read" + "content": "Linked Device API labels:manage" }, - "description": "**Required scope:** `templates:read`" - }, - "patch": { - "operationId": "updateProjectTemplate", + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" + } + }, + "/api/{session}/labels/chats/{chatId}": { + "get": { + "operationId": "getChatLabels", "tags": [ - "Templates" + "Labels" ], - "summary": "Update a project message template", + "summary": "Get labels attached to a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "labels:read", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "projectSlug", + "name": "session", "in": "path" }, { @@ -22245,26 +25233,29 @@ "type": "string" }, "required": true, - "name": "id", + "name": "chatId", "in": "path" + }, + { + "schema": { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + "required": false, + "name": "includeObservation", + "in": "query" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMessageTemplateRequest" - } - } - } - }, "responses": { "200": { - "description": "Updated", + "description": "Chat labels", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateProjectTemplateResponse" + "$ref": "#/components/schemas/GetChatLabelsResponse" } } } @@ -22320,30 +25311,33 @@ } } }, - "x-required-scope": "templates:manage", "x-mint": { - "content": "templates:manage" + "content": "Linked Device API labels:read" }, - "description": "**Required scope:** `templates:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:read`" }, - "delete": { - "operationId": "deleteProjectTemplate", + "put": { + "operationId": "setChatLabels", "tags": [ - "Templates" + "Labels" ], - "summary": "Delete a project message template", + "summary": "Replace the set of labels attached to a chat", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "labels:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "projectSlug", + "name": "session", "in": "path" }, { @@ -22351,13 +25345,22 @@ "type": "string" }, "required": true, - "name": "id", + "name": "chatId", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetChatLabelsRequest" + } + } + } + }, "responses": { "200": { - "description": "Deleted", + "description": "Chat labels updated", "content": { "application/json": { "schema": { @@ -22406,6 +25409,16 @@ } } }, + "409": { + "description": "The session has no authoritative view of its labels yet. Retry once a cached or project-retained snapshot is complete.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -22417,59 +25430,35 @@ } } }, - "x-required-scope": "templates:manage", "x-mint": { - "content": "templates:manage" + "content": "Linked Device API labels:manage" }, - "description": "**Required scope:** `templates:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `labels:manage`" } }, - "/api/projects/{projectSlug}/templates/{id}/preview": { - "post": { - "operationId": "previewProjectTemplate", + "/api/webhooks": { + "get": { + "operationId": "listWebhooks", "tags": [ - "Templates" + "Webhooks" ], - "summary": "Render a template preview (idealized) or surface-specific output", + "summary": "List webhooks for the tenant", "security": [ { "BearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "projectSlug", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PreviewMessageTemplateRequest" - } - } - } - }, + "x-required-scope": "webhooks:manage", "responses": { "200": { - "description": "Rendered", + "description": "Webhook list", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PreviewProjectTemplateResponse" + "$ref": "#/components/schemas/ListWebhooksResponse" } } } @@ -22525,60 +25514,39 @@ } } }, - "x-required-scope": "templates:read", "x-mint": { - "content": "templates:read" + "content": "webhooks:manage" }, - "description": "**Required scope:** `templates:read`" - } - }, - "/api/projects/{projectSlug}/templates/{id}/submit": { + "description": "**Required scope:** `webhooks:manage`" + }, "post": { - "operationId": "submitProjectTemplateToMeta", + "operationId": "createWebhook", "tags": [ - "Templates" + "Webhooks" ], - "summary": "Submit a project template to Meta for approval on a Cloud-API session", - "description": "**Required scope:** `templates:manage`\n\nRenders the canonical template into a Meta create-template payload and submits it through the given cloud_api session. Records a per-session approval pointer (cloudLinks) and returns the updated template. The Meta review verdict arrives later via webhook.", + "summary": "Register a new webhook", "security": [ { "BearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "projectSlug", - "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" - } - ], + "x-required-scope": "webhooks:manage", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubmitMessageTemplateRequest" + "$ref": "#/components/schemas/CreateWebhookRequest" } } } }, "responses": { - "200": { - "description": "Submitted", + "201": { + "description": "Webhook created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SubmitProjectTemplateResponse" + "$ref": "#/components/schemas/CreateWebhookResponse" } } } @@ -22634,41 +25602,42 @@ } } }, - "x-required-scope": "templates:manage", "x-mint": { - "content": "templates:manage" - } + "content": "webhooks:manage" + }, + "description": "**Required scope:** `webhooks:manage`" } }, - "/api/projects/{projectSlug}/campaigns": { + "/api/webhooks/{id}": { "get": { - "operationId": "listCampaigns", + "operationId": "getWebhook", "tags": [ - "Campaigns" + "Webhooks" ], - "summary": "List campaigns", + "summary": "Get a webhook by id", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "webhooks:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "projectSlug", + "name": "id", "in": "path" } ], "responses": { "200": { - "description": "OK", + "description": "Webhook details", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListCampaignsResponse" + "$ref": "#/components/schemas/GetWebhookResponse" } } } @@ -22724,30 +25693,30 @@ } } }, - "x-required-scope": "campaigns:read", "x-mint": { - "content": "campaigns:read" + "content": "webhooks:manage" }, - "description": "**Required scope:** `campaigns:read`" + "description": "**Required scope:** `webhooks:manage`" }, - "post": { - "operationId": "createCampaign", + "put": { + "operationId": "updateWebhook", "tags": [ - "Campaigns" + "Webhooks" ], - "summary": "Create a campaign", + "summary": "Update a webhook", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "webhooks:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "projectSlug", + "name": "id", "in": "path" } ], @@ -22755,18 +25724,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateCampaignRequest" + "$ref": "#/components/schemas/UpdateWebhookRequest" } } } }, "responses": { "200": { - "description": "OK", + "description": "Webhook updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateCampaignResponse" + "$ref": "#/components/schemas/UpdateWebhookResponse" } } } @@ -22791,16 +25760,6 @@ } } }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -22832,34 +25791,24 @@ } } }, - "x-required-scope": "campaigns:manage", "x-mint": { - "content": "campaigns:manage" + "content": "webhooks:manage" }, - "description": "**Required scope:** `campaigns:manage`" - } - }, - "/api/projects/{projectSlug}/campaigns/{id}": { - "get": { - "operationId": "getCampaign", + "description": "**Required scope:** `webhooks:manage`" + }, + "delete": { + "operationId": "deleteWebhook", "tags": [ - "Campaigns" + "Webhooks" ], - "summary": "Get a campaign", + "summary": "Delete a webhook", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "webhooks:manage", "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "projectSlug", - "in": "path" - }, { "schema": { "type": "string" @@ -22871,11 +25820,11 @@ ], "responses": { "200": { - "description": "OK", + "description": "Webhook deleted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetCampaignResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -22900,16 +25849,6 @@ } } }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -22941,34 +25880,26 @@ } } }, - "x-required-scope": "campaigns:read", "x-mint": { - "content": "campaigns:read" + "content": "webhooks:manage" }, - "description": "**Required scope:** `campaigns:read`" + "description": "**Required scope:** `webhooks:manage`" } }, - "/api/projects/{projectSlug}/campaigns/{id}/analytics": { + "/api/media/{id}": { "get": { - "operationId": "getCampaignAnalytics", + "operationId": "downloadMedia", "tags": [ - "Campaigns" + "Media" ], - "summary": "Campaign analytics (funnel, response timing, retries)", + "summary": "Download media (binary stream or redirect to S3)", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "media:read", "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "projectSlug", - "in": "path" - }, { "schema": { "type": "string" @@ -22980,14 +25911,10 @@ ], "responses": { "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CampaignAnalyticsResponse" - } - } - } + "description": "Media binary stream" + }, + "302": { + "description": "Redirect to S3" }, "400": { "description": "Bad request — request body or params failed validation", @@ -22998,19 +25925,9 @@ } } } - }, - "401": { - "description": "Missing or invalid authentication token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", + }, + "401": { + "description": "Missing or invalid authentication token", "content": { "application/json": { "schema": { @@ -23050,34 +25967,29 @@ } } }, - "x-required-scope": "campaigns:read", "x-mint": { - "content": "campaigns:read" + "content": "Linked Device API media:read" }, - "description": "**Required scope:** `campaigns:read`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `media:read`" } }, - "/api/projects/{projectSlug}/campaigns/{id}/launch": { - "post": { - "operationId": "launchCampaign", + "/api/media/{id}/info": { + "get": { + "operationId": "getMediaInfo", "tags": [ - "Campaigns" + "Media" ], - "summary": "Launch a campaign", + "summary": "Get media metadata", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "media:read", "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "projectSlug", - "in": "path" - }, { "schema": { "type": "string" @@ -23087,22 +25999,13 @@ "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LaunchCampaignRequest" - } - } - } - }, "responses": { "200": { - "description": "OK", + "description": "Media info", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LaunchCampaignResponse" + "$ref": "#/components/schemas/GetMediaInfoResponse" } } } @@ -23127,16 +26030,6 @@ } } }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -23168,34 +26061,29 @@ } } }, - "x-required-scope": "campaigns:manage", "x-mint": { - "content": "campaigns:manage" + "content": "Linked Device API media:read" }, - "description": "**Required scope:** `campaigns:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `media:read`" } }, - "/api/projects/{projectSlug}/campaigns/{id}/pause": { + "/api/media/{id}/download-and-save": { "post": { - "operationId": "pauseCampaign", + "operationId": "downloadAndSaveMedia", "tags": [ - "Campaigns" + "Media" ], - "summary": "Pause a campaign", + "summary": "Download media and persist it to the tenant's S3 bucket", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "media:manage", "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "projectSlug", - "in": "path" - }, { "schema": { "type": "string" @@ -23207,11 +26095,11 @@ ], "responses": { "200": { - "description": "OK", + "description": "Persistence queued or already persisted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PauseCampaignResponse" + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -23236,16 +26124,6 @@ } } }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -23277,25 +26155,28 @@ } } }, - "x-required-scope": "campaigns:manage", "x-mint": { - "content": "campaigns:manage" + "content": "Linked Device API media:manage" }, - "description": "**Required scope:** `campaigns:manage`" + "x-protocols": [ + "Linked Device API" + ], + "description": "**Protocols:** Linked Device API\n\n**Required scope:** `media:manage`" } }, - "/api/projects/{projectSlug}/campaigns/{id}/resume": { - "post": { - "operationId": "resumeCampaign", + "/api/projects/{projectSlug}/templates": { + "get": { + "operationId": "listProjectTemplates", "tags": [ - "Campaigns" + "Templates" ], - "summary": "Resume a campaign", + "summary": "List project message templates", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "templates:read", "parameters": [ { "schema": { @@ -23304,23 +26185,15 @@ "required": true, "name": "projectSlug", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" } ], "responses": { "200": { - "description": "OK", + "description": "Templates", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResumeCampaignResponse" + "$ref": "#/components/schemas/ListProjectTemplatesResponse" } } } @@ -23345,16 +26218,6 @@ } } }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -23386,25 +26249,23 @@ } } }, - "x-required-scope": "campaigns:manage", "x-mint": { - "content": "campaigns:manage" + "content": "templates:read" }, - "description": "**Required scope:** `campaigns:manage`" - } - }, - "/api/projects/{projectSlug}/campaigns/{id}/stop": { + "description": "**Required scope:** `templates:read`" + }, "post": { - "operationId": "stopCampaign", + "operationId": "createProjectTemplate", "tags": [ - "Campaigns" + "Templates" ], - "summary": "Stop a campaign", + "summary": "Create a project message template", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "templates:manage", "parameters": [ { "schema": { @@ -23413,23 +26274,24 @@ "required": true, "name": "projectSlug", "in": "path" - }, - { - "schema": { - "type": "string" - }, - "required": true, - "name": "id", - "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateMessageTemplateRequest" + } + } + } + }, "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StopCampaignResponse" + "$ref": "#/components/schemas/CreateProjectTemplateResponse" } } } @@ -23454,16 +26316,6 @@ } } }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -23495,25 +26347,25 @@ } } }, - "x-required-scope": "campaigns:manage", "x-mint": { - "content": "campaigns:manage" + "content": "templates:manage" }, - "description": "**Required scope:** `campaigns:manage`" + "description": "**Required scope:** `templates:manage`" } }, - "/api/projects/{projectSlug}/campaigns/{id}/requeue": { - "post": { - "operationId": "requeueCampaign", + "/api/projects/{projectSlug}/templates/{id}": { + "get": { + "operationId": "getProjectTemplate", "tags": [ - "Campaigns" + "Templates" ], - "summary": "Re-queue failed recipients", + "summary": "Get a project message template", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "templates:read", "parameters": [ { "schema": { @@ -23532,22 +26384,13 @@ "in": "path" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RequeueCampaignRequest" - } - } - } - }, "responses": { "200": { - "description": "OK", + "description": "Template", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RequeueCampaignResponse" + "$ref": "#/components/schemas/GetProjectTemplateResponse" } } } @@ -23572,16 +26415,6 @@ } } }, - "402": { - "description": "Payment required — feature requires an upgraded plan/number tier", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -23613,41 +26446,57 @@ } } }, - "x-required-scope": "campaigns:manage", "x-mint": { - "content": "campaigns:manage" + "content": "templates:read" }, - "description": "**Required scope:** `campaigns:manage`" - } - }, - "/api/widget/sessions": { - "post": { - "operationId": "createWidgetSession", + "description": "**Required scope:** `templates:read`" + }, + "patch": { + "operationId": "updateProjectTemplate", "tags": [ - "Widget" + "Templates" ], - "summary": "Create a widget session for embedding the connect flow", + "summary": "Update a project message template", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "templates:manage", + "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", + "in": "path" + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateWidgetSessionRequest" + "$ref": "#/components/schemas/UpdateMessageTemplateRequest" } } } }, "responses": { - "201": { - "description": "Widget session created", + "200": { + "description": "Updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WidgetSessionResponseWrapper" + "$ref": "#/components/schemas/UpdateProjectTemplateResponse" } } } @@ -23703,26 +26552,32 @@ } } }, - "x-required-scope": "widget:create", "x-mint": { - "content": "widget:create" + "content": "templates:manage" }, - "description": "**Required scope:** `widget:create`" - } - }, - "/api/widget/sessions/{id}": { + "description": "**Required scope:** `templates:manage`" + }, "delete": { - "operationId": "cancelWidgetSession", + "operationId": "deleteProjectTemplate", "tags": [ - "Widget" + "Templates" ], - "summary": "Cancel a pending widget session and revoke its CST", + "summary": "Delete a project message template", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "templates:manage", "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, { "schema": { "type": "string" @@ -23734,26 +26589,11 @@ ], "responses": { "200": { - "description": "Cancelled", + "description": "Deleted", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "message": { - "type": "string" - } - }, - "required": [ - "success", - "message" - ] + "$ref": "#/components/schemas/SuccessResponse" } } } @@ -23809,26 +26649,34 @@ } } }, - "x-required-scope": "widget:create", "x-mint": { - "content": "widget:create" + "content": "templates:manage" }, - "description": "**Required scope:** `widget:create`" + "description": "**Required scope:** `templates:manage`" } }, - "/api/widget/sessions/{id}/handoff": { + "/api/projects/{projectSlug}/templates/{id}/preview": { "post": { - "operationId": "createWidgetHandoff", + "operationId": "previewProjectTemplate", "tags": [ - "Widget" + "Templates" ], - "summary": "Mint a TV-style handoff token + meta-QR so the user can finish the connect flow on another device.", + "summary": "Render a template preview (idealized) or surface-specific output", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "templates:read", "parameters": [ + { + "schema": { + "type": "string" + }, + "required": true, + "name": "projectSlug", + "in": "path" + }, { "schema": { "type": "string" @@ -23838,28 +26686,22 @@ "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PreviewMessageTemplateRequest" + } + } + } + }, "responses": { - "201": { - "description": "Handoff minted", + "200": { + "description": "Rendered", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/WidgetHandoffResponse" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/PreviewProjectTemplateResponse" } } } @@ -23915,53 +26757,60 @@ } } }, - "x-required-scope": "widget:create", "x-mint": { - "content": "widget:create" + "content": "templates:read" }, - "description": "**Required scope:** `widget:create`" + "description": "**Required scope:** `templates:read`" } }, - "/widget/handoff/{token}/state": { - "get": { - "operationId": "getWidgetHandoffState", + "/api/projects/{projectSlug}/templates/{id}/submit": { + "post": { + "operationId": "submitProjectTemplateToMeta", "tags": [ - "Widget" + "Templates" ], - "security": [], - "summary": "Resolve a handoff token to the current QR + session status. Public — the token in the URL is the bearer.", + "summary": "Submit a project template to Meta for approval on a Cloud-API session", + "description": "**Required scope:** `templates:manage`\n\nRenders the canonical template into a Meta create-template payload and submits it through the given cloud_api session. Records a per-session approval pointer (cloudLinks) and returns the updated template. The Meta review verdict arrives later via webhook.", + "security": [ + { + "BearerAuth": [] + } + ], + "x-required-scope": "templates:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "token", + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", "in": "path" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitMessageTemplateRequest" + } + } + } + }, "responses": { "200": { - "description": "Current QR + status", + "description": "Submitted", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "enum": [ - true - ] - }, - "data": { - "$ref": "#/components/schemas/WidgetHandoffState" - } - }, - "required": [ - "success", - "data" - ] + "$ref": "#/components/schemas/SubmitProjectTemplateResponse" } } } @@ -24016,48 +26865,42 @@ } } } + }, + "x-mint": { + "content": "templates:manage" } } }, - "/api/{session}/business/products": { - "post": { - "operationId": "createBusinessProduct", + "/api/projects/{projectSlug}/campaigns": { + "get": { + "operationId": "listCampaigns", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Create a product in the connected WhatsApp Business App catalog", + "summary": "List campaigns", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:read", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessProductMutationRequest" - } - } - } - }, "responses": { "200": { - "description": "Product created", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBusinessProductResponse" + "$ref": "#/components/schemas/ListCampaignsResponse" } } } @@ -24113,55 +26956,49 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:read" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - } - }, - "/api/{session}/business/collections/reorder": { + "description": "**Required scope:** `campaigns:read`" + }, "post": { - "operationId": "reorderBusinessCollections", + "operationId": "createCampaign", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Reorder connected WhatsApp Business App catalog collections", + "summary": "Create a campaign", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", "in": "path" } ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessCollectionReorderRequest" + "$ref": "#/components/schemas/CreateCampaignRequest" } } } }, "responses": { "200": { - "description": "Collections reordered", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReorderBusinessCollectionsResponse" + "$ref": "#/components/schemas/CreateCampaignResponse" } } } @@ -24186,6 +27023,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24217,55 +27064,50 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Required scope:** `campaigns:manage`" } }, - "/api/{session}/business/catalog/cart": { - "patch": { - "operationId": "setBusinessCartEnabled", + "/api/projects/{projectSlug}/campaigns/{id}": { + "get": { + "operationId": "getCampaign", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Enable or disable the connected WhatsApp Business App catalog cart", + "summary": "Get a campaign", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:read", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessCartSettingRequest" - } - } - } - }, "responses": { "200": { - "description": "Cart setting updated", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetBusinessCartEnabledResponse" + "$ref": "#/components/schemas/GetCampaignResponse" } } } @@ -24290,6 +27132,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24321,65 +27173,50 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:read" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Required scope:** `campaigns:read`" } }, - "/api/{session}/business/products/{productId}/visibility": { - "patch": { - "operationId": "setBusinessProductVisibility", + "/api/projects/{projectSlug}/campaigns/{id}/analytics": { + "get": { + "operationId": "getCampaignAnalytics", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Hide or show a product in the connected WhatsApp Business App catalog", + "summary": "Campaign analytics (funnel, response timing, retries)", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:read", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", "in": "path" }, { "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256 + "type": "string" }, "required": true, - "name": "productId", + "name": "id", "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessProductVisibilityRequest" - } - } - } - }, "responses": { "200": { - "description": "Product visibility updated", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetBusinessProductVisibilityResponse" + "$ref": "#/components/schemas/CampaignAnalyticsResponse" } } } @@ -24404,6 +27241,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24435,65 +27282,59 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:read" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Required scope:** `campaigns:read`" } }, - "/api/{session}/business/products/{productId}/appeal": { + "/api/projects/{projectSlug}/campaigns/{id}/launch": { "post": { - "operationId": "appealBusinessProduct", + "operationId": "launchCampaign", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Request a review of a rejected WhatsApp Business App catalog product", + "summary": "Launch a campaign", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", "in": "path" }, { "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256 + "type": "string" }, "required": true, - "name": "productId", + "name": "id", "in": "path" } ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessCatalogAppealRequest" + "$ref": "#/components/schemas/LaunchCampaignRequest" } } } }, "responses": { "200": { - "description": "Product appeal submitted", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AppealBusinessProductResponse" + "$ref": "#/components/schemas/LaunchCampaignResponse" } } } @@ -24518,6 +27359,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24549,65 +27400,50 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Required scope:** `campaigns:manage`" } }, - "/api/{session}/business/collections/{collectionId}/appeal": { + "/api/projects/{projectSlug}/campaigns/{id}/pause": { "post": { - "operationId": "appealBusinessCollection", + "operationId": "pauseCampaign", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Request a review of a rejected WhatsApp Business App catalog collection", + "summary": "Pause a campaign", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", "in": "path" }, { "schema": { - "type": "string", - "minLength": 1, - "maxLength": 256 + "type": "string" }, "required": true, - "name": "collectionId", + "name": "id", "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessCatalogAppealRequest" - } - } - } - }, "responses": { "200": { - "description": "Collection appeal submitted", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AppealBusinessCollectionResponse" + "$ref": "#/components/schemas/PauseCampaignResponse" } } } @@ -24632,6 +27468,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24663,45 +27509,50 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Required scope:** `campaigns:manage`" } }, - "/api/{session}/business/compliance": { - "get": { - "operationId": "getBusinessMerchantCompliance", + "/api/projects/{projectSlug}/campaigns/{id}/resume": { + "post": { + "operationId": "resumeCampaign", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Read merchant compliance details for the connected WhatsApp Business App account", + "summary": "Resume a campaign", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", "in": "path" } ], "responses": { "200": { - "description": "Live merchant compliance details", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBusinessMerchantComplianceResponse" + "$ref": "#/components/schemas/ResumeCampaignResponse" } } } @@ -24726,6 +27577,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24757,53 +27618,50 @@ } } }, - "x-required-scope": "profile:read", "x-mint": { - "content": "Linked Device API profile:read" + "content": "campaigns:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" - }, - "put": { - "operationId": "setBusinessMerchantCompliance", + "description": "**Required scope:** `campaigns:manage`" + } + }, + "/api/projects/{projectSlug}/campaigns/{id}/stop": { + "post": { + "operationId": "stopCampaign", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Replace merchant compliance details for the connected WhatsApp Business App account", + "summary": "Stop a campaign", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessMerchantCompliance" - } - } - } - }, "responses": { "200": { - "description": "Merchant compliance details updated", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetBusinessMerchantComplianceResponse" + "$ref": "#/components/schemas/StopCampaignResponse" } } } @@ -24828,6 +27686,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24859,55 +27727,59 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Required scope:** `campaigns:manage`" } }, - "/api/{session}/business/quick-replies": { + "/api/projects/{projectSlug}/campaigns/{id}/requeue": { "post": { - "operationId": "createBusinessQuickReply", + "operationId": "requeueCampaign", "tags": [ - "Business App" + "Campaigns" ], - "summary": "Create a WhatsApp Business App quick reply", + "summary": "Re-queue failed recipients", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "campaigns:manage", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "projectSlug", + "in": "path" + }, + { + "schema": { + "type": "string" + }, + "required": true, + "name": "id", "in": "path" } ], "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessQuickReplyMutation" + "$ref": "#/components/schemas/RequeueCampaignRequest" } } } }, "responses": { "200": { - "description": "Quick reply created", + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateBusinessQuickReplyResponse" + "$ref": "#/components/schemas/RequeueCampaignResponse" } } } @@ -24932,6 +27804,16 @@ } } }, + "402": { + "description": "Payment required — feature requires an upgraded plan/number tier", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "403": { "description": "Authenticated but not permitted (missing scope or rule violation)", "content": { @@ -24963,43 +27845,41 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "campaigns:manage" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - }, - "get": { - "operationId": "listBusinessQuickReplies", + "description": "**Required scope:** `campaigns:manage`" + } + }, + "/api/widget/sessions": { + "post": { + "operationId": "createWidgetSession", "tags": [ - "Business App" + "Widget" ], - "summary": "List remembered WhatsApp Business App quick replies", + "summary": "Create a widget session for embedding the connect flow", "security": [ { "BearerAuth": [] } ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" + "x-required-scope": "widget:create", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWidgetSessionRequest" + } + } } - ], + }, "responses": { - "200": { - "description": "Best-effort quick reply list", + "201": { + "description": "Widget session created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListBusinessQuickRepliesResponse" + "$ref": "#/components/schemas/WidgetSessionResponseWrapper" } } } @@ -25055,66 +27935,57 @@ } } }, - "x-required-scope": "profile:read", "x-mint": { - "content": "Linked Device API profile:read" + "content": "widget:create" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:read`" + "description": "**Required scope:** `widget:create`" } }, - "/api/{session}/business/quick-replies/{quickReplyId}": { - "put": { - "operationId": "setBusinessQuickReply", + "/api/widget/sessions/{id}": { + "delete": { + "operationId": "cancelWidgetSession", "tags": [ - "Business App" + "Widget" ], - "summary": "Replace a WhatsApp Business App quick reply", + "summary": "Cancel a pending widget session and revoke its CST", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "widget:create", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 20, - "pattern": "^[0-9]+$" - }, - "required": true, - "name": "quickReplyId", + "name": "id", "in": "path" } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BusinessQuickReplyMutation" - } - } - } - }, "responses": { "200": { - "description": "Quick reply replaced", + "description": "Cancelled", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SetBusinessQuickReplyResponse" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "success", + "message" + ] } } } @@ -25170,54 +28041,57 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "widget:create" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" - }, - "delete": { - "operationId": "deleteBusinessQuickReply", + "description": "**Required scope:** `widget:create`" + } + }, + "/api/widget/sessions/{id}/handoff": { + "post": { + "operationId": "createWidgetHandoff", "tags": [ - "Business App" + "Widget" ], - "summary": "Delete a WhatsApp Business App quick reply", + "summary": "Mint a TV-style handoff token + meta-QR so the user can finish the connect flow on another device.", "security": [ { "BearerAuth": [] } ], + "x-required-scope": "widget:create", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 20, - "pattern": "^[0-9]+$" - }, - "required": true, - "name": "quickReplyId", + "name": "id", "in": "path" } ], "responses": { - "200": { - "description": "Quick reply deleted", + "201": { + "description": "Handoff minted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteBusinessQuickReplyResponse" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/WidgetHandoffResponse" + } + }, + "required": [ + "success", + "data" + ] } } } @@ -25273,45 +28147,52 @@ } } }, - "x-required-scope": "profile:write", "x-mint": { - "content": "Linked Device API profile:write" + "content": "widget:create" }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `profile:write`" + "description": "**Required scope:** `widget:create`" } }, - "/api/{session}/business/linked-accounts": { + "/widget/handoff/{token}/state": { "get": { - "operationId": "getBusinessLinkedAccounts", + "operationId": "getWidgetHandoffState", "tags": [ - "Business App" - ], - "summary": "Read live accounts linked to the connected WhatsApp Business App", - "security": [ - { - "BearerAuth": [] - } + "Widget" ], + "security": [], + "summary": "Resolve a handoff token to the current QR + session status. Public — the token in the URL is the bearer.", "parameters": [ { "schema": { "type": "string" }, "required": true, - "name": "session", + "name": "token", "in": "path" } ], "responses": { "200": { - "description": "Live linked-account state", + "description": "Current QR + status", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetBusinessLinkedAccountsResponse" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "enum": [ + true + ] + }, + "data": { + "$ref": "#/components/schemas/WidgetHandoffState" + } + }, + "required": [ + "success", + "data" + ] } } } @@ -25366,220 +28247,423 @@ } } } + } + } + } + }, + "x-webhooks": { + "message.received": { + "post": { + "operationId": "onMessageReceived", + "summary": "Incoming message", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageReceivedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.sent": { + "post": { + "operationId": "onMessageSent", + "summary": "Outgoing message successfully sent", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageSentEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.ack": { + "post": { + "operationId": "onMessageAck", + "summary": "Delivery acknowledgement", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageAckEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.revoked": { + "post": { + "operationId": "onMessageRevoked", + "summary": "Message revoked", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageRevokedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.reaction": { + "post": { + "operationId": "onMessageReaction", + "summary": "Reaction added or removed", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageReactionEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.edited": { + "post": { + "operationId": "onMessageEdited", + "summary": "Message edited", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageEditedEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.update": { + "post": { + "operationId": "onMessageUpdate", + "summary": "Generic message update", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageUpdateEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.delete": { + "post": { + "operationId": "onMessageDelete", + "summary": "Message deleted from another device", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageDeleteEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "message.vote": { + "post": { + "operationId": "onMessageVote", + "summary": "Poll vote cast", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageVoteEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "session.status": { + "post": { + "operationId": "onSessionStatus", + "summary": "Session status change", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionStatusEvent" + } + } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "session.qr": { + "post": { + "operationId": "onSessionQr", + "summary": "QR code issued for device linking", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionQREvent" + } + } + } }, - "x-required-scope": "business:read", - "x-mint": { - "content": "Linked Device API business:read" - }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } } }, - "/api/{session}/business/eligibility": { - "get": { - "operationId": "getBusinessEligibility", + "session.connected": { + "post": { + "operationId": "onSessionConnected", + "summary": "Session connected to WhatsApp", "tags": [ - "Business App" - ], - "summary": "Read live feature eligibility for the connected WhatsApp Business App", - "security": [ - { - "BearerAuth": [] - } + "Webhooks" ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionConnectedEvent" + } + } } - ], + }, "responses": { "200": { - "description": "Live business feature eligibility", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetBusinessEligibilityResponse" - } - } - } - }, - "400": { - "description": "Bad request — request body or params failed validation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "401": { - "description": "Missing or invalid authentication token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Authenticated but not permitted (missing scope or rule violation)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "session.logged_out": { + "post": { + "operationId": "onSessionLoggedOut", + "summary": "Session logged out", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionLoggedOutEvent" } } } }, - "x-required-scope": "business:read", - "x-mint": { - "content": "Linked Device API business:read" - }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `business:read`" + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } } }, - "/api/{session}/users/{id}/security-code": { - "get": { - "operationId": "getUserSecurityCode", + "session.phone_offline": { + "post": { + "operationId": "onSessionPhoneOffline", + "summary": "Primary phone has been offline for an extended period", "tags": [ - "Users" - ], - "summary": "Get an identity verification code for a user", - "security": [ - { - "BearerAuth": [] - } + "Webhooks" ], - "parameters": [ - { - "schema": { - "type": "string" - }, - "required": true, - "name": "session", - "in": "path" - }, - { - "schema": { - "type": "string", - "pattern": "^\\d+@lid$" - }, - "required": true, - "name": "id", - "in": "path" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionPhoneOfflineEvent" + } + } } - ], + }, "responses": { "200": { - "description": "Identity verification code and display QR", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserSecurityCodeResponse" - } - } - } - }, - "400": { - "description": "Bad request — request body or params failed validation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "401": { - "description": "Missing or invalid authentication token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "403": { - "description": "Authenticated but not permitted (missing scope or rule violation)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "group.update": { + "post": { + "operationId": "onGroupUpdate", + "summary": "Group metadata changed", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupUpdateEvent" } } - }, - "404": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "group.participant": { + "post": { + "operationId": "onGroupParticipant", + "summary": "Group participants added/removed/promoted/demoted", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupParticipantEvent" } } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } + } + }, + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } + } + }, + "presence.update": { + "post": { + "operationId": "onPresenceUpdate", + "summary": "User or chat presence change", + "tags": [ + "Webhooks" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PresenceUpdateEvent" } } } }, - "x-required-scope": "contacts:read", - "x-mint": { - "content": "Linked Device API contacts:read" - }, - "x-protocols": [ - "Linked Device API" - ], - "description": "**Protocols:** Linked Device API\n\n**Required scope:** `contacts:read`" + "responses": { + "200": { + "description": "Return any 2xx status to acknowledge receipt. Non-2xx responses are retried according to the retry policy." + } + } } - } - }, - "x-webhooks": { - "message.received": { + }, + "contact.update": { "post": { - "operationId": "onMessageReceived", - "summary": "Incoming message", + "operationId": "onContactUpdate", + "summary": "Contact info changed", "tags": [ "Webhooks" ], @@ -25588,7 +28672,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageReceivedEvent" + "$ref": "#/components/schemas/ContactUpdateEvent" } } } @@ -25600,10 +28684,10 @@ } } }, - "message.sent": { + "chat.archive": { "post": { - "operationId": "onMessageSent", - "summary": "Outgoing message successfully sent", + "operationId": "onChatArchive", + "summary": "Chat archived or pinned", "tags": [ "Webhooks" ], @@ -25612,7 +28696,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageSentEvent" + "$ref": "#/components/schemas/ChatArchiveEvent" } } } @@ -25624,10 +28708,10 @@ } } }, - "message.ack": { + "chat.mute": { "post": { - "operationId": "onMessageAck", - "summary": "Delivery acknowledgement", + "operationId": "onChatMute", + "summary": "Chat muted or unmuted", "tags": [ "Webhooks" ], @@ -25636,7 +28720,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageAckEvent" + "$ref": "#/components/schemas/ChatMuteEvent" } } } @@ -25648,10 +28732,10 @@ } } }, - "message.revoked": { + "chat.read": { "post": { - "operationId": "onMessageRevoked", - "summary": "Message revoked", + "operationId": "onChatRead", + "summary": "Chat marked read or unread", "tags": [ "Webhooks" ], @@ -25660,7 +28744,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageRevokedEvent" + "$ref": "#/components/schemas/ChatReadEvent" } } } @@ -25672,10 +28756,10 @@ } } }, - "message.reaction": { + "chat.clear": { "post": { - "operationId": "onMessageReaction", - "summary": "Reaction added or removed", + "operationId": "onChatClear", + "summary": "Chat cleared", "tags": [ "Webhooks" ], @@ -25684,7 +28768,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageReactionEvent" + "$ref": "#/components/schemas/ChatClearEvent" } } } @@ -25696,10 +28780,10 @@ } } }, - "message.edited": { + "chat.delete": { "post": { - "operationId": "onMessageEdited", - "summary": "Message edited", + "operationId": "onChatDelete", + "summary": "Chat deleted", "tags": [ "Webhooks" ], @@ -25708,7 +28792,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageEditedEvent" + "$ref": "#/components/schemas/ChatDeleteEvent" } } } @@ -25720,10 +28804,10 @@ } } }, - "message.update": { + "call.received": { "post": { - "operationId": "onMessageUpdate", - "summary": "Generic message update", + "operationId": "onCallReceived", + "summary": "Incoming call", "tags": [ "Webhooks" ], @@ -25732,7 +28816,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageUpdateEvent" + "$ref": "#/components/schemas/CallReceivedEvent" } } } @@ -25744,10 +28828,10 @@ } } }, - "message.delete": { + "call.missed": { "post": { - "operationId": "onMessageDelete", - "summary": "Message deleted from another device", + "operationId": "onCallMissed", + "summary": "Call missed", "tags": [ "Webhooks" ], @@ -25756,7 +28840,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageDeleteEvent" + "$ref": "#/components/schemas/CallMissedEvent" } } } @@ -25768,10 +28852,10 @@ } } }, - "message.vote": { + "call.accepted": { "post": { - "operationId": "onMessageVote", - "summary": "Poll vote cast", + "operationId": "onCallAccepted", + "summary": "Call accepted", "tags": [ "Webhooks" ], @@ -25780,7 +28864,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageVoteEvent" + "$ref": "#/components/schemas/CallAcceptedEvent" } } } @@ -25792,10 +28876,10 @@ } } }, - "session.status": { + "call.rejected": { "post": { - "operationId": "onSessionStatus", - "summary": "Session status change", + "operationId": "onCallRejected", + "summary": "Call rejected", "tags": [ "Webhooks" ], @@ -25804,7 +28888,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionStatusEvent" + "$ref": "#/components/schemas/CallRejectedEvent" } } } @@ -25816,10 +28900,10 @@ } } }, - "session.qr": { + "call.ended": { "post": { - "operationId": "onSessionQr", - "summary": "QR code issued for device linking", + "operationId": "onCallEnded", + "summary": "Call ended", "tags": [ "Webhooks" ], @@ -25828,7 +28912,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionQREvent" + "$ref": "#/components/schemas/CallEndedEvent" } } } @@ -25840,10 +28924,10 @@ } } }, - "session.connected": { + "call.telemetry": { "post": { - "operationId": "onSessionConnected", - "summary": "Session connected to WhatsApp", + "operationId": "onCallTelemetry", + "summary": "Call telemetry", "tags": [ "Webhooks" ], @@ -25852,7 +28936,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionConnectedEvent" + "$ref": "#/components/schemas/CallTelemetryEvent" } } } @@ -25864,10 +28948,10 @@ } } }, - "session.logged_out": { + "newsletter.update": { "post": { - "operationId": "onSessionLoggedOut", - "summary": "Session logged out", + "operationId": "onNewsletterUpdate", + "summary": "Newsletter subscription change", "tags": [ "Webhooks" ], @@ -25876,7 +28960,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionLoggedOutEvent" + "$ref": "#/components/schemas/NewsletterUpdateEvent" } } } @@ -25888,10 +28972,10 @@ } } }, - "session.phone_offline": { + "blocklist.update": { "post": { - "operationId": "onSessionPhoneOffline", - "summary": "Primary phone has been offline for an extended period", + "operationId": "onBlocklistUpdate", + "summary": "Blocklist changed", "tags": [ "Webhooks" ], @@ -25900,7 +28984,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionPhoneOfflineEvent" + "$ref": "#/components/schemas/BlocklistUpdateEvent" } } } @@ -25912,10 +28996,10 @@ } } }, - "group.update": { + "labels.update": { "post": { - "operationId": "onGroupUpdate", - "summary": "Group metadata changed", + "operationId": "onLabelsUpdate", + "summary": "Label edited / associated / star toggled", "tags": [ "Webhooks" ], @@ -25924,7 +29008,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GroupUpdateEvent" + "$ref": "#/components/schemas/LabelsUpdateEvent" } } } @@ -25936,10 +29020,10 @@ } } }, - "group.participant": { + "business.quick_reply.update": { "post": { - "operationId": "onGroupParticipant", - "summary": "Group participants added/removed/promoted/demoted", + "operationId": "onBusinessQuickReplyUpdate", + "summary": "WhatsApp Business App quick reply changed", "tags": [ "Webhooks" ], @@ -25948,7 +29032,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GroupParticipantEvent" + "$ref": "#/components/schemas/BusinessQuickReplyUpdateEvent" } } } @@ -25960,10 +29044,10 @@ } } }, - "presence.update": { + "history.sync": { "post": { - "operationId": "onPresenceUpdate", - "summary": "User or chat presence change", + "operationId": "onHistorySync", + "summary": "History sync chunk", "tags": [ "Webhooks" ], @@ -25972,7 +29056,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PresenceUpdateEvent" + "$ref": "#/components/schemas/HistorySyncEvent" } } } @@ -25984,10 +29068,10 @@ } } }, - "contact.update": { + "command.result": { "post": { - "operationId": "onContactUpdate", - "summary": "Contact info changed", + "operationId": "onCommandResult", + "summary": "Async command completed", "tags": [ "Webhooks" ], @@ -25996,7 +29080,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ContactUpdateEvent" + "$ref": "#/components/schemas/CommandResultEvent" } } } @@ -26008,10 +29092,10 @@ } } }, - "chat.archive": { + "customer.enabled": { "post": { - "operationId": "onChatArchive", - "summary": "Chat archived or pinned", + "operationId": "onCustomerEnabled", + "summary": "Customers enabled for a project", "tags": [ "Webhooks" ], @@ -26020,7 +29104,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChatArchiveEvent" + "$ref": "#/components/schemas/CustomerEnabledEvent" } } } @@ -26032,10 +29116,10 @@ } } }, - "chat.mute": { + "customer.created": { "post": { - "operationId": "onChatMute", - "summary": "Chat muted or unmuted", + "operationId": "onCustomerCreated", + "summary": "Customer created", "tags": [ "Webhooks" ], @@ -26044,7 +29128,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChatMuteEvent" + "$ref": "#/components/schemas/CustomerCreatedEvent" } } } @@ -26056,10 +29140,10 @@ } } }, - "chat.read": { + "customer.updated": { "post": { - "operationId": "onChatRead", - "summary": "Chat marked read or unread", + "operationId": "onCustomerUpdated", + "summary": "Customer updated", "tags": [ "Webhooks" ], @@ -26068,7 +29152,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChatReadEvent" + "$ref": "#/components/schemas/CustomerUpdatedEvent" } } } @@ -26080,10 +29164,10 @@ } } }, - "chat.clear": { + "customer.archiving": { "post": { - "operationId": "onChatClear", - "summary": "Chat cleared", + "operationId": "onCustomerArchiving", + "summary": "Customer archival started", "tags": [ "Webhooks" ], @@ -26092,7 +29176,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChatClearEvent" + "$ref": "#/components/schemas/CustomerArchivingEvent" } } } @@ -26104,10 +29188,10 @@ } } }, - "chat.delete": { + "customer.archived": { "post": { - "operationId": "onChatDelete", - "summary": "Chat deleted", + "operationId": "onCustomerArchived", + "summary": "Customer archived", "tags": [ "Webhooks" ], @@ -26116,7 +29200,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ChatDeleteEvent" + "$ref": "#/components/schemas/CustomerArchivedEvent" } } } @@ -26128,10 +29212,10 @@ } } }, - "call.received": { + "customer.restored": { "post": { - "operationId": "onCallReceived", - "summary": "Incoming call", + "operationId": "onCustomerRestored", + "summary": "Customer restored", "tags": [ "Webhooks" ], @@ -26140,7 +29224,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CallReceivedEvent" + "$ref": "#/components/schemas/CustomerRestoredEvent" } } } @@ -26152,10 +29236,10 @@ } } }, - "call.missed": { + "customer.pairing_link.created": { "post": { - "operationId": "onCallMissed", - "summary": "Call missed", + "operationId": "onCustomerPairingLinkCreated", + "summary": "Customer pairing link created", "tags": [ "Webhooks" ], @@ -26164,7 +29248,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CallMissedEvent" + "$ref": "#/components/schemas/CustomerPairingLinkCreatedEvent" } } } @@ -26176,10 +29260,10 @@ } } }, - "call.accepted": { + "customer.pairing_link.opened": { "post": { - "operationId": "onCallAccepted", - "summary": "Call accepted", + "operationId": "onCustomerPairingLinkOpened", + "summary": "Customer pairing link opened", "tags": [ "Webhooks" ], @@ -26188,7 +29272,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CallAcceptedEvent" + "$ref": "#/components/schemas/CustomerPairingLinkOpenedEvent" } } } @@ -26200,10 +29284,10 @@ } } }, - "call.rejected": { + "customer.pairing_link.connected": { "post": { - "operationId": "onCallRejected", - "summary": "Call rejected", + "operationId": "onCustomerPairingLinkConnected", + "summary": "Customer pairing link connected", "tags": [ "Webhooks" ], @@ -26212,7 +29296,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CallRejectedEvent" + "$ref": "#/components/schemas/CustomerPairingLinkConnectedEvent" } } } @@ -26224,10 +29308,10 @@ } } }, - "newsletter.update": { + "customer.pairing_link.failed": { "post": { - "operationId": "onNewsletterUpdate", - "summary": "Newsletter subscription change", + "operationId": "onCustomerPairingLinkFailed", + "summary": "Customer pairing attempt failed", "tags": [ "Webhooks" ], @@ -26236,7 +29320,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NewsletterUpdateEvent" + "$ref": "#/components/schemas/CustomerPairingLinkFailedEvent" } } } @@ -26248,10 +29332,10 @@ } } }, - "blocklist.update": { + "customer.pairing_link.expired": { "post": { - "operationId": "onBlocklistUpdate", - "summary": "Blocklist changed", + "operationId": "onCustomerPairingLinkExpired", + "summary": "Customer pairing link expired", "tags": [ "Webhooks" ], @@ -26260,7 +29344,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BlocklistUpdateEvent" + "$ref": "#/components/schemas/CustomerPairingLinkExpiredEvent" } } } @@ -26272,10 +29356,10 @@ } } }, - "labels.update": { + "customer.pairing_link.revoked": { "post": { - "operationId": "onLabelsUpdate", - "summary": "Label edited / associated / star toggled", + "operationId": "onCustomerPairingLinkRevoked", + "summary": "Customer pairing link revoked", "tags": [ "Webhooks" ], @@ -26284,7 +29368,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LabelsUpdateEvent" + "$ref": "#/components/schemas/CustomerPairingLinkRevokedEvent" } } } @@ -26296,10 +29380,10 @@ } } }, - "business.quick_reply.update": { + "customer.number.attached": { "post": { - "operationId": "onBusinessQuickReplyUpdate", - "summary": "WhatsApp Business App quick reply changed", + "operationId": "onCustomerNumberAttached", + "summary": "Number attached to a Customer", "tags": [ "Webhooks" ], @@ -26308,7 +29392,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BusinessQuickReplyUpdateEvent" + "$ref": "#/components/schemas/CustomerNumberAttachedEvent" } } } @@ -26320,10 +29404,10 @@ } } }, - "history.sync": { + "customer.number.transferred": { "post": { - "operationId": "onHistorySync", - "summary": "History sync chunk", + "operationId": "onCustomerNumberTransferred", + "summary": "Number transferred to a Customer", "tags": [ "Webhooks" ], @@ -26332,7 +29416,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HistorySyncEvent" + "$ref": "#/components/schemas/CustomerNumberTransferredEvent" } } } @@ -26344,10 +29428,10 @@ } } }, - "command.result": { + "customer.number.disconnected": { "post": { - "operationId": "onCommandResult", - "summary": "Async command completed", + "operationId": "onCustomerNumberDisconnected", + "summary": "Customer Number disconnected", "tags": [ "Webhooks" ], @@ -26356,7 +29440,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CommandResultEvent" + "$ref": "#/components/schemas/CustomerNumberDisconnectedEvent" } } } diff --git a/contracts/openapi.platform.json b/contracts/openapi.platform.json index 4052e378..a8672424 100644 --- a/contracts/openapi.platform.json +++ b/contracts/openapi.platform.json @@ -3,7 +3,7 @@ "info": { "title": "Polymorfa Console & Management API", "version": "1.0.0", - "description": "Complete console/management surface. Resource-oriented REST: `{param}` path variables, single-word action sub-resources.\n\nNamespaces decide inclusion:\n- `/v1/…` — **public registry** (71 endpoints): each endpoint declares either a scoped `pmfa_` organization key or a verified live dashboard JWT, or dashboard-only authorization when a specific user's authority is required. Only implemented handlers are published to customers and SDKs.\n- `/console/…` — **console-only** (50 endpoints): verified dashboard identity. EXCLUDED from the published spec; reason per endpoint.\n- `/admin/…` — **staff-only** (48 endpoints): Polymorfa internal staff. EXCLUDED.\n\nThe published customer spec = this document filtered to `x-audience: public` endpoints with an implemented runtime handler.\nMost transitional body/response schemas remain loose (`additionalProperties: true`); session-control operations use explicit strict schemas." + "description": "Complete console/management surface. Resource-oriented REST: `{param}` path variables, single-word action sub-resources.\n\nNamespaces decide inclusion:\n- `/v1/…` — **public registry** (123 endpoints): each endpoint declares either a scoped `pmfa_` organization key or a verified live dashboard JWT, or dashboard-only authorization when a specific user's authority is required. Only implemented handlers are published to customers and SDKs.\n- `/console/…` — **console-only** (50 endpoints): verified dashboard identity. EXCLUDED from the published spec; reason per endpoint.\n- `/admin/…` — **staff-only** (48 endpoints): Polymorfa internal staff. EXCLUDED.\n\nThe published customer spec = this document filtered to `x-audience: public` endpoints with an implemented runtime handler.\nMost transitional body/response schemas remain loose (`additionalProperties: true`); session-control operations use explicit strict schemas." }, "servers": [ { @@ -22,8 +22,9 @@ "sessions" ], "summary": "List sessions", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Lists every session in the authenticated organization or selected project, including disconnected sessions and their latest status reason.\n\nOrganization API-key callers require `sessions:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "x-merged-endpoints": [ @@ -50,22 +51,141 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlatformSession" + } + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" + } + } + } + }, + "/v1/sessions/{sessionId}/start": { + "post": { + "operationId": "startPlatformSession", + "tags": [ + "sessions" + ], + "summary": "Start a session", + "description": "Starts a stopped session or retries a session whose runtime failed. The identifier may be the session UUID or stable session slug.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `session` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "session", + "x-excluded-from-public": false, + "x-required-scope": "sessions:manage", + "security": [ + { + "ApiKeyAuth": [] }, - "501": { - "$ref": "#/components/responses/NotImplemented" + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "Database session UUID or stable session slug.", + "schema": { + "$ref": "#/components/schemas/SessionIdentifier" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionProjectContext" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/SessionStartResult" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "500": { + "$ref": "#/components/responses/InternalError" } } } @@ -77,8 +197,9 @@ "sessions" ], "summary": "Stop a session", - "description": "Requests an asynchronous stop for one session. The identifier may be the session UUID or stable session slug.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Requests an asynchronous stop for one session. The identifier may be the session UUID or stable session slug.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `session` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "session", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -113,6 +234,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -152,8 +287,9 @@ "sessions" ], "summary": "Remove a session", - "description": "Permanently removes one session and terminates its active connection when needed.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Permanently removes one session and terminates its active connection when needed.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `session` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "session", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -178,6 +314,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -212,8 +362,9 @@ "sessions" ], "summary": "Set a session tier override", - "description": "Sets an explicit session tier or clears the override with null.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Sets an explicit session tier or clears the override with null.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `session` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "session", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -248,6 +399,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -287,8 +452,9 @@ "sessions" ], "summary": "Stop multiple sessions", - "description": "Requests asynchronous stops for up to 100 session UUIDs or slugs in the authenticated project scope.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Requests asynchronous stops for up to 100 session UUIDs or slugs in the authenticated project scope.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -312,6 +478,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -351,8 +531,9 @@ "sessions" ], "summary": "Remove multiple sessions", - "description": "Permanently removes up to 100 session UUIDs or slugs and terminates any rows that were still active.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Permanently removes up to 100 session UUIDs or slugs and terminates any rows that were still active.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -376,6 +557,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -412,8 +607,9 @@ "sessions" ], "summary": "Create a testing number", - "description": "Creates a disconnected simulated session with testMode enabled. Organization keys and dashboard bearers must provide a projectId in the body.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Creates a disconnected simulated session with testMode enabled. Organization keys and dashboard bearers must provide a projectId in the body.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -437,6 +633,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -479,8 +689,9 @@ "bans" ], "summary": "List session bans", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -494,6 +705,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -521,8 +746,9 @@ "bans" ], "summary": "List active session bans", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -536,6 +762,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -563,8 +803,9 @@ "campaigns" ], "summary": "List platform campaigns", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -596,6 +837,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -621,8 +876,9 @@ "campaigns" ], "summary": "Create campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -647,11 +903,25 @@ "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } } } }, @@ -674,8 +944,9 @@ "campaigns" ], "summary": "Get campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -699,6 +970,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -724,8 +1009,9 @@ "campaigns" ], "summary": "Update campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -760,6 +1046,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -785,8 +1085,9 @@ "campaigns" ], "summary": "Delete campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -810,6 +1111,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -837,8 +1152,9 @@ "campaigns" ], "summary": "Launch campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -873,6 +1189,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -900,8 +1230,9 @@ "campaigns" ], "summary": "Pause campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -936,6 +1267,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -963,8 +1308,9 @@ "campaigns" ], "summary": "Resume campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -999,6 +1345,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1026,8 +1386,9 @@ "campaigns" ], "summary": "Stop campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -1062,6 +1423,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1089,8 +1464,9 @@ "campaigns" ], "summary": "Archive campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -1125,6 +1501,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1152,8 +1542,9 @@ "campaigns" ], "summary": "Duplicate campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -1188,6 +1579,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1215,8 +1620,9 @@ "campaigns" ], "summary": "Requeue failed campaign", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -1251,6 +1657,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1278,8 +1698,9 @@ "campaigns" ], "summary": "Get campaign analytics", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -1303,6 +1724,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1330,8 +1765,9 @@ "campaigns" ], "summary": "List campaign events", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -1355,6 +1791,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1382,8 +1832,9 @@ "campaigns" ], "summary": "List campaign recipients", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -1407,6 +1858,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1434,8 +1899,9 @@ "audiences" ], "summary": "List audiences", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -1449,6 +1915,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1474,8 +1954,9 @@ "audiences" ], "summary": "Create audience", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -1500,6 +1981,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1527,8 +2022,9 @@ "audiences" ], "summary": "Get audience", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -1552,6 +2048,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1577,8 +2087,9 @@ "audiences" ], "summary": "Delete audience", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -1602,9 +2113,23 @@ "responses": { "200": { "description": "Success", - "content": { - "application/json": { - "schema": { + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/DataEnvelope" } } @@ -1629,8 +2154,9 @@ "audiences" ], "summary": "Create audience upload url", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -1655,6 +2181,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1682,11 +2222,15 @@ "templates" ], "summary": "List message templates", - "description": "Lists the message-template drafts in one project.\n\nRequires a verified dashboard bearer and current organization authorization. Organization keys and project tokens are not accepted.", + "description": "Lists the message-template drafts in one project.\n\nOrganization API-key callers require `templates:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, + "x-required-scope": "templates:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -1704,6 +2248,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1745,12 +2303,15 @@ "templates" ], "summary": "Create a message-template draft", - "description": "Creates an incomplete or complete message-template draft in one project.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "description": "Creates an incomplete or complete message-template draft in one project.\n\nOrganization API-key callers require `templates:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "templates:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -1768,6 +2329,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1807,11 +2382,15 @@ "templates" ], "summary": "Get a message-template draft", - "description": "Gets one message-template draft from the specified project.\n\nRequires a verified dashboard bearer and current organization authorization. Organization keys and project tokens are not accepted.", + "description": "Gets one message-template draft from the specified project.\n\nOrganization API-key callers require `templates:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, + "x-required-scope": "templates:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -1837,6 +2416,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1880,12 +2473,15 @@ "templates" ], "summary": "Update a message-template draft", - "description": "Updates one message-template draft in the project supplied in the request body.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "description": "Updates one message-template draft in the project supplied in the request body.\n\nOrganization API-key callers require `templates:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "templates:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -1913,6 +2509,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -1950,12 +2560,15 @@ "templates" ], "summary": "Delete a message-template draft", - "description": "Deletes one message-template draft from the specified project.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "description": "Deletes one message-template draft from the specified project.\n\nOrganization API-key callers require `templates:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "templates:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -1981,6 +2594,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2020,11 +2647,15 @@ "flows" ], "summary": "List Flow drafts", - "description": "Lists compact WhatsApp Flow draft summaries in one project.\n\nRequires a verified dashboard bearer and current organization authorization. Organization keys and project tokens are not accepted.", + "description": "Lists compact WhatsApp Flow draft summaries in one project.\n\nOrganization API-key callers require `sessions:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, + "x-required-scope": "sessions:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -2042,6 +2673,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2083,12 +2728,15 @@ "flows" ], "summary": "Create a Flow draft", - "description": "Creates a project-scoped WhatsApp Flow draft. A supplied draftId makes retries idempotent.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "description": "Creates a project-scoped WhatsApp Flow draft. A supplied draftId makes retries idempotent.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "sessions:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -2106,6 +2754,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2145,11 +2807,15 @@ "flows" ], "summary": "Get a Flow draft", - "description": "Gets one project-scoped WhatsApp Flow draft.\n\nRequires a verified dashboard bearer and current organization authorization. Organization keys and project tokens are not accepted.", + "description": "Gets one project-scoped WhatsApp Flow draft.\n\nOrganization API-key callers require `sessions:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, + "x-required-scope": "sessions:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -2175,6 +2841,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2218,12 +2898,15 @@ "flows" ], "summary": "Update a Flow draft", - "description": "Updates a Flow draft when expectedUpdatedAt matches the stored revision.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "description": "Updates a Flow draft when expectedUpdatedAt matches the stored revision.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "sessions:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -2251,6 +2934,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2291,12 +2988,15 @@ "flows" ], "summary": "Delete a Flow draft", - "description": "Deletes a local Flow draft without deleting linked Meta Flow assets.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "description": "Deletes a local Flow draft without deleting linked Meta Flow assets.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "sessions:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } @@ -2322,6 +3022,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2361,8 +3075,9 @@ "optouts" ], "summary": "List opt outs", - "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:read", "security": [ @@ -2376,6 +3091,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2401,8 +3130,9 @@ "optouts" ], "summary": "Create opt out", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -2427,15 +3157,29 @@ "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { @@ -2454,8 +3198,9 @@ "optouts" ], "summary": "Create opt outs", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -2480,6 +3225,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2507,8 +3266,9 @@ "optouts" ], "summary": "Delete opt out", - "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `campaigns:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "campaigns:manage", "security": [ @@ -2532,6 +3292,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2559,8 +3333,9 @@ "billing" ], "summary": "Get billing", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -2574,6 +3349,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2601,8 +3390,9 @@ "billing" ], "summary": "List usage meters", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -2616,6 +3406,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2643,8 +3447,9 @@ "billing" ], "summary": "List billing transactions", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -2658,6 +3463,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2685,8 +3504,9 @@ "billing" ], "summary": "List tier pricing", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -2700,6 +3520,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2727,8 +3561,9 @@ "billing" ], "summary": "Update billing reminder settings", - "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -2753,6 +3588,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2780,8 +3629,9 @@ "organization" ], "summary": "Get organization", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -2795,6 +3645,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2822,6 +3686,7 @@ "summary": "Update organization", "description": "Public Management API. Requires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", "x-audience": "public", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": false, "x-dashboard-only": true, "x-required-organization-role": "owner_or_admin", @@ -2844,6 +3709,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2871,8 +3750,9 @@ "projects" ], "summary": "List projects", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:read", "security": [ @@ -2886,6 +3766,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2911,8 +3805,9 @@ "projects" ], "summary": "Create project", - "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -2937,6 +3832,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -2964,8 +3873,9 @@ "projects" ], "summary": "Request production enrollment", - "description": "Creates a durable production-enrollment operation. The project stays in development until provisioning succeeds. Poll the returned operationId for progress.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "description": "Creates a durable production-enrollment operation. The project stays in development until provisioning succeeds. Poll the returned operationId for progress.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, "x-required-scope": "sessions:manage", "security": [ @@ -3000,6 +3910,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -3035,17 +3959,18 @@ } } }, - "/v1/projects/{projectId}/production-enrollments/{operationId}/approve": { - "post": { - "operationId": "approveProductionEnrollment", + "/v1/projects/{projectId}/customers/status": { + "get": { + "operationId": "getCustomersStatus", "tags": [ - "projects" + "customers" ], - "summary": "Approve production enrollment", - "description": "Resumes a production-enrollment operation that requires approval.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "summary": "Get Customers status", + "description": "Returns whether Customers is enabled for the exact project and its protected default Customer when enabled.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "sessions:manage", + "x-required-scope": "customers:read", "security": [ { "ApiKeyAuth": [] @@ -3063,31 +3988,25 @@ "type": "string", "format": "uuid" } - }, - { - "name": "operationId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -3095,7 +4014,7 @@ "additionalProperties": false, "properties": { "data": { - "$ref": "#/components/schemas/ProductionEnrollmentCommandResult" + "$ref": "#/components/schemas/CustomersStatus" } }, "required": [ @@ -3114,23 +4033,24 @@ "403": { "$ref": "#/components/responses/Forbidden" }, - "409": { - "$ref": "#/components/responses/Conflict" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/v1/projects/{projectId}/production-enrollments/{operationId}/cancel": { + "/v1/projects/{projectId}/customers/enable": { "post": { - "operationId": "cancelProductionEnrollment", + "operationId": "enableCustomers", "tags": [ - "projects" + "customers" ], - "summary": "Cancel production enrollment", - "description": "Requests cancellation of an active production-enrollment operation. Provisioned resources are compensated before cancellation completes.\n\nOrganization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "summary": "Enable Customers", + "description": "Enables Customers once, creates the default Customer, and assigns every existing Number in the project to it atomically.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "sessions:manage", + "x-required-scope": "customers:manage", "security": [ { "ApiKeyAuth": [] @@ -3150,29 +4070,33 @@ } }, { - "name": "operationId", - "in": "path", + "name": "Idempotency-Key", + "in": "header", "required": true, "schema": { "type": "string", - "format": "uuid" + "minLength": 1, + "maxLength": 255 } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -3180,7 +4104,7 @@ "additionalProperties": false, "properties": { "data": { - "$ref": "#/components/schemas/ProductionEnrollmentCommandResult" + "$ref": "#/components/schemas/CustomersEnablement" } }, "required": [ @@ -3199,23 +4123,27 @@ "403": { "$ref": "#/components/responses/Forbidden" }, + "404": { + "$ref": "#/components/responses/NotFound" + }, "409": { "$ref": "#/components/responses/Conflict" } } } }, - "/v1/operations/{operationId}": { - "get": { - "operationId": "getPlatformOperation", + "/v1/projects/{projectId}/production-enrollments/{operationId}/approve": { + "post": { + "operationId": "approveProductionEnrollment", "tags": [ - "operations" + "projects" ], - "summary": "Get an asynchronous operation", - "description": "Returns the durable state of an asynchronous operation in the authenticated organization.\n\nOrganization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "summary": "Approve production enrollment", + "description": "Resumes a production-enrollment operation that requires approval.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "sessions:read", + "x-required-scope": "sessions:manage", "security": [ { "ApiKeyAuth": [] @@ -3225,6 +4153,15 @@ } ], "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, { "name": "operationId", "in": "path", @@ -3235,9 +4172,34 @@ } } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -3245,7 +4207,7 @@ "additionalProperties": false, "properties": { "data": { - "$ref": "#/components/schemas/ManagementOperation" + "$ref": "#/components/schemas/ProductionEnrollmentCommandResult" } }, "required": [ @@ -3264,23 +4226,24 @@ "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/v1/members": { - "get": { - "operationId": "listMembers", + "/v1/projects/{projectId}/production-enrollments/{operationId}/cancel": { + "post": { + "operationId": "cancelProductionEnrollment", "tags": [ - "members" + "projects" ], - "summary": "List members", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "summary": "Cancel production enrollment", + "description": "Requests cancellation of an active production-enrollment operation. Provisioned resources are compensated before cancellation completes.\n\nOrganization API-key callers require `sessions:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "sessions:read", + "x-required-scope": "sessions:manage", "security": [ { "ApiKeyAuth": [] @@ -3289,44 +4252,24 @@ "ConsoleSession": [] } ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/v1/members/invites": { - "post": { - "operationId": "inviteMember", - "tags": [ - "members" - ], - "summary": "Invite member", - "description": "Public Management API. Requires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", - "x-audience": "public", - "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", - "security": [ { - "ConsoleSession": [] + "name": "operationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } ], "requestBody": { @@ -3343,146 +4286,265 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProductionEnrollmentCommandResult" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/v1/members/{memberId}": { - "patch": { - "operationId": "updateMemberRole", + "/v1/webhooks": { + "get": { + "operationId": "listOrganizationWebhooks", "tags": [ - "members" + "webhooks" ], - "summary": "Update member role", - "description": "Public Management API. Requires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "summary": "List organization webhooks", + "description": "Lists only organization-owned webhook endpoints. Project-owned endpoints are excluded.\n\nOrganization API-key callers require `webhooks:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "webhooks:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "memberId", - "in": "path", - "required": true, + "name": "eventType", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + { + "name": "enabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationWebhook" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" } } }, - "delete": { - "operationId": "deleteMember", + "post": { + "operationId": "createOrganizationWebhook", "tags": [ - "members" + "webhooks" ], - "summary": "Delete member", - "description": "Public Management API. Requires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "summary": "Create an organization webhook", + "description": "Creates an organization-owned webhook and returns its generated signing secret once.\n\nOrganization API-key callers require `webhooks:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, - "x-dashboard-only": true, - "x-required-organization-role": "owner_or_admin", + "x-required-scope": "webhooks:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "memberId", - "in": "path", + "name": "Idempotency-Key", + "in": "header", "required": true, "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateOrganizationWebhookInput" + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookCreationReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/v1/keys": { + "/v1/webhooks/{webhookId}": { "get": { - "operationId": "listApiKeys", + "operationId": "getOrganizationWebhook", "tags": [ - "keys" + "webhooks" ], - "summary": "List api keys", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "summary": "Get an organization webhook", + "description": "Returns one organization-owned webhook without secret values.\n\nOrganization API-key callers require `webhooks:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, - "x-required-scope": "sessions:read", + "x-required-scope": "webhooks:read", "security": [ { "ApiKeyAuth": [] @@ -3491,40 +4553,76 @@ "ConsoleSession": [] } ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhook" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } - } - }, - "/v1/keys/{keyId}": { - "delete": { - "operationId": "deactivateApiKey", + }, + "patch": { + "operationId": "updateOrganizationWebhook", "tags": [ - "keys" + "webhooks" ], - "summary": "Deactivate api key", - "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "summary": "Update an organization webhook", + "description": "Updates settings without rotating the signing secret.\n\nOrganization API-key callers require `webhooks:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, - "x-required-scope": "sessions:manage", + "x-required-scope": "webhooks:manage", "security": [ { "ApiKeyAuth": [] @@ -3535,48 +4633,97 @@ ], "parameters": [ { - "name": "keyId", + "name": "webhookId", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateOrganizationWebhookInput" + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookMutationReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } - } - }, - "/v1/tokens": { - "get": { - "operationId": "listPolymorfaTokens", + }, + "delete": { + "operationId": "deleteOrganizationWebhook", "tags": [ - "tokens" + "webhooks" ], - "summary": "List polymorfa tokens", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "summary": "Delete an organization webhook", + "description": "Deletes one organization-owned webhook.\n\nOrganization API-key callers require `webhooks:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, - "x-required-scope": "sessions:read", + "x-required-scope": "webhooks:manage", "security": [ { "ApiKeyAuth": [] @@ -3587,90 +4734,89 @@ ], "parameters": [ { - "name": "projectId", - "in": "query", - "required": false, + "name": "webhookId", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/v1/audit": { - "get": { - "operationId": "listAuditLogs", - "tags": [ - "audit" - ], - "summary": "List audit logs", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-excluded-from-public": false, - "x-required-scope": "sessions:read", - "security": [ - { - "ApiKeyAuth": [] - }, - { - "ConsoleSession": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookDeletionReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/v1/incidents": { - "get": { - "operationId": "listSecurityIncidents", + "/v1/webhooks/{webhookId}/secret-rotations": { + "post": { + "operationId": "rotateOrganizationWebhookSecret", "tags": [ - "incidents" + "webhooks" ], - "summary": "List security incidents", - "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization.", + "summary": "Rotate an organization webhook secret", + "description": "Rotates the signing secret and returns the new secret once.\n\nOrganization API-key callers require `webhooks:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, - "x-required-scope": "sessions:read", + "x-required-scope": "webhooks:manage", "security": [ { "ApiKeyAuth": [] @@ -3679,40 +4825,101 @@ "ConsoleSession": [] } ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RotateWebhookSecretInput" + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookSecretRotationReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/v1/incidents/{incidentId}/acknowledge": { + "/v1/webhooks/{webhookId}/tests": { "post": { - "operationId": "acknowledgeSecurityIncident", + "operationId": "testOrganizationWebhook", "tags": [ - "incidents" + "webhooks" ], - "summary": "Acknowledge security incident", - "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization.", + "summary": "Send an organization webhook test", + "description": "Creates a synthetic organization event and a traceable webhook delivery.\n\nOrganization API-key callers require `webhooks:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", "x-excluded-from-public": false, - "x-required-scope": "sessions:manage", + "x-required-scope": "webhooks:manage", "security": [ { "ApiKeyAuth": [] @@ -3723,11 +4930,22 @@ ], "parameters": [ { - "name": "incidentId", + "name": "webhookId", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], @@ -3736,8 +4954,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/TestWebhookInput" } } } @@ -3745,37 +4962,67 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookTestReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/v1/media/{mediaId}": { + "/v1/projects/{projectId}/webhooks": { "get": { - "operationId": "getMediaUrl", + "operationId": "listProjectWebhooks", "tags": [ - "media" + "webhooks" ], - "summary": "Get media url", - "description": "Public Management API. Organization API-key callers require `media:read`; dashboard callers require current organization membership and authorization.", + "summary": "List project webhooks", + "description": "Lists webhooks owned by the exact project in the route.\n\nOrganization API-key callers require `webhooks:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "media:read", + "x-required-scope": "webhooks:read", "security": [ { "ApiKeyAuth": [] @@ -3786,9 +5033,47 @@ ], "parameters": [ { - "name": "mediaId", + "name": "projectId", "in": "path", "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "eventType", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + { + "name": "enabled", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, "schema": { "type": "string" } @@ -3797,35 +5082,69 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectWebhook" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } }, - "delete": { - "operationId": "deleteMedia", + "post": { + "operationId": "createProjectWebhook", "tags": [ - "media" + "webhooks" ], - "summary": "Delete media", - "description": "Public Management API. Organization API-key callers require `media:manage`; dashboard callers require current organization membership and authorization.", + "summary": "Create a project webhook", + "description": "Creates a project-owned webhook and returns its generated signing secret once.\n\nOrganization API-key callers require `webhooks:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "media:manage", + "x-required-scope": "webhooks:manage", "security": [ { "ApiKeyAuth": [] @@ -3836,63 +5155,31 @@ ], "parameters": [ { - "name": "mediaId", + "name": "projectId", "in": "path", "required": true, "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/v1/media/uploads": { - "post": { - "operationId": "createMediaUploadUrl", - "tags": [ - "media" - ], - "summary": "Create media upload url", - "description": "Public Management API. Organization API-key callers require `media:manage`; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-excluded-from-public": false, - "x-required-scope": "media:manage", - "security": [ - { - "ApiKeyAuth": [] - }, { - "ConsoleSession": [] + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } } ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/CreateProjectWebhookInput" } } } @@ -3900,37 +5187,67 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookCreationReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/v1/widget": { + "/v1/projects/{projectId}/webhooks/{webhookId}": { "get": { - "operationId": "getWidgetSettings", + "operationId": "getProjectWebhook", "tags": [ - "widget" + "webhooks" ], - "summary": "Get widget settings", - "description": "Public Management API. Organization API-key callers require `widget:create`; dashboard callers require current organization membership and authorization.", + "summary": "Get a project webhook", + "description": "Returns one webhook owned by the exact project without secret values.\n\nOrganization API-key callers require `webhooks:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "widget:create", + "x-required-scope": "webhooks:read", "security": [ { "ApiKeyAuth": [] @@ -3942,45 +5259,82 @@ "parameters": [ { "name": "projectId", - "in": "query", - "required": false, + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhook" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } }, - "put": { - "operationId": "updateWidgetSettings", + "patch": { + "operationId": "updateProjectWebhook", "tags": [ - "widget" + "webhooks" ], - "summary": "Update widget settings", - "description": "Updates the widget settings for the authenticated organization and project.\n\nOrganization API-key callers require `widget:create`; dashboard callers require current organization membership and authorization.", + "summary": "Update a project webhook", + "description": "Updates settings without rotating the signing secret.\n\nOrganization API-key callers require `webhooks:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", "x-audience": "public", + "x-polymorfa-resource-boundary": "project", "x-excluded-from-public": false, - "x-required-scope": "widget:create", + "x-required-scope": "webhooks:manage", "security": [ { "ApiKeyAuth": [] @@ -3989,12 +5343,42 @@ "ConsoleSession": [] } ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateWidgetSettingsRequest" + "$ref": "#/components/schemas/UpdateProjectWebhookInput" } } } @@ -4002,6 +5386,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -4009,8 +5407,7 @@ "additionalProperties": false, "properties": { "data": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/ProjectWebhookMutationReceipt" } }, "required": [ @@ -4028,83 +5425,172 @@ }, "403": { "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } - } - }, - "/console/keys": { - "post": { - "operationId": "mintForConsoleApiKey", + }, + "delete": { + "operationId": "deleteProjectWebhook", "tags": [ - "keys" + "webhooks" ], - "summary": "Mint for console api key", - "description": "**EXCLUDED from the public API** (`console`). Credential red line: a pmfa_ key must never mint a pmfa_ key. Human session only — keeps revocation meaningful.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Credential red line: a pmfa_ key must never mint a pmfa_ key. Human session only — keeps revocation meaningful.", + "summary": "Delete a project webhook", + "description": "Deletes one webhook owned by the exact project.\n\nOrganization API-key callers require `webhooks:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "responses": { - "200": { - "description": "Success", + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookDeletionReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/console/billing/topup": { + "/v1/projects/{projectId}/webhooks/{webhookId}/secret-rotations": { "post": { - "operationId": "topUpBilling", + "operationId": "rotateProjectWebhookSecret", "tags": [ - "billing" + "webhooks" ], - "summary": "Top up billing", - "description": "**EXCLUDED from the public API** (`console`). Starts a Stripe Checkout browser redirect; human session only. API clients use auto-top-up.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Starts a Stripe Checkout browser redirect; human session only. API clients use auto-top-up.", + "summary": "Rotate a project webhook secret", + "description": "Rotates the signing secret and returns the new secret once.\n\nOrganization API-key callers require `webhooks:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], "requestBody": { "required": false, "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/RotateWebhookSecretInput" } } } @@ -4112,89 +5598,103 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookSecretRotationReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/console/organizations/{slug}": { - "get": { - "operationId": "getOrganizationBySlug", + "/v1/projects/{projectId}/webhooks/{webhookId}/tests": { + "post": { + "operationId": "testProjectWebhook", "tags": [ - "organizations" + "webhooks" ], - "summary": "Get organization by slug", - "description": "**EXCLUDED from the public API** (`console`). Console org-switcher lookup; a key is already bound to one org.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Console org-switcher lookup; a key is already bound to one org.", + "summary": "Send a project webhook test", + "description": "Creates a synthetic project event and a traceable webhook delivery. An optional complete native envelope is accepted only with an exact HMS-enabled project session.\n\nOrganization API-key callers require `webhooks:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhooks:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "slug", + "name": "projectId", "in": "path", "required": true, "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/organization/avatar/uploads": { - "post": { - "operationId": "generateOrganizationAvatarUploadUrl", - "tags": [ - "organization" - ], - "summary": "Generate organization avatar upload url", - "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser logo-upload flow.", - "security": [ { - "ConsoleSession": [] + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } } ], "requestBody": { @@ -4202,8 +5702,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/TestWebhookInput" } } } @@ -4211,197 +5710,314 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookTestReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/Unprocessable" + } + } + } }, - "/console/organization/avatar": { - "put": { - "operationId": "saveAvatarOrganization", + "/v1/events": { + "get": { + "operationId": "listOrganizationEvents", "tags": [ - "organization" + "events" ], - "summary": "Save avatar organization", - "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser logo-upload flow.", + "summary": "List organization events", + "description": "Lists organization-owned event metadata. Project-owned events are excluded.\n\nOrganization API-key callers require `events:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "events:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + }, + { + "name": "since", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" + { + "name": "until", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - }, - "delete": { - "operationId": "removeAvatarOrganization", - "tags": [ - "organization" - ], - "summary": "Remove avatar organization", - "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser logo-upload flow.", - "security": [ { - "ConsoleSession": [] + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationEvent" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" } } } }, - "/console/organizations/ensure": { - "post": { - "operationId": "ensureForCurrentUserOrganization", + "/v1/events/{eventId}": { + "get": { + "operationId": "getOrganizationEvent", "tags": [ - "organizations" + "events" ], - "summary": "Ensure for current user organization", - "description": "**EXCLUDED from the public API** (`console`). First-sign-in bootstrap for the session user.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "First-sign-in bootstrap for the session user.", + "summary": "Get an organization event", + "description": "Returns one organization-owned event and includes exact payload bytes only when requested and available.\n\nOrganization API-key callers require `events:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "events:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "eventId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includePayload", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationEvent" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/projects/{projectId}/icon": { + "/v1/events/{eventId}/replays": { "post": { - "operationId": "generateProjectUploadUrl", + "operationId": "replayOrganizationEvent", "tags": [ - "projects" + "events" ], - "summary": "Generate project upload url", - "description": "**EXCLUDED from the public API** (`console`). Browser icon-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser icon-upload flow.", + "summary": "Replay an organization event", + "description": "Creates a new logical delivery for a retained organization event.\n\nOrganization API-key callers require `events:replay`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "events:replay", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "projectId", + "name": "eventId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } } ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/ReplayEventInput" } } } @@ -4409,136 +6025,344 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationEventReplayReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/Unprocessable" } } } }, - "/console/profile": { + "/v1/projects/{projectId}/events": { "get": { - "operationId": "getUserProfile", + "operationId": "listProjectEvents", "tags": [ - "profile" + "events" ], - "summary": "Get user profile", - "description": "**EXCLUDED from the public API** (`console`). 'Current user' only exists for a session; keys are org-scoped, not user-scoped.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "'Current user' only exists for a session; keys are org-scoped, not user-scoped.", + "summary": "List project events", + "description": "Lists event metadata owned by the exact project in the route.\n\nOrganization API-key callers require `events:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "events:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "since", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "until", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectEvent" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } - }, - "patch": { - "operationId": "updateUserProfileField", + } + }, + "/v1/projects/{projectId}/events/{eventId}": { + "get": { + "operationId": "getProjectEvent", "tags": [ - "profile" + "events" ], - "summary": "Update user profile field", - "description": "**EXCLUDED from the public API** (`console`). Personal profile edit for the signed-in user.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Personal profile edit for the signed-in user.", + "summary": "Get a project event", + "description": "Returns one event owned by the exact project and includes exact payload bytes only when requested and available.\n\nOrganization API-key callers require `events:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "events:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "eventId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "includePayload", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "default": false } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectEvent" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/profile/ensure": { + "/v1/projects/{projectId}/events/{eventId}/replays": { "post": { - "operationId": "ensureProfileUserProfile", + "operationId": "replayProjectEvent", "tags": [ - "profile" + "events" ], - "summary": "Ensure profile user profile", - "description": "**EXCLUDED from the public API** (`console`). Sign-in bootstrap.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Sign-in bootstrap.", + "summary": "Replay a project event", + "description": "Creates a new logical delivery for a retained event owned by the exact project.\n\nOrganization API-key callers require `events:replay`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "events:replay", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "eventId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/ReplayEventInput" } } } @@ -4546,362 +6370,670 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectEventReplayReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/Unprocessable" } } } }, - "/console/teams": { + "/v1/webhook-deliveries": { "get": { - "operationId": "listUserProfileTeams", + "operationId": "listOrganizationWebhookDeliveries", "tags": [ - "teams" + "webhook deliveries" ], - "summary": "List user profile teams", - "description": "**EXCLUDED from the public API** (`console`). Session user's team switcher.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Session user's team switcher.", + "summary": "List organization webhook deliveries", + "description": "Lists logical deliveries for organization-owned webhooks. Project-owned deliveries are excluded.\n\nOrganization API-key callers require `webhook-deliveries:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "webhookId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "eventId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "pending", + "delivering", + "retrying", + "succeeded", + "failed" + ] + } + }, + { + "name": "since", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "until", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationWebhookDelivery" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" } } } }, - "/console/profile/avatar/uploads": { - "post": { - "operationId": "generateUserProfileAvatarUploadUrl", + "/v1/webhook-deliveries/{deliveryId}": { + "get": { + "operationId": "getOrganizationWebhookDelivery", "tags": [ - "profile" + "webhook deliveries" ], - "summary": "Generate user profile avatar upload url", - "description": "**EXCLUDED from the public API** (`console`). Browser avatar-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser avatar-upload flow.", + "summary": "Get an organization webhook delivery", + "description": "Returns one logical organization webhook delivery.\n\nOrganization API-key callers require `webhook-deliveries:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookDelivery" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/profile/avatar": { - "put": { - "operationId": "saveAvatarUserProfile", + "/v1/webhook-deliveries/{deliveryId}/retry": { + "post": { + "operationId": "retryOrganizationWebhookDelivery", "tags": [ - "profile" + "webhook deliveries" ], - "summary": "Save avatar user profile", - "description": "**EXCLUDED from the public API** (`console`). Browser avatar-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser avatar-upload flow.", + "summary": "Retry an organization webhook delivery", + "description": "Appends a new immutable attempt to the same logical delivery.\n\nOrganization API-key callers require `webhook-deliveries:retry`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:retry", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - }, - "delete": { - "operationId": "removeAvatarUserProfile", - "tags": [ - "profile" - ], - "summary": "Remove avatar user profile", - "description": "**EXCLUDED from the public API** (`console`). Personal profile edit.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Personal profile edit.", - "security": [ - { - "ConsoleSession": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookDeliveryRetryReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/Unprocessable" } } } }, - "/console/auth/methods": { + "/v1/webhook-deliveries/{deliveryId}/attempts": { "get": { - "operationId": "getUserProfileAuthMethods", + "operationId": "listOrganizationWebhookDeliveryAttempts", "tags": [ - "auth" + "webhook deliveries" ], - "summary": "Get user profile auth methods", - "description": "**EXCLUDED from the public API** (`console`). Account-security introspection for the session user.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Account-security introspection for the session user.", + "summary": "List organization delivery attempts", + "description": "Lists immutable network attempts for one organization webhook delivery.\n\nOrganization API-key callers require `webhook-deliveries:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "parameters": [ + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/auth/totp": { - "post": { - "operationId": "enrollTotpUserProfile", - "tags": [ - "auth" - ], - "summary": "Enroll totp user profile", - "description": "**EXCLUDED from the public API** (`console`). MFA enrollment via API key = account-takeover vector.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "MFA enrollment via API key = account-takeover vector.", - "security": [ { - "ConsoleSession": [] - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationWebhookDeliveryAttempt" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/auth/totp/verify": { - "post": { - "operationId": "verifyTotpUserProfile", + "/v1/webhook-deliveries/{deliveryId}/attempts/{attemptId}": { + "get": { + "operationId": "getOrganizationWebhookDeliveryAttempt", "tags": [ - "auth" + "webhook deliveries" ], - "summary": "Verify totp user profile", - "description": "**EXCLUDED from the public API** (`console`). Part of the human MFA step-up flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Part of the human MFA step-up flow.", + "summary": "Get organization delivery attempt", + "description": "Returns one immutable network attempt.\n\nOrganization API-key callers require `webhook-deliveries:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "attemptId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationWebhookDeliveryAttempt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/auth/factors/{factorId}": { - "delete": { - "operationId": "deleteUserProfileAuthFactor", + "/v1/projects/{projectId}/webhook-deliveries": { + "get": { + "operationId": "listProjectWebhookDeliveries", "tags": [ - "auth" + "webhook deliveries" ], - "summary": "Delete user profile auth factor", - "description": "**EXCLUDED from the public API** (`console`). Removing MFA via API key = account-takeover vector.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Removing MFA via API key = account-takeover vector.", + "summary": "List project webhook deliveries", + "description": "Lists logical webhook deliveries owned by the exact project in the route.\n\nOrganization API-key callers require `webhook-deliveries:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "factorId", + "name": "projectId", "in": "path", "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "webhookId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "eventId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "pending", + "delivering", + "retrying", + "succeeded", + "failed" + ] + } + }, + { + "name": "since", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "until", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, "schema": { "type": "string" } @@ -4910,954 +7042,1272 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectWebhookDelivery" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/auth/providers": { + "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}": { "get": { - "operationId": "getUserProfileProviderConnectionUrl", + "operationId": "getProjectWebhookDelivery", "tags": [ - "auth" + "webhook deliveries" ], - "summary": "Get user profile provider connection url", - "description": "**EXCLUDED from the public API** (`console`). Deferred Better Auth provider-link flow; dashboard identity only.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Deferred Better Auth provider-link flow; dashboard identity only.", + "summary": "Get a project webhook delivery", + "description": "Returns one logical webhook delivery owned by the exact project.\n\nOrganization API-key callers require `webhook-deliveries:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookDelivery" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/auth/providers/{provider}": { - "delete": { - "operationId": "disconnectProviderUserProfile", + "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}/retry": { + "post": { + "operationId": "retryProjectWebhookDelivery", "tags": [ - "auth" + "webhook deliveries" ], - "summary": "Disconnect provider user profile", - "description": "**EXCLUDED from the public API** (`console`). Deferred Better Auth provider-unlink flow; dashboard identity only.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Deferred Better Auth provider-unlink flow; dashboard identity only.", + "summary": "Retry a project webhook delivery", + "description": "Appends a new immutable attempt to the same logical delivery owned by the exact project.\n\nOrganization API-key callers require `webhook-deliveries:retry`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:retry", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "provider", + "name": "projectId", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookDeliveryRetryReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" + }, + "422": { + "$ref": "#/components/responses/Unprocessable" } } } }, - "/console/teams/leave": { - "post": { - "operationId": "leaveTeamUserProfile", + "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}/attempts": { + "get": { + "operationId": "listProjectWebhookDeliveryAttempts", "tags": [ - "teams" + "webhook deliveries" ], - "summary": "Leave team user profile", - "description": "**EXCLUDED from the public API** (`console`). Acts on the session user's own membership.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Acts on the session user's own membership.", + "summary": "List project webhook delivery attempts", + "description": "Lists immutable network attempts for one project webhook delivery.\n\nOrganization API-key callers require `webhook-deliveries:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ { - "ConsoleSession": [] - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" + "ApiKeyAuth": [] }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/teams/{teamId}/delete": { - "post": { - "operationId": "deleteUserProfileTeam", - "tags": [ - "teams" - ], - "summary": "Delete user profile team", - "description": "**EXCLUDED from the public API** (`console`). Destructive, human-confirmed flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Destructive, human-confirmed flow.", - "security": [ { "ConsoleSession": [] } ], "parameters": [ { - "name": "teamId", + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "deliveryId", "in": "path", "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectWebhookDeliveryAttempt" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/notifications": { + "/v1/projects/{projectId}/webhook-deliveries/{deliveryId}/attempts/{attemptId}": { "get": { - "operationId": "listNotificationInbox", + "operationId": "getProjectWebhookDeliveryAttempt", "tags": [ - "notifications" + "webhook deliveries" ], - "summary": "List notification inbox", - "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "User-scoped console inbox.", + "summary": "Get a project webhook delivery attempt", + "description": "Returns one immutable network attempt owned by the exact project.\n\nOrganization API-key callers require `webhook-deliveries:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "webhook-deliveries:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/notifications/archived": { - "get": { - "operationId": "listNotificationArchived", - "tags": [ - "notifications" - ], - "summary": "List notification archived", - "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "User-scoped console inbox.", - "security": [ { - "ConsoleSession": [] + "name": "attemptId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectWebhookDeliveryAttempt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/notifications/count": { + "/v1/operations": { "get": { - "operationId": "unreadCountNotification", + "operationId": "listOrganizationOperations", "tags": [ - "notifications" + "operations" ], - "summary": "Unread count notification", - "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "User-scoped console inbox.", + "summary": "List organization operations", + "description": "Lists only organization-owned asynchronous operations. Project-owned operations are excluded.\n\nOrganization API-key callers require `operations:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "operations:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "pending", + "running", + "action_required", + "cancelling", + "succeeded", + "failed", + "cancelled" + ] + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/notifications/{notificationId}/read": { - "post": { - "operationId": "markReadNotification", - "tags": [ - "notifications" - ], - "summary": "Mark read notification", - "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "User-scoped console inbox.", - "security": [ { - "ConsoleSession": [] - } - ], - "parameters": [ + "name": "kind", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "auth_projection_repair", + "session_lifecycle", + "auth_session_purge", + "label_projection_purge", + "billing_reconciliation", + "auto_top_up", + "campaign", + "production_enrollment", + "retention_sweep", + "webhook_redrive" + ] + } + }, { - "name": "notificationId", - "in": "path", - "required": true, + "name": "resourceType", + "in": "query", + "required": false, "schema": { "type": "string" } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + }, + { + "name": "resourceId", + "in": "query", + "required": false, + "schema": { + "type": "string" } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + }, + { + "name": "since", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" + { + "name": "until", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/notifications/read": { - "post": { - "operationId": "markAllReadNotification", - "tags": [ - "notifications" - ], - "summary": "Mark all read notification", - "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "User-scoped console inbox.", - "security": [ { - "ConsoleSession": [] - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrganizationOperation" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" } } } }, - "/console/notifications/{notificationId}/archive": { - "post": { - "operationId": "archiveNotification", + "/v1/operations/{operationId}": { + "get": { + "operationId": "getOrganizationOperation", "tags": [ - "notifications" + "operations" ], - "summary": "Archive notification", - "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "User-scoped console inbox.", + "summary": "Get an organization operation", + "description": "Returns one organization-owned asynchronous operation.\n\nOrganization API-key callers require `operations:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "operations:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "notificationId", + "name": "operationId", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationOperation" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/notifications/archive": { - "post": { - "operationId": "archiveAllNotification", + "/v1/operations/{operationId}/transitions": { + "get": { + "operationId": "listOrganizationOperationTransitions", "tags": [ - "notifications" + "operations" ], - "summary": "Archive all notification", - "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "User-scoped console inbox.", + "summary": "List organization operation transitions", + "description": "Lists immutable transitions for one organization-owned operation in ascending sequence.\n\nOrganization API-key callers require `operations:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "operations:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "parameters": [ + { + "name": "operationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/notifications/preferences": { - "get": { - "operationId": "getNotificationPreferences", - "tags": [ - "notifications" - ], - "summary": "Get notification preferences", - "description": "**EXCLUDED from the public API** (`console`). Per-user notification preferences.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Per-user notification preferences.", - "security": [ { - "ConsoleSession": [] - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "name": "afterSequence", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 } }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - }, - "put": { - "operationId": "updateNotificationPreferences", - "tags": [ - "notifications" - ], - "summary": "Update notification preferences", - "description": "**EXCLUDED from the public API** (`console`). Per-user notification preferences.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Per-user notification preferences.", - "security": [ { - "ConsoleSession": [] - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OperationTransition" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/notifications/test": { + "/v1/operations/{operationId}/cancel": { "post": { - "operationId": "sendTestNotificationEmail", + "operationId": "cancelOrganizationOperation", "tags": [ - "notifications" + "operations" ], - "summary": "Send test notification email", - "description": "**EXCLUDED from the public API** (`console`). Console settings test button.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Console settings test button.", + "summary": "Cancel an organization operation", + "description": "Requests cancellation after transactionally rechecking that the operation is cancellable.\n\nOrganization API-key callers require `operations:cancel`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "operations:cancel", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "operationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/OrganizationOperationCancellationReceipt" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/OperationCancellationConflict" } } } }, - "/console/widget/logo/uploads": { - "post": { - "operationId": "generateWidgetSettingsLogoUploadUrl", + "/v1/projects/{projectId}/operations": { + "get": { + "operationId": "listProjectOperations", "tags": [ - "widget" + "operations" ], - "summary": "Generate widget settings logo upload url", - "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser logo-upload flow.", + "summary": "List project operations", + "description": "Lists asynchronous operations owned by the exact project in the route.\n\nOrganization API-key callers require `operations:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "operations:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "pending", + "running", + "action_required", + "cancelling", + "succeeded", + "failed", + "cancelled" + ] + } }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "kind", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "auth_projection_repair", + "session_lifecycle", + "auth_session_purge", + "label_projection_purge", + "billing_reconciliation", + "auto_top_up", + "campaign", + "production_enrollment", + "retention_sweep", + "webhook_redrive" + ] + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/widget/logo": { - "put": { - "operationId": "attachLogoWidgetSettings", - "tags": [ - "widget" - ], - "summary": "Attach logo widget settings", - "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser logo-upload flow.", - "security": [ { - "ConsoleSession": [] - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "name": "resourceType", + "in": "query", + "required": false, + "schema": { + "type": "string" } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + }, + { + "name": "resourceId", + "in": "query", + "required": false, + "schema": { + "type": "string" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" + { + "name": "since", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "until", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - }, - "delete": { - "operationId": "removeLogoWidgetSettings", - "tags": [ - "widget" - ], - "summary": "Remove logo widget settings", - "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Browser logo-upload flow.", - "security": [ { - "ConsoleSession": [] + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectOperation" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" + } + }, + "required": [ + "data", + "page" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/widget/logo/org": { - "post": { - "operationId": "useOrgLogoWidgetSettings", + "/v1/projects/{projectId}/operations/{operationId}": { + "get": { + "operationId": "getProjectOperation", "tags": [ - "widget" + "operations" ], - "summary": "Use org logo widget settings", - "description": "**EXCLUDED from the public API** (`console`). Console snapshot flow (copies org logo).", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Console snapshot flow (copies org logo).", + "summary": "Get a project operation", + "description": "Returns one asynchronous operation owned by the exact project in the route.\n\nOrganization API-key callers require `operations:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "operations:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "operationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/ProjectOperation" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/widget/logo/project": { - "post": { - "operationId": "useProjectIconWidgetSettings", + "/v1/projects/{projectId}/operations/{operationId}/transitions": { + "get": { + "operationId": "listProjectOperationTransitions", "tags": [ - "widget" + "operations" ], - "summary": "Use project icon widget settings", - "description": "**EXCLUDED from the public API** (`console`). Console snapshot flow (copies project icon).", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Console snapshot flow (copies project icon).", + "summary": "List project operation transitions", + "description": "Lists immutable transitions for one project-owned operation in ascending sequence.\n\nOrganization API-key callers require `operations:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "operations:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } + }, + { + "name": "operationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } }, - "401": { - "$ref": "#/components/responses/Unauthorized" + { + "name": "afterSequence", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } }, - "403": { - "$ref": "#/components/responses/Forbidden" + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/console/webhooks": { - "get": { - "operationId": "listWebhooks", - "tags": [ - "webhooks" - ], - "summary": "List webhook endpoints", - "description": "Lists sanitized webhook endpoint settings for the authenticated organization.\n\nRequires a verified dashboard bearer and current organization authorization. Organization keys and project tokens are not accepted.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", - "security": [ { - "ConsoleSession": [] + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -5867,66 +8317,16 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/ConsoleWebhook" + "$ref": "#/components/schemas/OperationTransition" } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" } }, "required": [ - "data" - ] - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - } - } - }, - "post": { - "operationId": "createWebhook", - "tags": [ - "webhooks" - ], - "summary": "Create a webhook endpoint", - "description": "Creates a webhook endpoint for the authenticated organization. Requires live owner or admin authority.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", - "x-required-organization-role": "owner_or_admin", - "security": [ - { - "ConsoleSession": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateConsoleWebhookRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/ConsoleWebhook" - } - }, - "required": [ - "data" + "data", + "page" ] } } @@ -5940,51 +8340,80 @@ }, "403": { "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/webhooks/{webhookId}": { - "patch": { - "operationId": "updateWebhook", + "/v1/projects/{projectId}/operations/{operationId}/cancel": { + "post": { + "operationId": "cancelProjectOperation", "tags": [ - "webhooks" + "operations" ], - "summary": "Update a webhook endpoint", - "description": "Updates a webhook endpoint in the authenticated organization. Requires live owner or admin authority.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", - "x-required-organization-role": "owner_or_admin", + "summary": "Cancel a project operation", + "description": "Requests cancellation for an operation in the exact project after transactionally rechecking that it is cancellable.\n\nOrganization API-key callers require `operations:cancel`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "operations:cancel", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "webhookId", + "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateConsoleWebhookRequest" - } + }, + { + "name": "operationId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -5992,7 +8421,7 @@ "additionalProperties": false, "properties": { "data": { - "$ref": "#/components/schemas/ConsoleWebhook" + "$ref": "#/components/schemas/ProjectOperationCancellationReceipt" } }, "required": [ @@ -6013,39 +8442,125 @@ }, "404": { "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/OperationCancellationConflict" } } - }, - "delete": { - "operationId": "deleteWebhook", + } + }, + "/v1/customers": { + "get": { + "operationId": "listCustomers", "tags": [ - "webhooks" + "customers" ], - "summary": "Delete a webhook endpoint", - "description": "Deletes a webhook endpoint in the authenticated organization. Requires live owner or admin authority.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", - "x-required-organization-role": "owner_or_admin", + "summary": "List Customers", + "description": "Lists masked Customer summaries in the exact authenticated project with cursor pagination and filters.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:read", "security": [ { - "ConsoleSession": [] + "ApiKeyAuth": [] + }, + { + "ConsoleSession": [] } ], "parameters": [ { - "name": "webhookId", - "in": "path", + "name": "projectId", + "in": "query", "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "required": false, "schema": { "type": "string", - "format": "uuid" + "enum": [ + "active", + "archiving", + "archived", + "all" + ] + } + }, + { + "name": "isDefault", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "hasNumbers", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "needsAttention", + "in": "query", + "required": false, + "schema": { + "type": "boolean" } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -6053,11 +8568,18 @@ "additionalProperties": false, "properties": { "data": { - "$ref": "#/components/schemas/DeleteConsoleWebhookResult" + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerSummary" + } + }, + "page": { + "$ref": "#/components/schemas/CursorPageMetadata" } }, "required": [ - "data" + "data", + "page" ] } } @@ -6076,399 +8598,716 @@ "$ref": "#/components/responses/NotFound" } } - } - }, - "/console/sandbox/pool": { - "get": { - "operationId": "getSandboxProjectPool", + }, + "post": { + "operationId": "createCustomer", "tags": [ - "sandbox" + "customers" ], - "summary": "Get sandbox project project pool", - "description": "**EXCLUDED from the public API** (`console`). Console/sandbox internal wiring.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Console/sandbox internal wiring.", + "summary": "Create a Customer", + "description": "Creates a Customer with optional name, phone, and caller-defined external identifier.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomerRequest" + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/Customer" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/console/deployments": { + "/v1/customers/{customerId}": { "get": { - "operationId": "listDeployments", + "operationId": "getCustomer", "tags": [ - "deployments" + "customers" ], - "summary": "List deployments", - "description": "**EXCLUDED from the public API** (`console`). Legacy console surface; not a customer contract.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Legacy console surface; not a customer contract.", + "summary": "Get a Customer", + "description": "Returns one Customer from the exact authenticated project.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "customerId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "projectId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/Customer" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } - } - }, - "/console/insights/logs": { - "get": { - "operationId": "logsInsight", + }, + "patch": { + "operationId": "updateCustomer", "tags": [ - "insights" + "customers" ], - "summary": "Logs insight", - "description": "**EXCLUDED from the public API** (`console`). Customer console observability for the selected organization and project.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Customer console observability for the selected organization and project.", + "summary": "Update a Customer", + "description": "Updates supplied Customer profile fields. Null clears an optional field.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "projectId", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "end", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, + "name": "customerId", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateCustomerRequest" + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/Customer" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/console/insights/usage": { - "get": { - "operationId": "usageInsight", + "/v1/customers/{customerId}/archive": { + "post": { + "operationId": "archiveCustomer", "tags": [ - "insights" + "customers" ], - "summary": "Usage insight", - "description": "**EXCLUDED from the public API** (`console`). Customer console usage and entitlement presentation.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Customer console usage and entitlement presentation.", + "summary": "Archive a Customer", + "description": "Archives a non-default Customer. The default Customer cannot be archived.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "projectId", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "required": false, + "name": "customerId", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, { - "name": "end", - "in": "query", - "required": false, + "name": "Idempotency-Key", + "in": "header", + "required": true, "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerProjectRequest" + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/Customer" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/console/insights/analytics": { - "get": { - "operationId": "analyticsInsight", + "/v1/customers/{customerId}/restore": { + "post": { + "operationId": "restoreCustomer", "tags": [ - "insights" + "customers" ], - "summary": "Analytics insight", - "description": "**EXCLUDED from the public API** (`console`). Customer console aggregate product analytics; message bodies are never returned.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Customer console aggregate product analytics; message bodies are never returned.", + "summary": "Restore a Customer", + "description": "Restores an archived Customer.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "projectId", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "start", - "in": "query", - "required": false, + "name": "customerId", + "in": "path", + "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } }, { - "name": "end", - "in": "query", - "required": false, + "name": "Idempotency-Key", + "in": "header", + "required": true, "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerProjectRequest" + } + } + } + }, "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/Customer" + } + }, + "required": [ + "data" + ] + } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/console/insights/preferences": { + "/v1/customers/{customerId}/numbers": { "get": { - "operationId": "getInsightPreferences", + "operationId": "listCustomerNumbers", "tags": [ - "insights" + "customers" ], - "summary": "Get insight preferences", - "description": "**EXCLUDED from the public API** (`console`). Organization analytics-consent controls for the console.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Organization analytics-consent controls for the console.", + "summary": "List Customer Numbers", + "description": "Lists Numbers assigned to one Customer in the exact project.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "customerId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "projectId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerNumber" + } + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } - }, - "put": { - "operationId": "updateInsightPreferences", + } + }, + "/v1/customers/{customerId}/events": { + "get": { + "operationId": "listCustomerEvents", "tags": [ - "insights" + "customers" ], - "summary": "Update insight preferences", - "description": "**EXCLUDED from the public API** (`console`). Organization analytics-consent controls for the console.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Organization analytics-consent controls for the console.", + "summary": "List recent Customer events", + "description": "Lists the newest Customer lifecycle events with safe metadata. Actor identities, message content, phone values, invitation tokens, and URLs are excluded.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "customerId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "projectId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerEvent" + } + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } } }, - "/console/organization-invitations/{invitationId}/accept": { + "/v1/customers/{customerId}/pairing-links": { "post": { - "operationId": "acceptOrganizationInvitation", + "operationId": "createCustomerPairingLink", "tags": [ - "organization-invitations" + "customers" ], - "summary": "Accept organization invitation", - "description": "**EXCLUDED from the public API** (`console`). Deferred Better Auth invitation-acceptance flow; dashboard identity only.", - "x-audience": "console", - "x-excluded-from-public": true, - "x-exclusion-reason": "Deferred Better Auth invitation-acceptance flow; dashboard identity only.", + "summary": "Create a Customer pairing link", + "description": "Creates a revocable Customer invitation. The raw URL is returned only in this response.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "invitationId", + "name": "customerId", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], @@ -6477,8 +9316,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/CreateCustomerPairingLinkRequest" } } } @@ -6486,86 +9324,87 @@ "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/admin/orgs": { - "get": { - "operationId": "adminListOrganizations", - "tags": [ - "admin/orgs" - ], - "summary": "List organizations", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "security": [ - { - "ConsoleSession": [] - } - ], - "responses": { - "200": { - "description": "Success", + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/CreatedCustomerPairingLink" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } - } - }, - "/admin/orgs/{orgId}": { + }, "get": { - "operationId": "adminGetOrganization", + "operationId": "listCustomerPairingLinks", "tags": [ - "admin/orgs" + "customers" ], - "summary": "Get organization", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "List Customer pairing links", + "description": "Lists safe pairing-link metadata. Raw invitation tokens and URLs are never returned.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "orgId", + "name": "customerId", "in": "path", "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "projectId", + "in": "query", + "required": true, "schema": { "type": "string" } @@ -6574,164 +9413,210 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerPairingLink" + } + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" } } - }, - "patch": { - "operationId": "adminRenameOrganization", + } + }, + "/v1/customers/{customerId}/pairing-links/{pairingLinkId}": { + "delete": { + "operationId": "revokeCustomerPairingLink", "tags": [ - "admin/orgs" + "customers" ], - "summary": "Rename organization", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Revoke a Customer pairing link", + "description": "Revokes an open pairing link.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "orgId", + "name": "customerId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "pairingLinkId", "in": "path", "required": true, "schema": { "type": "string" } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + }, + { + "name": "projectId", + "in": "query", + "required": true, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/CustomerPairingLink" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } - }, - "delete": { - "operationId": "adminCascadeDeleteOrganization", + } + }, + "/v1/customers/{customerId}/numbers/{sessionId}/transfer": { + "post": { + "operationId": "transferCustomerNumber", "tags": [ - "admin/orgs" + "customers" ], - "summary": "Cascade delete organization", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Transfer a Customer Number", + "description": "Atomically transfers one eligible Number from the explicitly confirmed source Customer to the destination Customer.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "customers:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "orgId", + "name": "customerId", "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "uuid" } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/admin/orgs/{orgId}/freeze": { - "post": { - "operationId": "adminFreezeOrganization", - "tags": [ - "admin/orgs" - ], - "summary": "Freeze organization", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "security": [ - { - "ConsoleSession": [] - } - ], - "parameters": [ { - "name": "orgId", + "name": "sessionId", "in": "path", "required": true, + "description": "Database session UUID or stable session slug.", "schema": { - "type": "string" + "$ref": "#/components/schemas/SessionIdentifier" + } + }, + { + "name": "Idempotency-Key", + "in": "header", + "required": true, + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 255 } } ], "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/TransferCustomerNumberRequest" } } } @@ -6739,66 +9624,92 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/CustomerNumber" + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "501": { - "$ref": "#/components/responses/NotImplemented" + "404": { + "$ref": "#/components/responses/NotFound" + }, + "409": { + "$ref": "#/components/responses/Conflict" } } } }, - "/admin/orgs/{orgId}/unfreeze": { - "post": { - "operationId": "adminUnfreezeOrganization", + "/v1/members": { + "get": { + "operationId": "listMembers", "tags": [ - "admin/orgs" + "members" ], - "summary": "Unfreeze organization", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "List members", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "sessions:read", "security": [ { - "ConsoleSession": [] - } - ], - "parameters": [ + "ApiKeyAuth": [] + }, { - "name": "orgId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -6819,25 +9730,52 @@ } } }, - "/admin/orgs/features": { - "get": { - "operationId": "adminListOrganizationFeatureGroups", + "/v1/members/invites": { + "post": { + "operationId": "inviteMember", "tags": [ - "admin/orgs" + "members" ], - "summary": "List organization feature groups", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Invite member", + "description": "Public Management API. Requires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": false, + "x-dashboard-only": true, + "x-required-organization-role": "owner_or_admin", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -6858,17 +9796,19 @@ } } }, - "/admin/orgs/{orgId}/features": { + "/v1/members/{memberId}": { "patch": { - "operationId": "adminSetOrganizationFeatureGroups", + "operationId": "updateMemberRole", "tags": [ - "admin/orgs" + "members" ], - "summary": "Set organization feature groups", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Update member role", + "description": "Public Management API. Requires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": false, + "x-dashboard-only": true, + "x-required-organization-role": "owner_or_admin", "security": [ { "ConsoleSession": [] @@ -6876,7 +9816,7 @@ ], "parameters": [ { - "name": "orgId", + "name": "memberId", "in": "path", "required": true, "schema": { @@ -6898,6 +9838,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -6916,34 +9870,58 @@ "$ref": "#/components/responses/NotImplemented" } } - } - }, - "/admin/users": { - "get": { - "operationId": "adminSearchUser", + }, + "delete": { + "operationId": "deleteMember", "tags": [ - "admin/users" + "members" ], - "summary": "Search user", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Delete member", + "description": "Public Management API. Requires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": false, + "x-dashboard-only": true, + "x-required-organization-role": "owner_or_admin", "security": [ { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "memberId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } }, "401": { "$ref": "#/components/responses/Unauthorized" @@ -6957,35 +9935,43 @@ } } }, - "/admin/users/{userId}": { + "/v1/keys": { "get": { - "operationId": "adminGetUser", + "operationId": "listApiKeys", "tags": [ - "admin/users" + "keys" ], - "summary": "Get user", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "List api keys", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "sessions:read", "security": [ { - "ConsoleSession": [] - } - ], - "parameters": [ + "ApiKeyAuth": [] + }, { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "ConsoleSession": [] } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7006,25 +9992,29 @@ } } }, - "/admin/users/{userId}/onboarding": { + "/v1/keys/{keyId}": { "delete": { - "operationId": "adminResetOnboardingUser", + "operationId": "deactivateApiKey", "tags": [ - "admin/users" + "keys" ], - "summary": "Reset onboarding user", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Deactivate api key", + "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "sessions:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "userId", + "name": "keyId", "in": "path", "required": true, "schema": { @@ -7035,6 +10025,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7055,46 +10059,53 @@ } } }, - "/admin/users/{userId}/suspend": { - "post": { - "operationId": "adminSuspendUser", + "/v1/tokens": { + "get": { + "operationId": "listPolymorfaTokens", "tags": [ - "admin/users" + "tokens" ], - "summary": "Suspend user user", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "List polymorfa tokens", + "description": "Public Management API. Organization API-key callers require `sessions:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "sessions:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "userId", - "in": "path", - "required": true, + "name": "projectId", + "in": "query", + "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7115,46 +10126,43 @@ } } }, - "/admin/users/{userId}/unsuspend": { - "post": { - "operationId": "adminUnsuspendUser", + "/v1/audit": { + "get": { + "operationId": "listAuditLogs", "tags": [ - "admin/users" + "audit" ], - "summary": "Unsuspend user user", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "List audit logs", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "sessions:read", "security": [ { - "ConsoleSession": [] - } - ], - "parameters": [ + "ApiKeyAuth": [] + }, { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7175,46 +10183,43 @@ } } }, - "/admin/users/{userId}/signout": { - "post": { - "operationId": "adminForceSignOutUser", + "/v1/incidents": { + "get": { + "operationId": "listSecurityIncidents", "tags": [ - "admin/users" + "incidents" ], - "summary": "Force sign out user", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "List security incidents", + "description": "Public Management API. Organization API-key callers require `sessions:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "sessions:read", "security": [ { - "ConsoleSession": [] - } - ], - "parameters": [ + "ApiKeyAuth": [] + }, { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7235,25 +10240,29 @@ } } }, - "/admin/members/{memberId}": { - "patch": { - "operationId": "adminSetMemberRole", + "/v1/incidents/{incidentId}/acknowledge": { + "post": { + "operationId": "acknowledgeSecurityIncident", "tags": [ - "admin/members" + "incidents" ], - "summary": "Set member role", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Acknowledge security incident", + "description": "Public Management API. Organization API-key callers require `sessions:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "sessions:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "memberId", + "name": "incidentId", "in": "path", "required": true, "schema": { @@ -7275,6 +10284,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7293,25 +10316,31 @@ "$ref": "#/components/responses/NotImplemented" } } - }, - "delete": { - "operationId": "adminRemoveMember", + } + }, + "/v1/media/{mediaId}": { + "get": { + "operationId": "getMediaUrl", "tags": [ - "admin/members" + "media" ], - "summary": "Remove member member", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Get media url", + "description": "Public Management API. Organization API-key callers require `media:read`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "media:read", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "memberId", + "name": "mediaId", "in": "path", "required": true, "schema": { @@ -7322,6 +10351,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7340,77 +10383,53 @@ "$ref": "#/components/responses/NotImplemented" } } - } - }, - "/admin/members/invites": { - "post": { - "operationId": "adminInviteMember", + }, + "delete": { + "operationId": "deleteMedia", "tags": [ - "admin/members" + "media" ], - "summary": "Invite member member", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Delete media", + "description": "Public Management API. Organization API-key callers require `media:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "media:manage", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "schema": { + "type": "string" } } - }, + ], "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/admin/sessions": { - "get": { - "operationId": "adminListSessions", - "tags": [ - "admin/sessions" - ], - "summary": "List sessions", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "security": [ - { - "ConsoleSession": [] - } - ], - "responses": { - "200": { - "description": "Success", + }, "content": { "application/json": { "schema": { @@ -7431,31 +10450,24 @@ } } }, - "/admin/sessions/{sessionId}/stop": { + "/v1/media/uploads": { "post": { - "operationId": "adminForceStopSession", + "operationId": "createMediaUploadUrl", "tags": [ - "admin/sessions" + "media" ], - "summary": "Force stop session", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Create media upload url", + "description": "Public Management API. Organization API-key callers require `media:manage`; dashboard callers require current organization membership and authorization. Project tokens are rejected.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "organization", + "x-excluded-from-public": false, + "x-required-scope": "media:manage", "security": [ { - "ConsoleSession": [] - } - ], - "parameters": [ + "ApiKeyAuth": [] + }, { - "name": "sessionId", - "in": "path", - "required": true, - "description": "Database session UUID or stable session slug.", - "schema": { - "$ref": "#/components/schemas/SessionIdentifier" - } + "ConsoleSession": [] } ], "requestBody": { @@ -7472,6 +10484,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7492,36 +10518,53 @@ } } }, - "/admin/sessions/{sessionId}": { - "delete": { - "operationId": "adminForceDeleteSession", + "/v1/widget": { + "get": { + "operationId": "getWidgetSettings", "tags": [ - "admin/sessions" + "widget" ], - "summary": "Force delete session", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Get widget settings", + "description": "Public Management API. Organization API-key callers require `widget:create`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "widget:create", "security": [ + { + "ApiKeyAuth": [] + }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "sessionId", - "in": "path", - "required": true, - "description": "Database session UUID or stable session slug.", + "name": "projectId", + "in": "query", + "required": false, "schema": { - "$ref": "#/components/schemas/SessionIdentifier" + "type": "string" } } ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7540,33 +10583,24 @@ "$ref": "#/components/responses/NotImplemented" } } - } - }, - "/admin/sessions/{sessionId}/comp": { - "post": { - "operationId": "adminCompSessionDaySession", - "tags": [ - "admin/sessions" + }, + "put": { + "operationId": "updateWidgetSettings", + "tags": [ + "widget" ], - "summary": "Comp session day session", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "summary": "Update widget settings", + "description": "Updates the widget settings for the authenticated organization and project.\n\nOrganization API-key callers require `widget:create`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", + "x-audience": "public", + "x-polymorfa-resource-boundary": "project", + "x-excluded-from-public": false, + "x-required-scope": "widget:create", "security": [ { - "ConsoleSession": [] - } - ], - "parameters": [ + "ApiKeyAuth": [] + }, { - "name": "sessionId", - "in": "path", - "required": true, - "description": "Database session UUID or stable session slug.", - "schema": { - "$ref": "#/components/schemas/SessionIdentifier" - } + "ConsoleSession": [] } ], "requestBody": { @@ -7574,8 +10608,7 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/UpdateWidgetSettingsRequest" } } } @@ -7583,45 +10616,95 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "data" + ] } } } }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" } } } }, - "/admin/campaigns": { - "get": { - "operationId": "adminListCampaigns", + "/console/keys": { + "post": { + "operationId": "mintForConsoleApiKey", "tags": [ - "admin/campaigns" + "keys" ], - "summary": "List campaigns", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Mint for console api key", + "description": "**EXCLUDED from the public API** (`console`). Credential red line: a pmfa_ key must never mint a pmfa_ key. Human session only — keeps revocation meaningful.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Credential red line: a pmfa_ key must never mint a pmfa_ key. Human session only — keeps revocation meaningful.", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7642,32 +10725,23 @@ } } }, - "/admin/campaigns/{campaignId}/pause": { + "/console/billing/topup": { "post": { - "operationId": "adminForcePauseCampaign", + "operationId": "topUpBilling", "tags": [ - "admin/campaigns" + "billing" ], - "summary": "Force pause campaign", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Top up billing", + "description": "**EXCLUDED from the public API** (`console`). Starts a Stripe Checkout browser redirect; human session only. API clients use auto-top-up.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Starts a Stripe Checkout browser redirect; human session only. API clients use auto-top-up.", "security": [ { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "campaignId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], "requestBody": { "required": false, "content": { @@ -7682,6 +10756,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7702,17 +10790,18 @@ } } }, - "/admin/campaigns/{campaignId}/stop": { - "post": { - "operationId": "adminForceStopCampaign", + "/console/organizations/{slug}": { + "get": { + "operationId": "getOrganizationBySlug", "tags": [ - "admin/campaigns" + "organizations" ], - "summary": "Force stop campaign", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Get organization by slug", + "description": "**EXCLUDED from the public API** (`console`). Console org-switcher lookup; a key is already bound to one org.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Console org-switcher lookup; a key is already bound to one org.", "security": [ { "ConsoleSession": [] @@ -7720,7 +10809,7 @@ ], "parameters": [ { - "name": "campaignId", + "name": "slug", "in": "path", "required": true, "schema": { @@ -7728,20 +10817,23 @@ } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7762,25 +10854,51 @@ } } }, - "/admin/campaigns/optouts": { - "get": { - "operationId": "adminOptOutLeaderboardCampaign", + "/console/organization/avatar/uploads": { + "post": { + "operationId": "generateOrganizationAvatarUploadUrl", "tags": [ - "admin/campaigns" + "organization" ], - "summary": "Opt out leaderboard campaign", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Generate organization avatar upload url", + "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Browser logo-upload flow.", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7801,64 +10919,51 @@ } } }, - "/admin/billing/revenue": { - "get": { - "operationId": "adminPlatformRevenueBilling", + "/console/organization/avatar": { + "put": { + "operationId": "saveAvatarOrganization", "tags": [ - "admin/billing" + "organization" ], - "summary": "Platform revenue billing", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Save avatar organization", + "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Browser logo-upload flow.", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/admin/billing/leaderboard": { - "get": { - "operationId": "adminTopOrgsByRevenueBilling", - "tags": [ - "admin/billing" - ], - "summary": "Top orgs by revenue billing", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "security": [ - { - "ConsoleSession": [] - } - ], - "responses": { - "200": { - "description": "Success", + }, "content": { "application/json": { "schema": { @@ -7877,19 +10982,18 @@ "$ref": "#/components/responses/NotImplemented" } } - } - }, - "/admin/billing/lowbalance": { - "get": { - "operationId": "adminLowBalanceOrgsBilling", + }, + "delete": { + "operationId": "removeAvatarOrganization", "tags": [ - "admin/billing" + "organization" ], - "summary": "Low balance orgs billing", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Remove avatar organization", + "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Browser logo-upload flow.", "security": [ { "ConsoleSession": [] @@ -7898,6 +11002,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7918,17 +11036,18 @@ } } }, - "/admin/billing/credits": { + "/console/organizations/ensure": { "post": { - "operationId": "adminCompCreditsBilling", + "operationId": "ensureForCurrentUserOrganization", "tags": [ - "admin/billing" + "organizations" ], - "summary": "Comp credits billing", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Ensure for current user organization", + "description": "**EXCLUDED from the public API** (`console`). First-sign-in bootstrap for the session user.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "First-sign-in bootstrap for the session user.", "security": [ { "ConsoleSession": [] @@ -7948,6 +11067,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -7968,22 +11101,34 @@ } } }, - "/admin/billing/refunds": { + "/console/projects/{projectId}/icon": { "post": { - "operationId": "adminRefundBilling", + "operationId": "generateProjectUploadUrl", "tags": [ - "admin/billing" + "projects" ], - "summary": "Refund billing", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Generate project upload url", + "description": "**EXCLUDED from the public API** (`console`). Browser icon-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Browser icon-upload flow.", "security": [ { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "projectId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], "requestBody": { "required": false, "content": { @@ -7998,6 +11143,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8018,17 +11177,18 @@ } } }, - "/admin/keys": { + "/console/profile": { "get": { - "operationId": "adminListApiKeys", + "operationId": "getUserProfile", "tags": [ - "admin/keys" + "profile" ], - "summary": "List api keys", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Get user profile", + "description": "**EXCLUDED from the public API** (`console`). 'Current user' only exists for a session; keys are org-scoped, not user-scoped.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "'Current user' only exists for a session; keys are org-scoped, not user-scoped.", "security": [ { "ConsoleSession": [] @@ -8037,6 +11197,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8055,37 +11229,51 @@ "$ref": "#/components/responses/NotImplemented" } } - } - }, - "/admin/keys/{keyId}": { - "delete": { - "operationId": "adminRevokeApiKeyApiKey", + }, + "patch": { + "operationId": "updateUserProfileField", "tags": [ - "admin/keys" + "profile" ], - "summary": "Revoke api key api key", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Update user profile field", + "description": "**EXCLUDED from the public API** (`console`). Personal profile edit for the signed-in user.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Personal profile edit for the signed-in user.", "security": [ { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "keyId", - "in": "path", - "required": true, - "schema": { - "type": "string" + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } } } - ], + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8106,30 +11294,56 @@ } } }, - "/admin/tenants": { - "get": { - "operationId": "adminListTenants", + "/console/profile/ensure": { + "post": { + "operationId": "ensureProfileUserProfile", "tags": [ - "admin/tenants" + "profile" ], - "summary": "List tenants", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Ensure profile user profile", + "description": "**EXCLUDED from the public API** (`console`). Sign-in bootstrap.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Sign-in bootstrap.", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } } } }, @@ -8145,51 +11359,40 @@ } } }, - "/admin/tenants/{tenantId}": { - "patch": { - "operationId": "adminSetTenantActive", + "/console/teams": { + "get": { + "operationId": "listUserProfileTeams", "tags": [ - "admin/tenants" + "teams" ], - "summary": "Set tenant active", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "List user profile teams", + "description": "**EXCLUDED from the public API** (`console`). Session user's team switcher.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-merged-endpoints": [ - "adminUpdateTenantRateLimits", - "adminUpdateTenantLogLevel", - "adminToggleDebugModeTenant" - ], + "x-exclusion-reason": "Session user's team switcher.", "security": [ { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "tenantId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8210,25 +11413,51 @@ } } }, - "/admin/pricing": { - "get": { - "operationId": "adminListPricing", + "/console/profile/avatar/uploads": { + "post": { + "operationId": "generateUserProfileAvatarUploadUrl", "tags": [ - "admin/pricing" + "profile" ], - "summary": "List pricing", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Generate user profile avatar upload url", + "description": "**EXCLUDED from the public API** (`console`). Browser avatar-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Browser avatar-upload flow.", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8249,32 +11478,23 @@ } } }, - "/admin/pricing/{tier}": { + "/console/profile/avatar": { "put": { - "operationId": "adminUpdatePricing", + "operationId": "saveAvatarUserProfile", "tags": [ - "admin/pricing" + "profile" ], - "summary": "Update pricing", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Save avatar user profile", + "description": "**EXCLUDED from the public API** (`console`). Browser avatar-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Browser avatar-upload flow.", "security": [ { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "tier", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], "requestBody": { "required": false, "content": { @@ -8289,6 +11509,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8308,45 +11542,39 @@ } } }, - "patch": { - "operationId": "adminSetPricingActive", + "delete": { + "operationId": "removeAvatarUserProfile", "tags": [ - "admin/pricing" + "profile" ], - "summary": "Set pricing active", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Remove avatar user profile", + "description": "**EXCLUDED from the public API** (`console`). Personal profile edit.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Personal profile edit.", "security": [ { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "tier", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8367,17 +11595,18 @@ } } }, - "/admin/organization-invitations": { + "/console/auth/methods": { "get": { - "operationId": "adminListOrganizationInvitations", + "operationId": "getUserProfileAuthMethods", "tags": [ - "admin/organization-invitations" + "auth" ], - "summary": "List organization invitations", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Get user profile auth methods", + "description": "**EXCLUDED from the public API** (`console`). Account-security introspection for the session user.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Account-security introspection for the session user.", "security": [ { "ConsoleSession": [] @@ -8386,6 +11615,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8404,17 +11647,20 @@ "$ref": "#/components/responses/NotImplemented" } } - }, + } + }, + "/console/auth/totp": { "post": { - "operationId": "adminCreateOrganizationInvitation", + "operationId": "enrollTotpUserProfile", "tags": [ - "admin/organization-invitations" + "auth" ], - "summary": "Create organization invitation", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Enroll totp user profile", + "description": "**EXCLUDED from the public API** (`console`). MFA enrollment via API key = account-takeover vector.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "MFA enrollment via API key = account-takeover vector.", "security": [ { "ConsoleSession": [] @@ -8434,55 +11680,20 @@ "responses": { "200": { "description": "Success", - "content": { - "application/json": { + "headers": { + "X-Request-Id": { "schema": { - "$ref": "#/components/schemas/DataEnvelope" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "501": { - "$ref": "#/components/responses/NotImplemented" - } - } - } - }, - "/admin/organization-invitations/{invitationId}": { - "delete": { - "operationId": "adminRevokeOrganizationInvitation", - "tags": [ - "admin/organization-invitations" - ], - "summary": "Revoke organization invitation", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", - "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "security": [ - { - "ConsoleSession": [] - } - ], - "parameters": [ - { - "name": "invitationId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Success", + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8503,25 +11714,51 @@ } } }, - "/admin/webhooks": { - "get": { - "operationId": "adminListWebhooks", + "/console/auth/totp/verify": { + "post": { + "operationId": "verifyTotpUserProfile", "tags": [ - "admin/webhooks" + "auth" ], - "summary": "List webhooks", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Verify totp user profile", + "description": "**EXCLUDED from the public API** (`console`). Part of the human MFA step-up flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Part of the human MFA step-up flow.", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8542,17 +11779,18 @@ } } }, - "/admin/webhooks/{webhookId}/disable": { - "post": { - "operationId": "adminForceDisableWebhook", + "/console/auth/factors/{factorId}": { + "delete": { + "operationId": "deleteUserProfileAuthFactor", "tags": [ - "admin/webhooks" + "auth" ], - "summary": "Force disable webhook", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Delete user profile auth factor", + "description": "**EXCLUDED from the public API** (`console`). Removing MFA via API key = account-takeover vector.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Removing MFA via API key = account-takeover vector.", "security": [ { "ConsoleSession": [] @@ -8560,29 +11798,31 @@ ], "parameters": [ { - "name": "webhookId", + "name": "factorId", "in": "path", "required": true, "schema": { - "type": "string", - "format": "uuid" + "type": "string" } } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8603,17 +11843,18 @@ } } }, - "/admin/audit": { + "/console/auth/providers": { "get": { - "operationId": "adminListAuditLogGlobal", + "operationId": "getUserProfileProviderConnectionUrl", "tags": [ - "admin/audit" + "auth" ], - "summary": "List audit log global", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Get user profile provider connection url", + "description": "**EXCLUDED from the public API** (`console`). Deferred Better Auth provider-link flow; dashboard identity only.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Deferred Better Auth provider-link flow; dashboard identity only.", "security": [ { "ConsoleSession": [] @@ -8622,6 +11863,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8642,25 +11897,50 @@ } } }, - "/admin/audit/actions": { - "get": { - "operationId": "adminListAuditLogAdminActions", + "/console/auth/providers/{provider}": { + "delete": { + "operationId": "disconnectProviderUserProfile", "tags": [ - "admin/audit" + "auth" ], - "summary": "List audit log admin actions", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Disconnect provider user profile", + "description": "**EXCLUDED from the public API** (`console`). Deferred Better Auth provider-unlink flow; dashboard identity only.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Deferred Better Auth provider-unlink flow; dashboard identity only.", "security": [ { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "provider", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8681,25 +11961,51 @@ } } }, - "/admin/kpis": { - "get": { - "operationId": "adminPlatformKpisPlatform", + "/console/teams/leave": { + "post": { + "operationId": "leaveTeamUserProfile", "tags": [ - "admin/kpis" + "teams" ], - "summary": "Platform kpis platform", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Leave team user profile", + "description": "**EXCLUDED from the public API** (`console`). Acts on the session user's own membership.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Acts on the session user's own membership.", "security": [ { "ConsoleSession": [] } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8720,25 +12026,61 @@ } } }, - "/admin/signups": { - "get": { - "operationId": "adminRecentSignupsPlatform", + "/console/teams/{teamId}/delete": { + "post": { + "operationId": "deleteUserProfileTeam", "tags": [ - "admin/signups" + "teams" ], - "summary": "Recent signups platform", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "Delete user profile team", + "description": "**EXCLUDED from the public API** (`console`). Destructive, human-confirmed flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "Destructive, human-confirmed flow.", "security": [ { "ConsoleSession": [] } ], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8759,36 +12101,40 @@ } } }, - "/admin/sync": { - "post": { - "operationId": "adminEnsureCurrentUserStaffSyncStaffSync", + "/console/notifications": { + "get": { + "operationId": "listNotificationInbox", "tags": [ - "admin/sync" + "notifications" ], - "summary": "Ensure current user staff sync staff sync", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "List notification inbox", + "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "User-scoped console inbox.", "security": [ { "ConsoleSession": [] } ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8809,17 +12155,18 @@ } } }, - "/admin/bans": { + "/console/notifications/archived": { "get": { - "operationId": "adminListRecentSessionBans", + "operationId": "listNotificationArchived", "tags": [ - "admin/bans" + "notifications" ], - "summary": "List recent session bans", - "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", - "x-audience": "staff", + "summary": "List notification archived", + "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", "x-excluded-from-public": true, - "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-exclusion-reason": "User-scoped console inbox.", "security": [ { "ConsoleSession": [] @@ -8828,6 +12175,20 @@ "responses": { "200": { "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -8848,37 +12209,23 @@ } } }, - "/v1/projects/{projectId}/customers/status": { + "/console/notifications/count": { "get": { - "operationId": "getCustomersStatus", + "operationId": "unreadCountNotification", "tags": [ - "customers" + "notifications" ], - "summary": "Get Customers status", - "description": "Returns whether Customers is enabled for the exact project and its protected default Customer when enabled.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:read", + "summary": "Unread count notification", + "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "User-scoped console inbox.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "projectId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], "responses": { "200": { "description": "Success", @@ -8899,76 +12246,61 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/CustomersStatus" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/projects/{projectId}/customers/enable": { + "/console/notifications/{notificationId}/read": { "post": { - "operationId": "enableCustomers", + "operationId": "markReadNotification", "tags": [ - "customers" + "notifications" ], - "summary": "Enable Customers", - "description": "Enables Customers once, creates the default Customer, and assigns every existing Number in the project to it atomically.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "Mark read notification", + "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "User-scoped console inbox.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "projectId", + "name": "notificationId", "in": "path", "required": true, "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": true, - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255 + "type": "string" } } ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, "responses": { "200": { "description": "Success", @@ -8989,133 +12321,51 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/CustomersEnablement" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/customers": { - "get": { - "operationId": "listCustomers", + "/console/notifications/read": { + "post": { + "operationId": "markAllReadNotification", "tags": [ - "customers" + "notifications" ], - "summary": "List Customers", - "description": "Lists masked Customer summaries in the exact authenticated project with cursor pagination and filters.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:read", + "summary": "Mark all read notification", + "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "User-scoped console inbox.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "projectId", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "cursor", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 25 - } - }, - { - "name": "search", - "in": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "name": "status", - "in": "query", - "required": false, - "schema": { - "type": "string", - "enum": [ - "active", - "archiving", - "archived", - "all" - ] - } - }, - { - "name": "isDefault", - "in": "query", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "hasNumbers", - "in": "query", - "required": false, - "schema": { - "type": "boolean" - } - }, - { - "name": "needsAttention", - "in": "query", - "required": false, - "schema": { - "type": "boolean" + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } } } - ], + }, "responses": { "200": { "description": "Success", @@ -9136,69 +12386,47 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerSummary" - } - }, - "page": { - "$ref": "#/components/schemas/CursorPageMetadata" - } - }, - "required": [ - "data", - "page" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" + "501": { + "$ref": "#/components/responses/NotImplemented" } } - }, + } + }, + "/console/notifications/{notificationId}/archive": { "post": { - "operationId": "createCustomer", + "operationId": "archiveNotification", "tags": [ - "customers" + "notifications" ], - "summary": "Create a Customer", - "description": "Creates a Customer with optional name, phone, and caller-defined external identifier.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "Archive notification", + "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "User-scoped console inbox.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "Idempotency-Key", - "in": "header", + "name": "notificationId", + "in": "path", "required": true, "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255 + "type": "string" } } ], @@ -9207,7 +12435,8 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateCustomerRequest" + "type": "object", + "additionalProperties": true } } } @@ -9232,77 +12461,51 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/Customer" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/customers/{customerId}": { - "get": { - "operationId": "getCustomer", + "/console/notifications/archive": { + "post": { + "operationId": "archiveAllNotification", "tags": [ - "customers" + "notifications" ], - "summary": "Get a Customer", - "description": "Returns one Customer from the exact authenticated project.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:read", + "summary": "Archive all notification", + "description": "**EXCLUDED from the public API** (`console`). User-scoped console inbox.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "User-scoped console inbox.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "projectId", - "in": "query", - "required": true, - "schema": { - "type": "string" + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } } } - ], + }, "responses": { "200": { "description": "Success", @@ -9323,74 +12526,40 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/Customer" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" + "501": { + "$ref": "#/components/responses/NotImplemented" } } - }, - "patch": { - "operationId": "updateCustomer", + } + }, + "/console/notifications/preferences": { + "get": { + "operationId": "getNotificationPreferences", "tags": [ - "customers" + "notifications" ], - "summary": "Update a Customer", - "description": "Updates supplied Customer profile fields. Null clears an optional field.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "Get notification preferences", + "description": "**EXCLUDED from the public API** (`console`). Per-user notification preferences.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Per-user notification preferences.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateCustomerRequest" - } - } - } - }, "responses": { "200": { "description": "Success", @@ -9411,85 +12580,45 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/Customer" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" + "501": { + "$ref": "#/components/responses/NotImplemented" } } - } - }, - "/v1/customers/{customerId}/archive": { - "post": { - "operationId": "archiveCustomer", + }, + "put": { + "operationId": "updateNotificationPreferences", "tags": [ - "customers" + "notifications" ], - "summary": "Archive a Customer", - "description": "Archives a non-default Customer. The default Customer cannot be archived.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "Update notification preferences", + "description": "**EXCLUDED from the public API** (`console`). Per-user notification preferences.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Per-user notification preferences.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": true, - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255 - } - } - ], "requestBody": { "required": false, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerProjectRequest" + "type": "object", + "additionalProperties": true } } } @@ -9514,85 +12643,47 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/Customer" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/customers/{customerId}/restore": { + "/console/notifications/test": { "post": { - "operationId": "restoreCustomer", + "operationId": "sendTestNotificationEmail", "tags": [ - "customers" + "notifications" ], - "summary": "Restore a Customer", - "description": "Restores an archived Customer.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "Send test notification email", + "description": "**EXCLUDED from the public API** (`console`). Console settings test button.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Console settings test button.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": true, - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255 - } - } - ], "requestBody": { "required": false, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerProjectRequest" + "type": "object", + "additionalProperties": true } } } @@ -9617,77 +12708,51 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/Customer" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/customers/{customerId}/numbers": { - "get": { - "operationId": "listCustomerNumbers", + "/console/widget/logo/uploads": { + "post": { + "operationId": "generateWidgetSettingsLogoUploadUrl", "tags": [ - "customers" + "widget" ], - "summary": "List Customer Numbers", - "description": "Lists Numbers assigned to one Customer in the exact project.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:read", + "summary": "Generate widget settings logo upload url", + "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Browser logo-upload flow.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "projectId", - "in": "query", - "required": true, - "schema": { - "type": "string" + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } } } - ], + }, "responses": { "200": { "description": "Success", @@ -9708,86 +12773,101 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerNumber" - } - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/customers/{customerId}/events": { - "get": { - "operationId": "listCustomerEvents", + "/console/widget/logo": { + "put": { + "operationId": "attachLogoWidgetSettings", "tags": [ - "customers" + "widget" ], - "summary": "List recent Customer events", - "description": "Lists the newest Customer lifecycle events with safe metadata. Actor identities, message content, phone values, invitation tokens, and URLs are excluded.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:read", + "summary": "Attach logo widget settings", + "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Browser logo-upload flow.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } } - }, - { - "name": "projectId", - "in": "query", - "required": true, - "schema": { - "type": "string" + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } } }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + }, + "delete": { + "operationId": "removeLogoWidgetSettings", + "tags": [ + "widget" + ], + "summary": "Remove logo widget settings", + "description": "**EXCLUDED from the public API** (`console`). Browser logo-upload flow.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Browser logo-upload flow.", + "security": [ { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 25 - } + "ConsoleSession": [] } ], "responses": { @@ -9810,85 +12890,47 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerEvent" - } - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/customers/{customerId}/pairing-links": { + "/console/widget/logo/org": { "post": { - "operationId": "createCustomerPairingLink", + "operationId": "useOrgLogoWidgetSettings", "tags": [ - "customers" + "widget" ], - "summary": "Create a Customer pairing link", - "description": "Creates a revocable Customer invitation. The raw URL is returned only in this response.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "Use org logo widget settings", + "description": "**EXCLUDED from the public API** (`console`). Console snapshot flow (copies org logo).", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Console snapshot flow (copies org logo).", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": true, - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255 - } - } - ], "requestBody": { "required": false, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateCustomerPairingLinkRequest" + "type": "object", + "additionalProperties": true } } } @@ -9913,75 +12955,51 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "$ref": "#/components/schemas/CreatedCustomerPairingLink" - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" + "501": { + "$ref": "#/components/responses/NotImplemented" } } - }, - "get": { - "operationId": "listCustomerPairingLinks", + } + }, + "/console/widget/logo/project": { + "post": { + "operationId": "useProjectIconWidgetSettings", "tags": [ - "customers" + "widget" ], - "summary": "List Customer pairing links", - "description": "Lists safe pairing-link metadata. Raw invitation tokens and URLs are never returned.\n\nOrganization API-key callers require `customers:read`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:read", + "summary": "Use project icon widget settings", + "description": "**EXCLUDED from the public API** (`console`). Console snapshot flow (copies project icon).", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Console snapshot flow (copies project icon).", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "projectId", - "in": "query", - "required": true, - "schema": { - "type": "string" + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } } } - ], + }, "responses": { "200": { "description": "Success", @@ -10002,85 +13020,112 @@ "content": { "application/json": { "schema": { - "type": "object", - "additionalProperties": false, - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CustomerPairingLink" - } - } - }, - "required": [ - "data" - ] + "$ref": "#/components/schemas/DataEnvelope" } } } }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }, - "404": { - "$ref": "#/components/responses/NotFound" + "501": { + "$ref": "#/components/responses/NotImplemented" } } } }, - "/v1/customers/{customerId}/pairing-links/{pairingLinkId}": { - "delete": { - "operationId": "revokeCustomerPairingLink", + "/console/webhooks": { + "get": { + "operationId": "listWebhooks", "tags": [ - "customers" + "webhooks" ], - "summary": "Revoke a Customer pairing link", - "description": "Revokes an open pairing link.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "List webhook endpoints", + "description": "Lists sanitized webhook endpoint settings for the authenticated organization.\n\nRequires a verified dashboard bearer and current organization authorization. Organization keys and project tokens are not accepted.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], - "parameters": [ - { - "name": "customerId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConsoleWebhook" + } + } + }, + "required": [ + "data" + ] + } + } } }, - { - "name": "pairingLinkId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "401": { + "$ref": "#/components/responses/Unauthorized" }, + "403": { + "$ref": "#/components/responses/Forbidden" + } + } + }, + "post": { + "operationId": "createWebhook", + "tags": [ + "webhooks" + ], + "summary": "Create a webhook endpoint", + "description": "Creates a webhook endpoint for the authenticated organization. Requires live owner or admin authority.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", + "x-required-organization-role": "owner_or_admin", + "security": [ { - "name": "projectId", - "in": "query", - "required": true, - "schema": { - "type": "string" - } + "ConsoleSession": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateConsoleWebhookRequest" + } + } + } + }, "responses": { "200": { "description": "Success", @@ -10105,7 +13150,7 @@ "additionalProperties": false, "properties": { "data": { - "$ref": "#/components/schemas/CustomerPairingLink" + "$ref": "#/components/schemas/ConsoleWebhook" } }, "required": [ @@ -10123,64 +13168,37 @@ }, "403": { "$ref": "#/components/responses/Forbidden" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" } } } }, - "/v1/customers/{customerId}/numbers/{sessionId}/transfer": { - "post": { - "operationId": "transferCustomerNumber", + "/console/webhooks/{webhookId}": { + "patch": { + "operationId": "updateWebhook", "tags": [ - "customers" + "webhooks" ], - "summary": "Transfer a Customer Number", - "description": "Atomically transfers one eligible Number from the explicitly confirmed source Customer to the destination Customer.\n\nOrganization API-key callers require `customers:manage`; project-token callers require the same action scope and are accepted only for `project` resources inside the authenticated project; dashboard callers require current organization membership and authorization.", - "x-audience": "public", - "x-polymorfa-resource-boundary": "project", - "x-excluded-from-public": false, - "x-required-scope": "customers:manage", + "summary": "Update a webhook endpoint", + "description": "Updates a webhook endpoint in the authenticated organization. Requires live owner or admin authority.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", + "x-required-organization-role": "owner_or_admin", "security": [ - { - "ApiKeyAuth": [] - }, { "ConsoleSession": [] } ], "parameters": [ { - "name": "customerId", + "name": "webhookId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } - }, - { - "name": "sessionId", - "in": "path", - "required": true, - "description": "Database session UUID or stable session slug.", - "schema": { - "$ref": "#/components/schemas/SessionIdentifier" - } - }, - { - "name": "Idempotency-Key", - "in": "header", - "required": true, - "schema": { - "type": "string", - "minLength": 1, - "maxLength": 255 - } } ], "requestBody": { @@ -10188,7 +13206,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TransferCustomerNumberRequest" + "$ref": "#/components/schemas/UpdateConsoleWebhookRequest" } } } @@ -10217,7 +13235,7 @@ "additionalProperties": false, "properties": { "data": { - "$ref": "#/components/schemas/CustomerNumber" + "$ref": "#/components/schemas/ConsoleWebhook" } }, "required": [ @@ -10238,181 +13256,6234 @@ }, "404": { "$ref": "#/components/responses/NotFound" - }, - "409": { - "$ref": "#/components/responses/Conflict" } } - } - } - }, - "components": { - "securitySchemes": { - "ApiKeyAuth": { - "type": "http", - "scheme": "bearer", - "description": "Polymorfa server credential: a scoped pmfa_ organization key. Public /v1 operations reject pmfa_pt_ project tokens." }, - "ConsoleSession": { - "type": "http", - "scheme": "bearer", - "description": "Verified dashboard JWT for signed-in Console requests. This is not a server credential." - } - }, - "schemas": { - "DataEnvelope": { - "type": "object", - "properties": { - "data": { - "description": "Operation result." + "delete": { + "operationId": "deleteWebhook", + "tags": [ + "webhooks" + ], + "summary": "Delete a webhook endpoint", + "description": "Deletes a webhook endpoint in the authenticated organization. Requires live owner or admin authority.\n\nRequires a verified dashboard bearer and current organization authorization. The signed-in member must be an organization owner or admin. Organization keys and project tokens are not accepted.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Dashboard webhook settings for the authenticated organization.", + "x-required-organization-role": "owner_or_admin", + "security": [ + { + "ConsoleSession": [] } - }, - "required": [ - "data" - ] - }, - "ErrorEnvelope": { - "type": "object", - "properties": { - "error": { - "type": "object", - "properties": { - "code": { - "type": "string" + ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." }, - "message": { - "type": "string" + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." } }, - "required": [ - "code" - ] + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": false, + "properties": { + "data": { + "$ref": "#/components/schemas/DeleteConsoleWebhookResult" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" } - }, - "required": [ - "error" - ] - }, + } + } + }, + "/console/sandbox/pool": { + "get": { + "operationId": "getSandboxProjectPool", + "tags": [ + "sandbox" + ], + "summary": "Get sandbox project project pool", + "description": "**EXCLUDED from the public API** (`console`). Console/sandbox internal wiring.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Console/sandbox internal wiring.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/console/deployments": { + "get": { + "operationId": "listDeployments", + "tags": [ + "deployments" + ], + "summary": "List deployments", + "description": "**EXCLUDED from the public API** (`console`). Legacy console surface; not a customer contract.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Legacy console surface; not a customer contract.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/console/insights/logs": { + "get": { + "operationId": "logsInsight", + "tags": [ + "insights" + ], + "summary": "Logs insight", + "description": "**EXCLUDED from the public API** (`console`). Customer console observability for the selected organization and project.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Customer console observability for the selected organization and project.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "start", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "end", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/console/insights/usage": { + "get": { + "operationId": "usageInsight", + "tags": [ + "insights" + ], + "summary": "Usage insight", + "description": "**EXCLUDED from the public API** (`console`). Customer console usage and entitlement presentation.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Customer console usage and entitlement presentation.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "start", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "end", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/console/insights/analytics": { + "get": { + "operationId": "analyticsInsight", + "tags": [ + "insights" + ], + "summary": "Analytics insight", + "description": "**EXCLUDED from the public API** (`console`). Customer console aggregate product analytics; message bodies are never returned.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Customer console aggregate product analytics; message bodies are never returned.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "projectId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "start", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "end", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/console/insights/preferences": { + "get": { + "operationId": "getInsightPreferences", + "tags": [ + "insights" + ], + "summary": "Get insight preferences", + "description": "**EXCLUDED from the public API** (`console`). Organization analytics-consent controls for the console.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Organization analytics-consent controls for the console.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + }, + "put": { + "operationId": "updateInsightPreferences", + "tags": [ + "insights" + ], + "summary": "Update insight preferences", + "description": "**EXCLUDED from the public API** (`console`). Organization analytics-consent controls for the console.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Organization analytics-consent controls for the console.", + "security": [ + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/console/organization-invitations/{invitationId}/accept": { + "post": { + "operationId": "acceptOrganizationInvitation", + "tags": [ + "organization-invitations" + ], + "summary": "Accept organization invitation", + "description": "**EXCLUDED from the public API** (`console`). Deferred Better Auth invitation-acceptance flow; dashboard identity only.", + "x-audience": "console", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Deferred Better Auth invitation-acceptance flow; dashboard identity only.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "invitationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/orgs": { + "get": { + "operationId": "adminListOrganizations", + "tags": [ + "admin/orgs" + ], + "summary": "List organizations", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/orgs/{orgId}": { + "get": { + "operationId": "adminGetOrganization", + "tags": [ + "admin/orgs" + ], + "summary": "Get organization", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + }, + "patch": { + "operationId": "adminRenameOrganization", + "tags": [ + "admin/orgs" + ], + "summary": "Rename organization", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + }, + "delete": { + "operationId": "adminCascadeDeleteOrganization", + "tags": [ + "admin/orgs" + ], + "summary": "Cascade delete organization", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/orgs/{orgId}/freeze": { + "post": { + "operationId": "adminFreezeOrganization", + "tags": [ + "admin/orgs" + ], + "summary": "Freeze organization", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/orgs/{orgId}/unfreeze": { + "post": { + "operationId": "adminUnfreezeOrganization", + "tags": [ + "admin/orgs" + ], + "summary": "Unfreeze organization", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/orgs/features": { + "get": { + "operationId": "adminListOrganizationFeatureGroups", + "tags": [ + "admin/orgs" + ], + "summary": "List organization feature groups", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/orgs/{orgId}/features": { + "patch": { + "operationId": "adminSetOrganizationFeatureGroups", + "tags": [ + "admin/orgs" + ], + "summary": "Set organization feature groups", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "orgId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/users": { + "get": { + "operationId": "adminSearchUser", + "tags": [ + "admin/users" + ], + "summary": "Search user", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/users/{userId}": { + "get": { + "operationId": "adminGetUser", + "tags": [ + "admin/users" + ], + "summary": "Get user", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/users/{userId}/onboarding": { + "delete": { + "operationId": "adminResetOnboardingUser", + "tags": [ + "admin/users" + ], + "summary": "Reset onboarding user", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/users/{userId}/suspend": { + "post": { + "operationId": "adminSuspendUser", + "tags": [ + "admin/users" + ], + "summary": "Suspend user user", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/users/{userId}/unsuspend": { + "post": { + "operationId": "adminUnsuspendUser", + "tags": [ + "admin/users" + ], + "summary": "Unsuspend user user", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/users/{userId}/signout": { + "post": { + "operationId": "adminForceSignOutUser", + "tags": [ + "admin/users" + ], + "summary": "Force sign out user", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/members/{memberId}": { + "patch": { + "operationId": "adminSetMemberRole", + "tags": [ + "admin/members" + ], + "summary": "Set member role", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "memberId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + }, + "delete": { + "operationId": "adminRemoveMember", + "tags": [ + "admin/members" + ], + "summary": "Remove member member", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "memberId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/members/invites": { + "post": { + "operationId": "adminInviteMember", + "tags": [ + "admin/members" + ], + "summary": "Invite member member", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/sessions": { + "get": { + "operationId": "adminListSessions", + "tags": [ + "admin/sessions" + ], + "summary": "List sessions", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/sessions/{sessionId}/stop": { + "post": { + "operationId": "adminForceStopSession", + "tags": [ + "admin/sessions" + ], + "summary": "Force stop session", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "Database session UUID or stable session slug.", + "schema": { + "$ref": "#/components/schemas/SessionIdentifier" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/sessions/{sessionId}": { + "delete": { + "operationId": "adminForceDeleteSession", + "tags": [ + "admin/sessions" + ], + "summary": "Force delete session", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "Database session UUID or stable session slug.", + "schema": { + "$ref": "#/components/schemas/SessionIdentifier" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/sessions/{sessionId}/comp": { + "post": { + "operationId": "adminCompSessionDaySession", + "tags": [ + "admin/sessions" + ], + "summary": "Comp session day session", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "Database session UUID or stable session slug.", + "schema": { + "$ref": "#/components/schemas/SessionIdentifier" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/campaigns": { + "get": { + "operationId": "adminListCampaigns", + "tags": [ + "admin/campaigns" + ], + "summary": "List campaigns", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/campaigns/{campaignId}/pause": { + "post": { + "operationId": "adminForcePauseCampaign", + "tags": [ + "admin/campaigns" + ], + "summary": "Force pause campaign", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "campaignId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/campaigns/{campaignId}/stop": { + "post": { + "operationId": "adminForceStopCampaign", + "tags": [ + "admin/campaigns" + ], + "summary": "Force stop campaign", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "campaignId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/campaigns/optouts": { + "get": { + "operationId": "adminOptOutLeaderboardCampaign", + "tags": [ + "admin/campaigns" + ], + "summary": "Opt out leaderboard campaign", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/billing/revenue": { + "get": { + "operationId": "adminPlatformRevenueBilling", + "tags": [ + "admin/billing" + ], + "summary": "Platform revenue billing", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/billing/leaderboard": { + "get": { + "operationId": "adminTopOrgsByRevenueBilling", + "tags": [ + "admin/billing" + ], + "summary": "Top orgs by revenue billing", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/billing/lowbalance": { + "get": { + "operationId": "adminLowBalanceOrgsBilling", + "tags": [ + "admin/billing" + ], + "summary": "Low balance orgs billing", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/billing/credits": { + "post": { + "operationId": "adminCompCreditsBilling", + "tags": [ + "admin/billing" + ], + "summary": "Comp credits billing", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/billing/refunds": { + "post": { + "operationId": "adminRefundBilling", + "tags": [ + "admin/billing" + ], + "summary": "Refund billing", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/keys": { + "get": { + "operationId": "adminListApiKeys", + "tags": [ + "admin/keys" + ], + "summary": "List api keys", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/keys/{keyId}": { + "delete": { + "operationId": "adminRevokeApiKeyApiKey", + "tags": [ + "admin/keys" + ], + "summary": "Revoke api key api key", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "keyId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/tenants": { + "get": { + "operationId": "adminListTenants", + "tags": [ + "admin/tenants" + ], + "summary": "List tenants", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/tenants/{tenantId}": { + "patch": { + "operationId": "adminSetTenantActive", + "tags": [ + "admin/tenants" + ], + "summary": "Set tenant active", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-merged-endpoints": [ + "adminUpdateTenantRateLimits", + "adminUpdateTenantLogLevel", + "adminToggleDebugModeTenant" + ], + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "tenantId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/pricing": { + "get": { + "operationId": "adminListPricing", + "tags": [ + "admin/pricing" + ], + "summary": "List pricing", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/pricing/{tier}": { + "put": { + "operationId": "adminUpdatePricing", + "tags": [ + "admin/pricing" + ], + "summary": "Update pricing", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "tier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + }, + "patch": { + "operationId": "adminSetPricingActive", + "tags": [ + "admin/pricing" + ], + "summary": "Set pricing active", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "tier", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/organization-invitations": { + "get": { + "operationId": "adminListOrganizationInvitations", + "tags": [ + "admin/organization-invitations" + ], + "summary": "List organization invitations", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + }, + "post": { + "operationId": "adminCreateOrganizationInvitation", + "tags": [ + "admin/organization-invitations" + ], + "summary": "Create organization invitation", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/organization-invitations/{invitationId}": { + "delete": { + "operationId": "adminRevokeOrganizationInvitation", + "tags": [ + "admin/organization-invitations" + ], + "summary": "Revoke organization invitation", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "invitationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/webhooks": { + "get": { + "operationId": "adminListWebhooks", + "tags": [ + "admin/webhooks" + ], + "summary": "List webhooks", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/webhooks/{webhookId}/disable": { + "post": { + "operationId": "adminForceDisableWebhook", + "tags": [ + "admin/webhooks" + ], + "summary": "Force disable webhook", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/audit": { + "get": { + "operationId": "adminListAuditLogGlobal", + "tags": [ + "admin/audit" + ], + "summary": "List audit log global", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/audit/actions": { + "get": { + "operationId": "adminListAuditLogAdminActions", + "tags": [ + "admin/audit" + ], + "summary": "List audit log admin actions", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/kpis": { + "get": { + "operationId": "adminPlatformKpisPlatform", + "tags": [ + "admin/kpis" + ], + "summary": "Platform kpis platform", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/signups": { + "get": { + "operationId": "adminRecentSignupsPlatform", + "tags": [ + "admin/signups" + ], + "summary": "Recent signups platform", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/sync": { + "post": { + "operationId": "adminEnsureCurrentUserStaffSyncStaffSync", + "tags": [ + "admin/sync" + ], + "summary": "Ensure current user staff sync staff sync", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + }, + "/admin/bans": { + "get": { + "operationId": "adminListRecentSessionBans", + "tags": [ + "admin/bans" + ], + "summary": "List recent session bans", + "description": "**EXCLUDED from the public API** (`staff`). Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "x-audience": "staff", + "x-polymorfa-resource-boundary": "human_only", + "x-excluded-from-public": true, + "x-exclusion-reason": "Polymorfa staff endpoint (cross-org / platform control). Requires a staff dashboard identity; never key-authenticated.", + "security": [ + { + "ConsoleSession": [] + } + ], + "responses": { + "200": { + "description": "Success", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DataEnvelope" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "501": { + "$ref": "#/components/responses/NotImplemented" + } + } + } + } + }, + "components": { + "securitySchemes": { + "ApiKeyAuth": { + "type": "http", + "scheme": "bearer", + "description": "Polymorfa server credential: a scoped pmfa_ organization key, or a pmfa_pt_ project token on project- and session-bound operations inside its exact authenticated project. Listener and browser client credentials are rejected." + }, + "ConsoleSession": { + "type": "http", + "scheme": "bearer", + "description": "Verified dashboard JWT for signed-in Console requests. This is not a server credential." + } + }, + "schemas": { + "DataEnvelope": { + "type": "object", + "properties": { + "data": { + "description": "Operation result." + } + }, + "required": [ + "data" + ] + }, + "ErrorEnvelope": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code" + ] + } + }, + "required": [ + "error" + ] + }, "ProjectId": { "type": "string", - "format": "uuid", - "description": "Project UUID. Optional where an operation can infer or search organization scope; required where the operation creates a project-owned resource." + "format": "uuid", + "description": "Project UUID. Optional where an operation can infer or search organization scope; required where the operation creates a project-owned resource." + }, + "CustomerId": { + "type": "string", + "format": "uuid", + "description": "Project-owned Customer UUID." + }, + "Customer": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/CustomerId" + }, + "orgId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "name": { + "type": "string", + "nullable": true, + "maxLength": 120 + }, + "phone": { + "type": "string", + "nullable": true, + "pattern": "^\\+[1-9][0-9]{6,14}$" + }, + "externalCustomerId": { + "type": "string", + "nullable": true, + "maxLength": 200 + }, + "status": { + "type": "string", + "enum": [ + "active", + "archiving", + "archived" + ] + }, + "isDefault": { + "type": "boolean" + }, + "archivedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "updatedAt": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "orgId", + "projectId", + "name", + "phone", + "externalCustomerId", + "status", + "isDefault", + "archivedAt", + "createdAt", + "updatedAt" + ] + }, + "CustomerSummary": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/CustomerId" + }, + "orgId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "name": { + "type": "string", + "nullable": true, + "maxLength": 120 + }, + "phoneMasked": { + "type": "string", + "nullable": true + }, + "externalCustomerId": { + "type": "string", + "nullable": true, + "maxLength": 200 + }, + "status": { + "type": "string", + "enum": [ + "active", + "archiving", + "archived" + ] + }, + "isDefault": { + "type": "boolean" + }, + "archivedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "numberCount": { + "type": "integer", + "minimum": 0 + }, + "connectedNumberCount": { + "type": "integer", + "minimum": 0 + }, + "activePairingLinkState": { + "type": "string", + "nullable": true + }, + "lastActivityAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "needsAttention": { + "type": "boolean" + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "updatedAt": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "orgId", + "projectId", + "name", + "phoneMasked", + "externalCustomerId", + "status", + "isDefault", + "archivedAt", + "numberCount", + "connectedNumberCount", + "activePairingLinkState", + "lastActivityAt", + "needsAttention", + "createdAt", + "updatedAt" + ] + }, + "CustomersStatus": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "enabledAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "enabledBy": { + "type": "string", + "nullable": true + }, + "defaultCustomer": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Customer" + } + ], + "nullable": true + } + }, + "required": [ + "enabled", + "enabledAt", + "enabledBy", + "defaultCustomer" + ] + }, + "CustomersEnablement": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "enabledAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "enabledBy": { + "type": "string", + "nullable": true + }, + "defaultCustomer": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Customer" + } + ], + "nullable": true + }, + "migratedNumberCount": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "enabled", + "enabledAt", + "enabledBy", + "defaultCustomer", + "migratedNumberCount" + ] + }, + "CustomerProjectRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + } + } + }, + "CreateCustomerRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "name": { + "type": "string", + "nullable": true, + "minLength": 1, + "maxLength": 120 + }, + "phone": { + "type": "string", + "nullable": true, + "description": "Phone number normalized to E.164." + }, + "externalCustomerId": { + "type": "string", + "nullable": true, + "minLength": 1, + "maxLength": 200 + } + } + }, + "UpdateCustomerRequest": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "name": { + "type": "string", + "nullable": true, + "minLength": 1, + "maxLength": 120 + }, + "phone": { + "type": "string", + "nullable": true, + "description": "Phone number normalized to E.164." + }, + "externalCustomerId": { + "type": "string", + "nullable": true, + "minLength": 1, + "maxLength": 200 + } + } + }, + "CustomerNumber": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "customerId": { + "$ref": "#/components/schemas/CustomerId" + }, + "sessionId": { + "type": "string" + }, + "name": { + "type": "string", + "nullable": true + }, + "phoneMasked": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "backend": { + "type": "string", + "nullable": true + }, + "createdAt": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "customerId", + "sessionId", + "name", + "phoneMasked", + "status", + "backend", + "createdAt" + ] + }, + "CustomerEvent": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "action": { + "type": "string", + "pattern": "^customer(?:\\.[a-z0-9_]+){1,3}$", + "maxLength": 128 + }, + "fromStatus": { + "type": "string", + "nullable": true + }, + "toStatus": { + "type": "string", + "nullable": true + }, + "sessionId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "pairingLinkId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "fields": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "name", + "phone", + "externalCustomerId" + ] + } + } + } + }, + "occurredAt": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "action", + "fromStatus", + "toStatus", + "sessionId", + "pairingLinkId", + "metadata", + "occurredAt" + ] + }, + "CustomerPairingLink": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "orgId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "customerId": { + "$ref": "#/components/schemas/CustomerId" + }, + "expectedPhoneMasked": { + "type": "string", + "nullable": true + }, + "methods": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "qr", + "phone" + ] + } + }, + "locale": { + "type": "string", + "enum": [ + "en", + "pt-BR" + ], + "nullable": true + }, + "theme": { + "type": "string", + "enum": [ + "light", + "dark", + "system" + ], + "nullable": true + }, + "expiresAt": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string", + "enum": [ + "active", + "opened", + "connecting", + "connected", + "failed", + "expired", + "revoked" + ] + }, + "attemptCount": { + "type": "integer", + "minimum": 0 + }, + "maxAttempts": { + "type": "integer", + "minimum": 1, + "maximum": 20 + }, + "pendingSessionId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "createdBy": { + "type": "string", + "nullable": true + }, + "reservedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "openedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "connectingAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "connectedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "failedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "expiredAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "revokedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "lastErrorCode": { + "type": "string", + "nullable": true + }, + "failedExchangeCount": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "phoneMismatchCount": { + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "updatedAt": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "id", + "orgId", + "projectId", + "customerId", + "expectedPhoneMasked", + "methods", + "locale", + "theme", + "expiresAt", + "status", + "attemptCount", + "maxAttempts", + "pendingSessionId", + "createdBy", + "reservedAt", + "openedAt", + "connectingAt", + "connectedAt", + "failedAt", + "expiredAt", + "revokedAt", + "lastErrorCode", + "failedExchangeCount", + "phoneMismatchCount", + "createdAt", + "updatedAt" + ] + }, + "CreatedCustomerPairingLink": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "orgId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "customerId": { + "$ref": "#/components/schemas/CustomerId" + }, + "expectedPhoneMasked": { + "type": "string", + "nullable": true + }, + "methods": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "qr", + "phone" + ] + } + }, + "locale": { + "type": "string", + "enum": [ + "en", + "pt-BR" + ], + "nullable": true + }, + "theme": { + "type": "string", + "enum": [ + "light", + "dark", + "system" + ], + "nullable": true + }, + "expiresAt": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string", + "enum": [ + "active", + "opened", + "connecting", + "connected", + "failed", + "expired", + "revoked" + ] + }, + "attemptCount": { + "type": "integer", + "minimum": 0 + }, + "maxAttempts": { + "type": "integer", + "minimum": 1, + "maximum": 20 + }, + "pendingSessionId": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "createdBy": { + "type": "string", + "nullable": true + }, + "reservedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "openedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "connectingAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "connectedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "failedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "expiredAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "revokedAt": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "lastErrorCode": { + "type": "string", + "nullable": true + }, + "failedExchangeCount": { + "type": "integer", + "minimum": 0, + "maximum": 5 + }, + "phoneMismatchCount": { + "type": "integer", + "minimum": 0, + "maximum": 3 + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "updatedAt": { + "type": "integer", + "format": "int64" + }, + "url": { + "type": "string", + "format": "uri", + "nullable": true, + "description": "Returned once; null on an idempotent replay." + } + }, + "required": [ + "id", + "orgId", + "projectId", + "customerId", + "expectedPhoneMasked", + "methods", + "locale", + "theme", + "expiresAt", + "status", + "attemptCount", + "maxAttempts", + "pendingSessionId", + "createdBy", + "reservedAt", + "openedAt", + "connectingAt", + "connectedAt", + "failedAt", + "expiredAt", + "revokedAt", + "lastErrorCode", + "failedExchangeCount", + "phoneMismatchCount", + "createdAt", + "updatedAt", + "url" + ] + }, + "CreateCustomerPairingLinkRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "expectedPhone": { + "type": "string", + "nullable": true, + "description": "Optional expected phone, normalized to E.164." + }, + "methods": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "qr", + "phone" + ] + } + }, + "expiresInSeconds": { + "type": "integer", + "minimum": 900, + "maximum": 604800, + "default": 86400 + }, + "locale": { + "type": "string", + "enum": [ + "en", + "pt-BR" + ], + "nullable": true + }, + "theme": { + "type": "string", + "enum": [ + "light", + "dark", + "system" + ], + "nullable": true + } + } + }, + "TransferCustomerNumberRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "sourceCustomerId": { + "$ref": "#/components/schemas/CustomerId" + }, + "confirm": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "projectId", + "sourceCustomerId", + "confirm" + ] + }, + "TemplateId": { + "type": "string", + "format": "uuid", + "description": "Message-template draft UUID." + }, + "ConsoleWebhookRetry": { + "type": "object", + "additionalProperties": false, + "properties": { + "attempts": { + "type": "integer", + "minimum": 0, + "maximum": 10 + }, + "delaySeconds": { + "type": "integer", + "minimum": 0, + "maximum": 3600 + }, + "policy": { + "type": "string", + "enum": [ + "constant", + "linear", + "exponential" + ] + } + }, + "required": [ + "attempts", + "delaySeconds", + "policy" + ] + }, + "ConsoleWebhook": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "url": { + "type": "string", + "format": "uri", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://", + "minLength": 1, + "maxLength": 2048, + "description": "Public HTTP(S) endpoint. Private, reserved, link-local, multicast, and credential-bearing targets are rejected." + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "retries": { + "$ref": "#/components/schemas/ConsoleWebhookRetry" + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": [ + "native", + "meta" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "hasSigningSecret": { + "type": "boolean" + } + }, + "required": [ + "id", + "url", + "events", + "retries", + "enabled", + "format", + "createdAt", + "hasSigningSecret" + ] + }, + "CreateConsoleWebhookRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://", + "minLength": 1, + "maxLength": 2048, + "description": "Public HTTP(S) endpoint. Private, reserved, link-local, multicast, and credential-bearing targets are rejected." + }, + "events": { + "type": "array", + "items": { + "type": "string" + }, + "default": [] + }, + "retries": { + "$ref": "#/components/schemas/ConsoleWebhookRetry" + }, + "signingSecret": { + "type": "string", + "minLength": 16, + "maxLength": 4096, + "writeOnly": true + }, + "format": { + "type": "string", + "enum": [ + "native", + "meta" + ], + "default": "native" + } + }, + "required": [ + "url" + ] + }, + "UpdateConsoleWebhookRequest": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "url": { + "type": "string", + "format": "uri", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://", + "minLength": 1, + "maxLength": 2048, + "description": "Public HTTP(S) endpoint. Private, reserved, link-local, multicast, and credential-bearing targets are rejected." + }, + "events": { + "type": "array", + "items": { + "type": "string" + } + }, + "retries": { + "$ref": "#/components/schemas/ConsoleWebhookRetry" + }, + "signingSecret": { + "type": "string", + "minLength": 16, + "maxLength": 4096, + "writeOnly": true + }, + "clearSigningSecret": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": [ + "native", + "meta" + ] + } + }, + "not": { + "required": [ + "signingSecret", + "clearSigningSecret" + ], + "properties": { + "clearSigningSecret": { + "enum": [ + true + ] + } + } + } + }, + "DeleteConsoleWebhookResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "removed": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "removed" + ] + }, + "WidgetColorPalette": { + "type": "object", + "additionalProperties": false, + "properties": { + "background": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "foreground": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "card": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "cardForeground": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "primary": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "primaryForeground": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "muted": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "mutedForeground": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "border": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "accent": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "destructive": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + }, + "ring": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + } + }, + "UpdateWidgetSettingsRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "enabled": { + "type": "boolean" + }, + "modesAllowed": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "embedded", + "redirect" + ] + } + }, + "allowedRedirectUris": { + "type": "array", + "maxItems": 50, + "items": { + "type": "string", + "format": "uri", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://", + "maxLength": 2048 + }, + "description": "Up to 50 exact HTTPS callback URLs without credentials." + }, + "allowedOrigins": { + "type": "array", + "maxItems": 50, + "items": { + "type": "string", + "format": "uri", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[^/?#@,]+/?$", + "maxLength": 2048 + }, + "description": "Up to 50 exact HTTPS origins without credentials, paths, queries, fragments, or commas. A trailing root slash is accepted and removed." + }, + "businessName": { + "type": "string", + "maxLength": 120, + "nullable": true + }, + "accent": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$", + "nullable": true + }, + "theme": { + "type": "string", + "enum": [ + "light", + "dark", + "system" + ] + }, + "hideWatermark": { + "type": "boolean" + }, + "colors": { + "type": "object", + "additionalProperties": false, + "nullable": true, + "properties": { + "light": { + "$ref": "#/components/schemas/WidgetColorPalette" + }, + "dark": { + "$ref": "#/components/schemas/WidgetColorPalette" + } + } + }, + "shape": { + "type": "string", + "enum": [ + "square", + "rounded", + "pill" + ], + "nullable": true + }, + "radiusPx": { + "type": "number", + "minimum": 0, + "maximum": 999, + "nullable": true + }, + "qrLogoMode": { + "type": "string", + "enum": [ + "none", + "custom", + "organization", + "project" + ] + }, + "qrLogoStorageId": { + "type": "string", + "nullable": true + }, + "qrLogoSourceStorageId": { + "type": "string", + "nullable": true + }, + "historySync": { + "type": "string", + "enum": [ + "ask", + "force_on", + "force_off" + ] + }, + "methods": { + "type": "array", + "nullable": true, + "minItems": 1, + "maxItems": 50, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "qr", + "pairing", + "cloud-api" + ] + } + } + } + }, + "FlowId": { + "type": "string", + "format": "uuid", + "description": "Polymorfa Flow draft UUID." + }, + "FlowSummary": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/FlowId" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "status": { + "type": "string", + "enum": [ + "draft", + "ready", + "archived" + ] + }, + "version": { + "type": "string" + }, + "screenCount": { + "type": "integer", + "minimum": 0, + "maximum": 50 + }, + "metaLinks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "createdAt": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "updatedAt": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + "required": [ + "id", + "name", + "status", + "version", + "screenCount", + "metaLinks", + "createdAt", + "updatedAt" + ] + }, + "FlowDraft": { + "allOf": [ + { + "$ref": "#/components/schemas/FlowSummary" + }, + { + "type": "object", + "properties": { + "definition": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "definition" + ] + } + ] + }, + "CreateFlowRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "draftId": { + "$ref": "#/components/schemas/FlowId" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "definition": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "projectId", + "name", + "definition" + ] + }, + "UpdateFlowRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "expectedUpdatedAt": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "status": { + "type": "string", + "enum": [ + "draft", + "ready", + "archived" + ] + }, + "definition": { + "type": "object", + "additionalProperties": true + } + }, + "required": [ + "projectId", + "expectedUpdatedAt" + ] + }, + "DeleteFlowResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "ok": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "ok" + ] + }, + "ManagementTemplate": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/TemplateId" + }, + "createdAt": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "category": { + "type": "string" + }, + "language": { + "type": "string" + }, + "status": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "definition": { + "type": "object", + "additionalProperties": true, + "nullable": true + }, + "sampleValues": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + }, + "cloudLinks": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "updatedAt": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + "required": [ + "id", + "createdAt", + "name", + "category", + "language", + "status", + "kind", + "definition", + "sampleValues", + "cloudLinks", + "updatedAt" + ] + }, + "CreateManagementTemplateRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "draftId": { + "$ref": "#/components/schemas/TemplateId" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "definition": { + "type": "object", + "additionalProperties": true + }, + "sampleValues": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + } + }, + "required": [ + "projectId", + "name", + "definition" + ] + }, + "UpdateManagementTemplateRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "status": { + "type": "string" + }, + "definition": { + "type": "object", + "additionalProperties": true + }, + "sampleValues": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "nullable": true + } + }, + "required": [ + "projectId" + ] + }, + "DeleteManagementTemplateResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "ok": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "ok" + ] + }, + "SessionIdentifier": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "Database session UUID or stable session slug." + }, + "SessionProjectContext": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + } + } + }, + "SessionTierOverrideRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "tierOverride": { + "type": "string", + "enum": [ + "free", + "standard", + "pro" + ], + "nullable": true + } + }, + "required": [ + "tierOverride" + ] + }, + "SessionBatchRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "sessionIds": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/SessionIdentifier" + } + }, + "ids": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "deprecated": true, + "description": "Compatibility alias for sessionIds.", + "items": { + "$ref": "#/components/schemas/SessionIdentifier" + } + } + }, + "anyOf": [ + { + "required": [ + "sessionIds" + ] + }, + { + "required": [ + "ids" + ] + } + ] + }, + "CreateTestingNumberRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "customerId": { + "type": "string", + "format": "uuid", + "description": "Customer owner. Defaults to the protected Default customer when Customers is enabled." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Optional Console display name." + }, + "country": { + "type": "string", + "enum": [ + "ES", + "DE", + "MX", + "GB", + "SA", + "IT", + "AF", + "AX", + "AL", + "DZ", + "AS", + "AD", + "AO", + "AI", + "AG", + "AR", + "AM", + "AW", + "AC", + "AU", + "AT", + "AZ", + "BS", + "BH", + "BD", + "BB", + "BY", + "BE", + "BZ", + "BJ", + "BM", + "BT", + "BO", + "BQ", + "BA", + "BW", + "BR", + "IO", + "VG", + "BN", + "BG", + "BF", + "BI", + "CI", + "KH", + "CM", + "CA", + "CV", + "KY", + "CF", + "TD", + "CL", + "CN", + "CX", + "CC", + "CO", + "KM", + "CG", + "CD", + "CK", + "CR", + "HR", + "CU", + "CW", + "CY", + "CZ", + "DK", + "DJ", + "DM", + "DO", + "EC", + "EG", + "SV", + "GQ", + "ER", + "EE", + "ET", + "FK", + "FO", + "FJ", + "FI", + "FR", + "GF", + "PF", + "GA", + "GM", + "GE", + "GH", + "GI", + "GR", + "GL", + "GD", + "GP", + "GU", + "GT", + "GG", + "GN", + "GW", + "GY", + "HT", + "HN", + "HK", + "HU", + "IS", + "IN", + "ID", + "IR", + "IQ", + "IE", + "IM", + "IL", + "JM", + "JP", + "JE", + "JO", + "KZ", + "KE", + "KI", + "KW", + "KG", + "LA", + "LV", + "LB", + "LS", + "LR", + "LY", + "LI", + "LT", + "LU", + "MO", + "MK", + "MG", + "MW", + "MY", + "MV", + "ML", + "MT", + "MH", + "MQ", + "MR", + "MU", + "YT", + "FM", + "MD", + "MC", + "MN", + "ME", + "MS", + "MA", + "MZ", + "MM", + "NA", + "NR", + "NP", + "NL", + "NC", + "NZ", + "NI", + "NE", + "NG", + "NU", + "NF", + "KP", + "MP", + "NO", + "OM", + "PK", + "PW", + "PS", + "PA", + "PG", + "PY", + "PE", + "PH", + "PL", + "PT", + "PR", + "QA", + "RO", + "RU", + "RW", + "RE", + "BL", + "SH", + "KN", + "LC", + "MF", + "PM", + "VC", + "WS", + "SM", + "SN", + "RS", + "SC", + "SL", + "SG", + "SX", + "SK", + "SI", + "SB", + "SO", + "ZA", + "KR", + "SS", + "LK", + "SD", + "SR", + "SJ", + "SZ", + "SE", + "CH", + "SY", + "ST", + "TW", + "TJ", + "TZ", + "TH", + "TL", + "TG", + "TK", + "TO", + "TT", + "TN", + "TR", + "TM", + "TC", + "TV", + "VI", + "UG", + "UA", + "AE", + "US", + "UY", + "UZ", + "VU", + "VA", + "VE", + "VN", + "WF", + "YE", + "ZM", + "ZW", + "XK" + ], + "default": "US", + "description": "WhatsApp-supported ISO 3166-1 alpha-2 country used to generate a realistic E.164 identity." + } + }, + "required": [ + "projectId" + ] + }, + "ManagedSession": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "sessionId": { + "type": "string", + "description": "Stable session slug." + }, + "status": { + "type": "string" + }, + "tierOverride": { + "type": "string", + "enum": [ + "free", + "standard", + "pro" + ], + "nullable": true + } + }, + "required": [ + "id", + "sessionId", + "status", + "tierOverride" + ] + }, + "PlatformSession": { + "type": "object", + "additionalProperties": false, + "properties": { + "_id": { + "type": "string", + "format": "uuid" + }, + "_creationTime": { + "type": "number", + "description": "Creation time as Unix epoch milliseconds." + }, + "projectId": { + "$ref": "#/components/schemas/ProjectId" + }, + "sessionId": { + "$ref": "#/components/schemas/SessionIdentifier" + }, + "name": { + "type": "string", + "nullable": true + }, + "phone": { + "type": "string", + "nullable": true + }, + "platform": { + "type": "string", + "nullable": true + }, + "isBusiness": { + "type": "boolean" + }, + "testMode": { + "type": "boolean" + }, + "tierOverride": { + "type": "string", + "enum": [ + "free", + "standard", + "pro", + "scale" + ], + "nullable": true + }, + "status": { + "type": "string", + "description": "Latest runtime connection status." + }, + "statusReason": { + "type": "string", + "nullable": true, + "description": "Machine-readable reason for the latest connection status, when available." + }, + "messageCount": { + "type": "integer", + "minimum": 0 + }, + "lastActiveAt": { + "type": "number", + "nullable": true, + "description": "Last activity time as Unix epoch milliseconds." + }, + "customer": { + "type": "object", + "nullable": true, + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "isDefault": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "isDefault" + ] + }, + "claim": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "unclaimed" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "type": "string", + "enum": [ + "claimed-elsewhere" + ] + }, + "displayName": { + "type": "string" + }, + "avatarUrl": { + "type": "string", + "nullable": true + }, + "avatarSeed": { + "type": "string" + }, + "claimedAt": { + "type": "number" + } + }, + "required": [ + "kind", + "displayName", + "avatarUrl", + "avatarSeed", + "claimedAt" + ] + } + ] + } + }, + "required": [ + "_id", + "_creationTime", + "projectId", + "sessionId", + "name", + "phone", + "platform", + "isBusiness", + "testMode", + "tierOverride", + "status", + "statusReason", + "messageCount", + "lastActiveAt", + "customer", + "claim" + ] + }, + "SessionStopResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "stopping": { + "type": "boolean", + "enum": [ + true + ] + }, + "sessionId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "stopping", + "sessionId" + ] + }, + "SessionStartResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "starting": { + "type": "boolean", + "enum": [ + true + ] + }, + "sessionId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "starting", + "sessionId" + ] + }, + "SessionRemoveResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "removed": { + "type": "boolean", + "enum": [ + true + ] + }, + "sessionId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "removed", + "sessionId" + ] + }, + "SessionBatchStopResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "stopping": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + "required": [ + "stopping" + ] + }, + "SessionBatchRemoveResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "removed": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + "required": [ + "removed" + ] + }, + "TestingSessionId": { + "type": "string", + "pattern": "^test_[A-Za-z0-9_-]{16}$", + "description": "Generated testing-session slug." + }, + "ProductionEnrollmentRequest": { + "type": "object", + "additionalProperties": false, + "properties": { + "business": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 120 + }, + "website": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "maxLength": 2048 + }, + "supportEmail": { + "type": "string", + "format": "email", + "maxLength": 320 + } + }, + "required": [ + "name", + "website", + "supportEmail" + ] + } + }, + "required": [ + "business" + ] + }, + "ProductionEnrollmentResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/components/schemas/ProjectId" + }, + "orgId": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "stage": { + "type": "string", + "enum": [ + "development" + ] + }, + "operationId": { + "type": "string", + "format": "uuid" + }, + "enrollmentStatus": { + "type": "string", + "enum": [ + "requested", + "approval_required", + "provisioning", + "ready" + ] + } + }, + "required": [ + "id", + "orgId", + "name", + "slug", + "stage", + "operationId", + "enrollmentStatus" + ] }, - "TemplateId": { - "type": "string", - "format": "uuid", - "description": "Message-template draft UUID." + "ProductionEnrollmentCommandResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "operationId": { + "type": "string", + "format": "uuid" + }, + "action": { + "type": "string", + "enum": [ + "approve", + "cancel" + ] + }, + "accepted": { + "type": "boolean", + "enum": [ + true + ] + } + }, + "required": [ + "operationId", + "action", + "accepted" + ] }, - "ConsoleWebhookRetry": { + "CursorPageMetadata": { "type": "object", "additionalProperties": false, "properties": { - "attempts": { + "nextCursor": { + "type": "string", + "nullable": true + }, + "hasMore": { + "type": "boolean" + } + }, + "required": [ + "nextCursor", + "hasMore" + ] + }, + "WebhookRetryPolicyInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "maximumAttempts": { "type": "integer", - "minimum": 0, + "minimum": 1, "maximum": 10 }, - "delaySeconds": { + "backoff": { + "type": "string", + "enum": [ + "constant", + "linear", + "exponential" + ] + }, + "initialDelaySeconds": { "type": "integer", - "minimum": 0, + "minimum": 1, "maximum": 3600 + } + }, + "required": [ + "maximumAttempts", + "backoff", + "initialDelaySeconds" + ] + }, + "WebhookRetryPolicy": { + "type": "object", + "additionalProperties": false, + "properties": { + "maximumAttempts": { + "type": "integer", + "minimum": 1, + "maximum": 10 }, - "policy": { + "backoff": { "type": "string", "enum": [ "constant", "linear", "exponential" ] + }, + "initialDelaySeconds": { + "type": "integer", + "minimum": 1, + "maximum": 3600 } }, "required": [ - "attempts", - "delaySeconds", - "policy" + "maximumAttempts", + "backoff", + "initialDelaySeconds" ] }, - "ConsoleWebhook": { + "WebhookHeaderInput": { "type": "object", "additionalProperties": false, "properties": { - "id": { + "name": { "type": "string", - "format": "uuid" + "minLength": 1, + "maxLength": 128, + "pattern": "^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$" + }, + "value": { + "type": "string", + "maxLength": 4096, + "pattern": "^(?![\\s\\S]*[\\u0000-\\u0008\\u000A-\\u001F\\u007F])[\\s\\S]*$", + "x-polymorfa-max-utf8-bytes": 4096, + "writeOnly": true, + "description": "Encrypted write-only HTTP field value. Limited to 4096 UTF-8 bytes. Line breaks, NUL, and other control characters except horizontal tab are rejected." + } + }, + "required": [ + "name", + "value" + ] + }, + "WebhookHeaderMetadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "required": [ + "name" + ] + }, + "WebhookSigningSecretMetadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "version": { + "type": "integer", + "minimum": 1 + }, + "createdAt": { + "type": "string", + "format": "date-time" }, + "previousValidUntil": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "required": [ + "version", + "createdAt", + "previousValidUntil" + ] + }, + "CreateOrganizationWebhookInput": { + "type": "object", + "additionalProperties": false, + "properties": { "url": { "type": "string", "format": "uri", - "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://", "minLength": 1, - "maxLength": 2048, - "description": "Public HTTP(S) endpoint. Private, reserved, link-local, multicast, and credential-bearing targets are rejected." + "maxLength": 2048 }, - "events": { + "eventTypes": { "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, "items": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 128 } }, - "retries": { - "$ref": "#/components/schemas/ConsoleWebhookRetry" - }, "enabled": { - "type": "boolean" + "type": "boolean", + "default": true }, "format": { "type": "string", "enum": [ "native", "meta" - ] + ], + "default": "native" }, - "createdAt": { - "type": "string", - "format": "date-time" + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" }, - "hasSigningSecret": { - "type": "boolean" + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" + } } }, "required": [ - "id", "url", - "events", - "retries", - "enabled", - "format", - "createdAt", - "hasSigningSecret" + "eventTypes" ] }, - "CreateConsoleWebhookRequest": { + "CreateProjectWebhookInput": { "type": "object", "additionalProperties": false, "properties": { "url": { "type": "string", "format": "uri", - "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://", "minLength": 1, - "maxLength": 2048, - "description": "Public HTTP(S) endpoint. Private, reserved, link-local, multicast, and credential-bearing targets are rejected." + "maxLength": 2048 }, - "events": { + "eventTypes": { "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, "items": { - "type": "string" - }, - "default": [] - }, - "retries": { - "$ref": "#/components/schemas/ConsoleWebhookRetry" + "type": "string", + "minLength": 1, + "maxLength": 128 + } }, - "signingSecret": { - "type": "string", - "minLength": 16, - "maxLength": 4096, - "writeOnly": true + "enabled": { + "type": "boolean", + "default": true }, "format": { "type": "string", @@ -10421,43 +19492,88 @@ "meta" ], "default": "native" + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" + } } }, "required": [ - "url" + "url", + "eventTypes" ] }, - "UpdateConsoleWebhookRequest": { + "UpdateOrganizationWebhookInput": { "type": "object", "additionalProperties": false, - "minProperties": 1, "properties": { "url": { "type": "string", "format": "uri", - "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://", "minLength": 1, - "maxLength": 2048, - "description": "Public HTTP(S) endpoint. Private, reserved, link-local, multicast, and credential-bearing targets are rejected." + "maxLength": 2048 }, - "events": { + "eventTypes": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean" + }, + "format": { + "type": "string", + "enum": [ + "native", + "meta" + ] + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" + } + } + }, + "minProperties": 1 + }, + "UpdateProjectWebhookInput": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2048 + }, + "eventTypes": { "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, "items": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 128 } }, - "retries": { - "$ref": "#/components/schemas/ConsoleWebhookRetry" - }, - "signingSecret": { - "type": "string", - "minLength": 16, - "maxLength": 4096, - "writeOnly": true - }, - "clearSigningSecret": { - "type": "boolean" - }, "enabled": { "type": "boolean" }, @@ -10467,639 +19583,809 @@ "native", "meta" ] - } - }, - "not": { - "required": [ - "signingSecret", - "clearSigningSecret" - ], - "properties": { - "clearSigningSecret": { - "enum": [ - true - ] + }, + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicyInput" + }, + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderInput" } } - } + }, + "minProperties": 1 }, - "DeleteConsoleWebhookResult": { + "RotateWebhookSecretInput": { "type": "object", "additionalProperties": false, "properties": { - "removed": { - "type": "boolean", - "enum": [ - true - ] + "overlapSeconds": { + "type": "integer", + "minimum": 0, + "maximum": 86400, + "default": 3600 } - }, - "required": [ - "removed" - ] + } }, - "WidgetColorPalette": { + "OrganizationWebhook": { "type": "object", "additionalProperties": false, "properties": { - "background": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "foreground": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "card": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "cardForeground": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "primary": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "primaryForeground": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "muted": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - }, - "mutedForeground": { + "id": { "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" + "format": "uuid" }, - "border": { + "organizationId": { "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" + "format": "uuid" }, - "accent": { + "owner": { "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" + "enum": [ + "organization" + ] }, - "destructive": { + "projectId": { "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" + "nullable": true, + "enum": [ + null + ] }, - "ring": { + "url": { "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$" - } - } - }, - "UpdateWidgetSettingsRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "enabled": { - "type": "boolean" + "format": "uri", + "maxLength": 2048 }, - "modesAllowed": { + "eventTypes": { "type": "array", "minItems": 1, - "maxItems": 50, + "maxItems": 100, "uniqueItems": true, "items": { "type": "string", - "enum": [ - "embedded", - "redirect" - ] + "minLength": 1, + "maxLength": 128 } }, - "allowedRedirectUris": { - "type": "array", - "maxItems": 50, - "items": { - "type": "string", - "format": "uri", - "pattern": "^[Hh][Tt][Tt][Pp][Ss]://", - "maxLength": 2048 - }, - "description": "Up to 50 exact HTTPS callback URLs without credentials." - }, - "allowedOrigins": { - "type": "array", - "maxItems": 50, - "items": { - "type": "string", - "format": "uri", - "pattern": "^[Hh][Tt][Tt][Pp][Ss]://[^/?#@,]+/?$", - "maxLength": 2048 - }, - "description": "Up to 50 exact HTTPS origins without credentials, paths, queries, fragments, or commas. A trailing root slash is accepted and removed." - }, - "businessName": { - "type": "string", - "maxLength": 120, - "nullable": true - }, - "accent": { - "type": "string", - "pattern": "^#[0-9A-Fa-f]{6}$", - "nullable": true + "enabled": { + "type": "boolean" }, - "theme": { + "format": { "type": "string", "enum": [ - "light", - "dark", - "system" + "native", + "meta" ] }, - "hideWatermark": { - "type": "boolean" + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicy" }, - "colors": { - "type": "object", - "additionalProperties": false, - "nullable": true, - "properties": { - "light": { - "$ref": "#/components/schemas/WidgetColorPalette" - }, - "dark": { - "$ref": "#/components/schemas/WidgetColorPalette" - } + "headers": { + "type": "array", + "maxItems": 10, + "items": { + "$ref": "#/components/schemas/WebhookHeaderMetadata" } }, - "shape": { - "type": "string", - "enum": [ - "square", - "rounded", - "pill" - ], - "nullable": true - }, - "radiusPx": { - "type": "number", - "minimum": 0, - "maximum": 999, - "nullable": true + "secret": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" }, - "qrLogoMode": { + "createdAt": { "type": "string", - "enum": [ - "none", - "custom", - "organization", - "project" - ] + "format": "date-time" }, - "qrLogoStorageId": { + "updatedAt": { "type": "string", - "nullable": true + "format": "date-time" + } + }, + "required": [ + "id", + "organizationId", + "owner", + "projectId", + "url", + "eventTypes", + "enabled", + "format", + "retryPolicy", + "headers", + "secret", + "createdAt", + "updatedAt" + ] + }, + "ProjectWebhook": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "format": "uuid" }, - "qrLogoSourceStorageId": { + "organizationId": { "type": "string", - "nullable": true + "format": "uuid" }, - "historySync": { + "owner": { "type": "string", "enum": [ - "ask", - "force_on", - "force_off" + "project" ] }, - "methods": { - "type": "array", - "nullable": true, - "minItems": 1, - "maxItems": 50, - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ - "qr", - "pairing", - "cloud-api" - ] - } - } - } - }, - "FlowId": { - "type": "string", - "format": "uuid", - "description": "Polymorfa Flow draft UUID." - }, - "FlowSummary": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "$ref": "#/components/schemas/FlowId" + "projectId": { + "type": "string", + "format": "uuid" }, - "name": { + "url": { "type": "string", - "minLength": 1, - "maxLength": 120 + "format": "uri", + "maxLength": 2048 }, - "status": { + "eventTypes": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 128 + } + }, + "enabled": { + "type": "boolean" + }, + "format": { "type": "string", "enum": [ - "draft", - "ready", - "archived" + "native", + "meta" ] }, - "version": { - "type": "string" - }, - "screenCount": { - "type": "integer", - "minimum": 0, - "maximum": 50 + "retryPolicy": { + "$ref": "#/components/schemas/WebhookRetryPolicy" }, - "metaLinks": { + "headers": { "type": "array", + "maxItems": 10, "items": { - "type": "object", - "additionalProperties": true + "$ref": "#/components/schemas/WebhookHeaderMetadata" } }, + "secret": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" + }, "createdAt": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "date-time" }, "updatedAt": { - "type": "integer", - "format": "int64", - "minimum": 0 + "type": "string", + "format": "date-time" } }, "required": [ "id", - "name", - "status", - "version", - "screenCount", - "metaLinks", + "organizationId", + "owner", + "projectId", + "url", + "eventTypes", + "enabled", + "format", + "retryPolicy", + "headers", + "secret", "createdAt", "updatedAt" ] }, - "FlowDraft": { - "allOf": [ - { - "$ref": "#/components/schemas/FlowSummary" + "OrganizationWebhookCreationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhook": { + "$ref": "#/components/schemas/OrganizationWebhook" }, - { - "type": "object", - "properties": { - "definition": { - "type": "object", - "additionalProperties": true - } - }, - "required": [ - "definition" + "operationId": { + "type": "string", + "nullable": true, + "enum": [ + null ] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + }, + "secret": { + "type": "string", + "nullable": true, + "writeOnly": true + }, + "secretAvailable": { + "type": "boolean" } + }, + "required": [ + "webhook", + "operationId", + "idempotency", + "secret", + "secretAvailable" ] }, - "CreateFlowRequest": { + "ProjectWebhookCreationReceipt": { "type": "object", "additionalProperties": false, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" + "webhook": { + "$ref": "#/components/schemas/ProjectWebhook" }, - "draftId": { - "$ref": "#/components/schemas/FlowId" + "operationId": { + "type": "string", + "nullable": true, + "enum": [ + null + ] }, - "name": { + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + }, + "secret": { "type": "string", - "minLength": 1, - "maxLength": 120 + "nullable": true, + "writeOnly": true }, - "definition": { - "type": "object", - "additionalProperties": true + "secretAvailable": { + "type": "boolean" } }, "required": [ - "projectId", - "name", - "definition" + "webhook", + "operationId", + "idempotency", + "secret", + "secretAvailable" ] }, - "UpdateFlowRequest": { + "OrganizationWebhookMutationReceipt": { "type": "object", "additionalProperties": false, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "expectedUpdatedAt": { - "type": "integer", - "format": "int64", - "minimum": 0 + "webhook": { + "$ref": "#/components/schemas/OrganizationWebhook" }, - "name": { + "operationId": { "type": "string", - "minLength": 1, - "maxLength": 120 + "nullable": true, + "enum": [ + null + ] }, - "status": { + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": [ + "webhook", + "operationId", + "idempotency" + ] + }, + "ProjectWebhookMutationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "webhook": { + "$ref": "#/components/schemas/ProjectWebhook" + }, + "operationId": { "type": "string", + "nullable": true, "enum": [ - "draft", - "ready", - "archived" + null ] }, - "definition": { - "type": "object", - "additionalProperties": true + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" } }, "required": [ - "projectId", - "expectedUpdatedAt" + "webhook", + "operationId", + "idempotency" ] }, - "DeleteFlowResult": { + "OrganizationWebhookDeletionReceipt": { "type": "object", "additionalProperties": false, "properties": { - "ok": { + "webhookId": { + "type": "string", + "format": "uuid" + }, + "deleted": { "type": "boolean", "enum": [ true ] + }, + "operationId": { + "type": "string", + "nullable": true, + "enum": [ + null + ] + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" } }, "required": [ - "ok" + "webhookId", + "deleted", + "operationId", + "idempotency" ] }, - "ManagementTemplate": { + "ProjectWebhookDeletionReceipt": { "type": "object", "additionalProperties": false, "properties": { - "id": { - "$ref": "#/components/schemas/TemplateId" - }, - "createdAt": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "name": { + "webhookId": { "type": "string", - "minLength": 1, - "maxLength": 512 - }, - "category": { - "type": "string" - }, - "language": { - "type": "string" - }, - "status": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "definition": { - "type": "object", - "additionalProperties": true, - "nullable": true + "format": "uuid" }, - "sampleValues": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true + "deleted": { + "type": "boolean", + "enum": [ + true + ] }, - "cloudLinks": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": true - } + "operationId": { + "type": "string", + "nullable": true, + "enum": [ + null + ] }, - "updatedAt": { - "type": "integer", - "format": "int64", - "minimum": 0 + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" } }, "required": [ - "id", - "createdAt", - "name", - "category", - "language", - "status", - "kind", - "definition", - "sampleValues", - "cloudLinks", - "updatedAt" + "webhookId", + "deleted", + "operationId", + "idempotency" ] }, - "CreateManagementTemplateRequest": { + "OrganizationWebhookSecretRotationReceipt": { "type": "object", "additionalProperties": false, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "draftId": { - "$ref": "#/components/schemas/TemplateId" - }, - "name": { + "webhookId": { "type": "string", - "minLength": 1, - "maxLength": 512 + "format": "uuid" }, - "definition": { - "type": "object", - "additionalProperties": true + "operationId": { + "type": "string", + "nullable": true, + "enum": [ + null + ] }, - "sampleValues": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true + "secret": { + "type": "string", + "nullable": true, + "writeOnly": true + }, + "secretAvailable": { + "type": "boolean" + }, + "secretMetadata": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" } }, "required": [ - "projectId", - "name", - "definition" + "webhookId", + "operationId", + "secret", + "secretAvailable", + "secretMetadata", + "idempotency" ] }, - "UpdateManagementTemplateRequest": { + "ProjectWebhookSecretRotationReceipt": { "type": "object", "additionalProperties": false, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" + "webhookId": { + "type": "string", + "format": "uuid" }, - "name": { + "operationId": { "type": "string", - "minLength": 1, - "maxLength": 512 + "nullable": true, + "enum": [ + null + ] }, - "status": { - "type": "string" + "secret": { + "type": "string", + "nullable": true, + "writeOnly": true }, - "definition": { - "type": "object", - "additionalProperties": true + "secretAvailable": { + "type": "boolean" }, - "sampleValues": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true + "secretMetadata": { + "$ref": "#/components/schemas/WebhookSigningSecretMetadata" + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" } }, "required": [ - "projectId" + "webhookId", + "operationId", + "secret", + "secretAvailable", + "secretMetadata", + "idempotency" ] }, - "DeleteManagementTemplateResult": { + "EncodedEventPayload": { "type": "object", "additionalProperties": false, "properties": { - "ok": { - "type": "boolean", + "encoding": { + "type": "string", "enum": [ - true + "base64" + ] + }, + "contentType": { + "type": "string", + "enum": [ + "application/json" ] + }, + "data": { + "type": "string", + "format": "byte", + "maxLength": 87384, + "description": "Canonical base64 for the complete native webhook body; decoded bytes are limited to 65,536 bytes." } }, "required": [ - "ok" + "encoding", + "contentType", + "data" ] }, - "SessionIdentifier": { + "PayloadAvailability": { "type": "string", - "minLength": 1, - "maxLength": 64, - "description": "Database session UUID or stable session slug." - }, - "SessionProjectContext": { - "type": "object", - "additionalProperties": false, - "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" - } - } + "enum": [ + "available", + "not_retained", + "expired", + "redacted", + "unavailable" + ] }, - "SessionTierOverrideRequest": { + "OrganizationEvent": { "type": "object", "additionalProperties": false, "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, "projectId": { - "$ref": "#/components/schemas/ProjectId" + "type": "string", + "nullable": true, + "enum": [ + null + ] }, - "tierOverride": { + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "source": { "type": "string", "enum": [ - "free", - "standard", - "pro", - "scale" - ], + "runtime", + "platform", + "test" + ] + }, + "environment": { + "type": "string", + "enum": [ + "development", + "production" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "payloadAvailability": { + "type": "string", + "enum": [ + "available", + "not_retained", + "expired", + "redacted", + "unavailable" + ] + }, + "payload": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/EncodedEventPayload" + } + ] + }, + "replayableUntil": { + "type": "string", + "format": "date-time", "nullable": true + }, + "metadataExpiresAt": { + "type": "string", + "format": "date-time" } }, "required": [ - "tierOverride" + "id", + "organizationId", + "projectId", + "type", + "source", + "environment", + "createdAt", + "payloadAvailability", + "payload", + "replayableUntil", + "metadataExpiresAt" ] }, - "SessionBatchRequest": { + "ProjectEvent": { "type": "object", "additionalProperties": false, "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, "projectId": { - "$ref": "#/components/schemas/ProjectId" + "type": "string", + "format": "uuid" }, - "sessionIds": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "uniqueItems": true, - "items": { - "$ref": "#/components/schemas/SessionIdentifier" - } + "type": { + "type": "string", + "minLength": 1, + "maxLength": 128 }, - "ids": { - "type": "array", - "minItems": 1, - "maxItems": 100, - "uniqueItems": true, - "deprecated": true, - "description": "Compatibility alias for sessionIds.", - "items": { - "$ref": "#/components/schemas/SessionIdentifier" - } - } - }, - "anyOf": [ - { - "required": [ - "sessionIds" + "source": { + "type": "string", + "enum": [ + "runtime", + "platform", + "test" ] }, - { - "required": [ - "ids" + "environment": { + "type": "string", + "enum": [ + "development", + "production" + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "payloadAvailability": { + "type": "string", + "enum": [ + "available", + "not_retained", + "expired", + "redacted", + "unavailable" + ] + }, + "payload": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/EncodedEventPayload" + } ] + }, + "replayableUntil": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metadataExpiresAt": { + "type": "string", + "format": "date-time" } + }, + "required": [ + "id", + "organizationId", + "projectId", + "type", + "source", + "environment", + "createdAt", + "payloadAvailability", + "payload", + "replayableUntil", + "metadataExpiresAt" ] }, - "CreateTestingNumberRequest": { + "OrganizationWebhookDelivery": { "type": "object", "additionalProperties": false, "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, "projectId": { - "$ref": "#/components/schemas/ProjectId" + "type": "string", + "nullable": true, + "enum": [ + null + ] }, - "name": { + "eventId": { + "type": "string", + "format": "uuid" + }, + "webhookId": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "delivering", + "retrying", + "succeeded", + "failed" + ] + }, + "attemptCount": { + "type": "integer", + "minimum": 0 + }, + "capabilities": { + "type": "object", + "additionalProperties": false, + "properties": { + "retryable": { + "type": "boolean" + } + }, + "required": [ + "retryable" + ] + }, + "payloadAvailability": { + "type": "string", + "enum": [ + "available", + "not_retained", + "expired", + "redacted", + "unavailable" + ] + }, + "replayableUntil": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metadataExpiresAt": { + "type": "string", + "format": "date-time" + }, + "nextAttemptAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastAttemptAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "completedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "createdAt": { "type": "string", - "minLength": 1, - "maxLength": 100, - "description": "Optional Console display name." + "format": "date-time" }, - "country": { + "updatedAt": { "type": "string", - "enum": [ - "US", - "GB", - "BR", - "IN" - ], - "default": "US", - "description": "ISO 3166-1 alpha-2 country used to generate a realistic E.164 identity." + "format": "date-time" + }, + "lastOutcome": { + "type": "object", + "nullable": true, + "additionalProperties": false, + "properties": { + "statusCode": { + "type": "integer", + "minimum": 100, + "maximum": 599, + "nullable": true + }, + "errorCode": { + "type": "string", + "nullable": true + } + }, + "required": [ + "statusCode", + "errorCode" + ] } }, "required": [ - "projectId" + "id", + "organizationId", + "projectId", + "eventId", + "webhookId", + "status", + "attemptCount", + "capabilities", + "payloadAvailability", + "replayableUntil", + "metadataExpiresAt", + "nextAttemptAt", + "lastAttemptAt", + "completedAt", + "createdAt", + "updatedAt", + "lastOutcome" ] }, - "ManagedSession": { + "ProjectWebhookDelivery": { "type": "object", "additionalProperties": false, "properties": { @@ -11107,216 +20393,419 @@ "type": "string", "format": "uuid" }, - "sessionId": { + "organizationId": { "type": "string", - "description": "Stable session slug." + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "eventId": { + "type": "string", + "format": "uuid" + }, + "webhookId": { + "type": "string", + "format": "uuid" }, "status": { - "type": "string" + "type": "string", + "enum": [ + "pending", + "delivering", + "retrying", + "succeeded", + "failed" + ] }, - "tierOverride": { + "attemptCount": { + "type": "integer", + "minimum": 0 + }, + "capabilities": { + "type": "object", + "additionalProperties": false, + "properties": { + "retryable": { + "type": "boolean" + } + }, + "required": [ + "retryable" + ] + }, + "payloadAvailability": { "type": "string", "enum": [ - "free", - "standard", - "pro", - "scale" - ], + "available", + "not_retained", + "expired", + "redacted", + "unavailable" + ] + }, + "replayableUntil": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "metadataExpiresAt": { + "type": "string", + "format": "date-time" + }, + "nextAttemptAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "lastAttemptAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "completedAt": { + "type": "string", + "format": "date-time", "nullable": true + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "lastOutcome": { + "type": "object", + "nullable": true, + "additionalProperties": false, + "properties": { + "statusCode": { + "type": "integer", + "minimum": 100, + "maximum": 599, + "nullable": true + }, + "errorCode": { + "type": "string", + "nullable": true + } + }, + "required": [ + "statusCode", + "errorCode" + ] } }, "required": [ "id", - "sessionId", + "organizationId", + "projectId", + "eventId", + "webhookId", "status", - "tierOverride" + "attemptCount", + "capabilities", + "payloadAvailability", + "replayableUntil", + "metadataExpiresAt", + "nextAttemptAt", + "lastAttemptAt", + "completedAt", + "createdAt", + "updatedAt", + "lastOutcome" ] }, - "SessionStopResult": { + "OrganizationWebhookDeliveryAttempt": { "type": "object", "additionalProperties": false, "properties": { - "stopping": { - "type": "boolean", + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "nullable": true, "enum": [ - true + null ] }, - "sessionId": { + "deliveryId": { "type": "string", "format": "uuid" + }, + "number": { + "type": "integer", + "minimum": 1 + }, + "status": { + "type": "string", + "enum": [ + "pending", + "delivering", + "succeeded", + "failed" + ] + }, + "startedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "completedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "nextRetryAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "statusCode": { + "type": "integer", + "minimum": 100, + "maximum": 599, + "nullable": true + }, + "errorCode": { + "type": "string", + "nullable": true + }, + "metadataExpiresAt": { + "type": "string", + "format": "date-time" } }, "required": [ - "stopping", - "sessionId" + "id", + "organizationId", + "projectId", + "deliveryId", + "number", + "status", + "startedAt", + "completedAt", + "nextRetryAt", + "durationMs", + "statusCode", + "errorCode", + "metadataExpiresAt" ] }, - "SessionRemoveResult": { + "ProjectWebhookDeliveryAttempt": { "type": "object", "additionalProperties": false, "properties": { - "removed": { - "type": "boolean", + "id": { + "type": "string", + "format": "uuid" + }, + "organizationId": { + "type": "string", + "format": "uuid" + }, + "projectId": { + "type": "string", + "format": "uuid" + }, + "deliveryId": { + "type": "string", + "format": "uuid" + }, + "number": { + "type": "integer", + "minimum": 1 + }, + "status": { + "type": "string", "enum": [ - true + "pending", + "delivering", + "succeeded", + "failed" ] }, - "sessionId": { + "startedAt": { "type": "string", - "format": "uuid" + "format": "date-time", + "nullable": true + }, + "completedAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "nextRetryAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "durationMs": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "statusCode": { + "type": "integer", + "minimum": 100, + "maximum": 599, + "nullable": true + }, + "errorCode": { + "type": "string", + "nullable": true + }, + "metadataExpiresAt": { + "type": "string", + "format": "date-time" } }, "required": [ - "removed", - "sessionId" + "id", + "organizationId", + "projectId", + "deliveryId", + "number", + "status", + "startedAt", + "completedAt", + "nextRetryAt", + "durationMs", + "statusCode", + "errorCode", + "metadataExpiresAt" ] }, - "SessionBatchStopResult": { - "type": "object", - "additionalProperties": false, - "properties": { - "stopping": { - "type": "integer", - "minimum": 0, - "maximum": 100 + "JsonValue": { + "nullable": true, + "oneOf": [ + { + "type": "string", + "nullable": true, + "enum": [ + null + ] + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/JsonValue" + } + }, + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + } } - }, - "required": [ - "stopping" ] }, - "SessionBatchRemoveResult": { + "OperationCapabilities": { "type": "object", "additionalProperties": false, "properties": { - "removed": { - "type": "integer", - "minimum": 0, - "maximum": 100 + "cancellable": { + "type": "boolean" + }, + "watchable": { + "type": "boolean" } }, "required": [ - "removed" + "cancellable", + "watchable" ] }, - "TestingSessionId": { - "type": "string", - "pattern": "^test_[A-Za-z0-9_-]{16}$", - "description": "Generated testing-session slug." - }, - "ProductionEnrollmentRequest": { + "OperationProgress": { "type": "object", "additionalProperties": false, "properties": { - "business": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 2, - "maxLength": 120 - }, - "website": { - "type": "string", - "format": "uri", - "pattern": "^https://", - "maxLength": 2048 - }, - "supportEmail": { - "type": "string", - "format": "email", - "maxLength": 320 - } - }, - "required": [ - "name", - "website", - "supportEmail" - ] + "code": { + "type": "string" + }, + "current": { + "type": "integer", + "nullable": true + }, + "total": { + "type": "integer", + "nullable": true } }, "required": [ - "business" + "code", + "current", + "total" ] }, - "ProductionEnrollmentResult": { + "OperationError": { "type": "object", "additionalProperties": false, "properties": { - "id": { - "$ref": "#/components/schemas/ProjectId" - }, - "orgId": { - "type": "string", - "format": "uuid" - }, - "name": { + "code": { "type": "string" }, - "slug": { - "type": "string" - }, - "stage": { - "type": "string", - "enum": [ - "development" - ] - }, - "operationId": { - "type": "string", - "format": "uuid" + "retryable": { + "type": "boolean" }, - "enrollmentStatus": { - "type": "string", - "enum": [ - "requested", - "approval_required", - "provisioning", - "ready" - ] + "details": { + "type": "object", + "nullable": true, + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + } } }, "required": [ - "id", - "orgId", - "name", - "slug", - "stage", - "operationId", - "enrollmentStatus" + "code", + "retryable", + "details" ] }, - "ProductionEnrollmentCommandResult": { + "OperationActionRequired": { "type": "object", "additionalProperties": false, "properties": { - "operationId": { - "type": "string", - "format": "uuid" - }, - "action": { - "type": "string", - "enum": [ - "approve", - "cancel" - ] + "code": { + "type": "string" }, - "accepted": { - "type": "boolean", - "enum": [ - true - ] + "details": { + "type": "object", + "nullable": true, + "additionalProperties": { + "$ref": "#/components/schemas/JsonValue" + } } }, "required": [ - "operationId", - "action", - "accepted" + "code", + "details" ] }, - "ManagementOperation": { + "OrganizationOperation": { "type": "object", "additionalProperties": false, "properties": { @@ -11324,19 +20813,47 @@ "type": "string", "format": "uuid" }, - "kind": { - "type": "string" - }, - "resourceType": { - "type": "string" - }, - "resourceId": { - "type": "string" + "organizationId": { + "type": "string", + "format": "uuid" }, "projectId": { "type": "string", - "format": "uuid", - "nullable": true + "nullable": true, + "enum": [ + null + ] + }, + "kind": { + "type": "string", + "enum": [ + "auth_projection_repair", + "session_lifecycle", + "auth_session_purge", + "label_projection_purge", + "billing_reconciliation", + "auto_top_up", + "campaign", + "production_enrollment", + "retention_sweep", + "webhook_redrive" + ] + }, + "resource": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ] }, "status": { "type": "string", @@ -11344,18 +20861,51 @@ "pending", "running", "action_required", + "cancelling", "succeeded", "failed", "cancelled" ] }, - "progressCode": { - "type": "string", - "nullable": true + "sequence": { + "type": "integer", + "format": "int64", + "minimum": 1 }, - "failureCode": { - "type": "string", - "nullable": true + "capabilities": { + "$ref": "#/components/schemas/OperationCapabilities" + }, + "progress": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationProgress" + } + ] + }, + "result": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/JsonValue" + } + ] + }, + "error": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationError" + } + ] + }, + "actionRequired": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationActionRequired" + } + ] }, "createdAt": { "type": "string", @@ -11373,695 +20923,619 @@ }, "required": [ "id", - "kind", - "resourceType", - "resourceId", + "organizationId", "projectId", + "kind", + "resource", "status", - "progressCode", - "failureCode", + "sequence", + "capabilities", + "progress", + "result", + "error", + "actionRequired", "createdAt", "updatedAt", "completedAt" ] }, - "CustomerId": { - "type": "string", - "format": "uuid", - "description": "Project-owned Customer UUID." - }, - "Customer": { + "ProjectOperation": { "type": "object", "additionalProperties": false, "properties": { "id": { - "$ref": "#/components/schemas/CustomerId" - }, - "orgId": { "type": "string", "format": "uuid" }, - "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "name": { - "type": "string", - "nullable": true, - "maxLength": 120 - }, - "phone": { - "type": "string", - "nullable": true, - "pattern": "^\\+[1-9][0-9]{6,14}$" - }, - "externalCustomerId": { - "type": "string", - "nullable": true, - "maxLength": 200 - }, - "status": { - "type": "string", - "enum": [ - "active", - "archiving", - "archived" - ] - }, - "isDefault": { - "type": "boolean" - }, - "archivedAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "createdAt": { - "type": "integer", - "format": "int64" - }, - "updatedAt": { - "type": "integer", - "format": "int64" - } - }, - "required": [ - "id", - "orgId", - "projectId", - "name", - "phone", - "externalCustomerId", - "status", - "isDefault", - "archivedAt", - "createdAt", - "updatedAt" - ] - }, - "CustomerSummary": { - "type": "object", - "additionalProperties": false, - "properties": { - "id": { - "$ref": "#/components/schemas/CustomerId" - }, - "orgId": { + "organizationId": { "type": "string", "format": "uuid" }, "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "name": { "type": "string", - "nullable": true, - "maxLength": 120 + "format": "uuid" }, - "phoneMasked": { + "kind": { "type": "string", - "nullable": true + "enum": [ + "auth_projection_repair", + "session_lifecycle", + "auth_session_purge", + "label_projection_purge", + "billing_reconciliation", + "auto_top_up", + "campaign", + "production_enrollment", + "retention_sweep", + "webhook_redrive" + ] }, - "externalCustomerId": { - "type": "string", - "nullable": true, - "maxLength": 200 + "resource": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "required": [ + "type", + "id" + ] }, "status": { "type": "string", "enum": [ - "active", - "archiving", - "archived" + "pending", + "running", + "action_required", + "cancelling", + "succeeded", + "failed", + "cancelled" ] }, - "isDefault": { - "type": "boolean" - }, - "archivedAt": { + "sequence": { "type": "integer", "format": "int64", - "nullable": true + "minimum": 1 }, - "numberCount": { - "type": "integer", - "minimum": 0 + "capabilities": { + "$ref": "#/components/schemas/OperationCapabilities" }, - "connectedNumberCount": { - "type": "integer", - "minimum": 0 + "progress": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationProgress" + } + ] }, - "activePairingLinkState": { - "type": "string", - "nullable": true + "result": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/JsonValue" + } + ] }, - "lastActivityAt": { - "type": "integer", - "format": "int64", - "nullable": true + "error": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationError" + } + ] }, - "needsAttention": { - "type": "boolean" + "actionRequired": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationActionRequired" + } + ] }, "createdAt": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "date-time" }, "updatedAt": { - "type": "integer", - "format": "int64" + "type": "string", + "format": "date-time" + }, + "completedAt": { + "type": "string", + "format": "date-time", + "nullable": true } }, "required": [ "id", - "orgId", + "organizationId", "projectId", - "name", - "phoneMasked", - "externalCustomerId", + "kind", + "resource", "status", - "isDefault", - "archivedAt", - "numberCount", - "connectedNumberCount", - "activePairingLinkState", - "lastActivityAt", - "needsAttention", + "sequence", + "capabilities", + "progress", + "result", + "error", + "actionRequired", "createdAt", - "updatedAt" + "updatedAt", + "completedAt" ] }, - "CustomersStatus": { + "OperationTransition": { "type": "object", "additionalProperties": false, "properties": { - "enabled": { - "type": "boolean" + "operationId": { + "type": "string", + "format": "uuid" }, - "enabledAt": { + "sequence": { "type": "integer", "format": "int64", - "nullable": true + "minimum": 1 }, - "enabledBy": { + "fromStatus": { "type": "string", + "enum": [ + "pending", + "running", + "action_required", + "cancelling", + "succeeded", + "failed", + "cancelled" + ], "nullable": true }, - "defaultCustomer": { - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/Customer" - } - ], + "toStatus": { + "type": "string", + "enum": [ + "pending", + "running", + "action_required", + "cancelling", + "succeeded", + "failed", + "cancelled" + ] + }, + "reasonCode": { + "type": "string", "nullable": true - } - }, - "required": [ - "enabled", - "enabledAt", - "enabledBy", - "defaultCustomer" - ] - }, - "CustomersEnablement": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomersStatus" }, - { + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "snapshot": { "type": "object", + "additionalProperties": false, "properties": { - "migratedNumberCount": { - "type": "integer", - "minimum": 0 + "progress": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationProgress" + } + ] + }, + "error": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationError" + } + ] + }, + "actionRequired": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/OperationActionRequired" + } + ] } }, "required": [ - "migratedNumberCount" + "progress", + "error", + "actionRequired" ] } + }, + "required": [ + "operationId", + "sequence", + "fromStatus", + "toStatus", + "reasonCode", + "occurredAt", + "snapshot" ] }, - "CustomerProjectRequest": { - "type": "object", - "additionalProperties": false, - "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" - } - } - }, - "CreateCustomerRequest": { + "IdempotencyReceipt": { "type": "object", "additionalProperties": false, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" + "id": { + "type": "string", + "format": "uuid" }, - "name": { + "key": { "type": "string", - "nullable": true, "minLength": 1, - "maxLength": 120 + "maxLength": 255 }, - "phone": { + "replayed": { + "type": "boolean" + }, + "createdAt": { "type": "string", - "nullable": true, - "description": "Phone number normalized to E.164." + "format": "date-time" }, - "externalCustomerId": { + "expiresAt": { "type": "string", - "nullable": true, - "minLength": 1, - "maxLength": 200 + "format": "date-time" } - } + }, + "required": [ + "id", + "key", + "replayed", + "createdAt", + "expiresAt" + ] }, - "UpdateCustomerRequest": { + "ReplayEventInput": { "type": "object", "additionalProperties": false, - "minProperties": 1, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "name": { - "type": "string", - "nullable": true, - "minLength": 1, - "maxLength": 120 - }, - "phone": { + "webhookId": { "type": "string", - "nullable": true, - "description": "Phone number normalized to E.164." + "format": "uuid" + } + }, + "required": [ + "webhookId" + ] + }, + "RetryWebhookDeliveryInput": { + "type": "object", + "additionalProperties": false, + "maxProperties": 0 + }, + "TestWebhookInput": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "eventType": { + "type": "string", + "default": "polymorfa.test", + "pattern": "^[a-z0-9][a-z0-9_.-]{0,127}$" + } + } }, - "externalCustomerId": { - "type": "string", - "nullable": true, - "minLength": 1, - "maxLength": 200 + { + "type": "object", + "additionalProperties": false, + "properties": { + "eventType": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9_.-]{0,127}$" + }, + "body": { + "$ref": "#/components/schemas/EncodedEventPayload" + }, + "sessionId": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "body", + "sessionId" + ] } - } + ] }, - "CustomerNumber": { + "OrganizationEventReplayReceipt": { "type": "object", "additionalProperties": false, "properties": { - "id": { + "operationId": { "type": "string", "format": "uuid" }, - "customerId": { - "$ref": "#/components/schemas/CustomerId" - }, - "sessionId": { - "type": "string" - }, - "name": { - "type": "string", - "nullable": true + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" }, - "phoneMasked": { + "eventId": { "type": "string", - "nullable": true - }, - "status": { - "type": "string" + "format": "uuid" }, - "backend": { + "deliveryId": { "type": "string", - "nullable": true - }, - "createdAt": { - "type": "integer", - "format": "int64" + "format": "uuid" } }, "required": [ - "id", - "customerId", - "sessionId", - "name", - "phoneMasked", - "status", - "backend", - "createdAt" + "eventId", + "deliveryId", + "operationId", + "idempotency" ] }, - "CustomerEvent": { + "ProjectEventReplayReceipt": { "type": "object", "additionalProperties": false, "properties": { - "id": { + "operationId": { "type": "string", "format": "uuid" }, - "action": { - "type": "string", - "pattern": "^customer(?:\\.[a-z0-9_]+){1,3}$", - "maxLength": 128 - }, - "fromStatus": { - "type": "string", - "nullable": true - }, - "toStatus": { - "type": "string", - "nullable": true + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" }, - "sessionId": { + "eventId": { "type": "string", - "format": "uuid", - "nullable": true + "format": "uuid" }, - "pairingLinkId": { + "deliveryId": { "type": "string", - "format": "uuid", - "nullable": true - }, - "metadata": { - "type": "object", - "additionalProperties": false, - "properties": { - "fields": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ - "name", - "phone", - "externalCustomerId" - ] - } - } - } - }, - "occurredAt": { - "type": "integer", - "format": "int64" + "format": "uuid" } }, "required": [ - "id", - "action", - "fromStatus", - "toStatus", - "sessionId", - "pairingLinkId", - "metadata", - "occurredAt" + "eventId", + "deliveryId", + "operationId", + "idempotency" ] }, - "CustomerPairingLink": { + "OrganizationWebhookDeliveryRetryReceipt": { "type": "object", "additionalProperties": false, "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "orgId": { + "operationId": { "type": "string", "format": "uuid" }, - "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "customerId": { - "$ref": "#/components/schemas/CustomerId" - }, - "expectedPhoneMasked": { - "type": "string", - "nullable": true - }, - "methods": { - "type": "array", - "minItems": 1, - "maxItems": 2, - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ - "qr", - "phone" - ] - } + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" }, - "locale": { + "deliveryId": { "type": "string", - "enum": [ - "en", - "pt-BR" - ], - "nullable": true + "format": "uuid" }, - "theme": { + "attemptId": { "type": "string", - "enum": [ - "light", - "dark", - "system" - ], - "nullable": true - }, - "expiresAt": { - "type": "integer", - "format": "int64" - }, - "status": { + "format": "uuid" + } + }, + "required": [ + "deliveryId", + "attemptId", + "operationId", + "idempotency" + ] + }, + "ProjectWebhookDeliveryRetryReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "operationId": { "type": "string", - "enum": [ - "active", - "opened", - "connecting", - "connected", - "failed", - "expired", - "revoked" - ] - }, - "attemptCount": { - "type": "integer", - "minimum": 0 - }, - "maxAttempts": { - "type": "integer", - "minimum": 1, - "maximum": 20 + "format": "uuid" }, - "pendingSessionId": { - "type": "string", - "format": "uuid", - "nullable": true + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" }, - "createdBy": { + "deliveryId": { "type": "string", - "nullable": true - }, - "reservedAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "openedAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "connectingAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "connectedAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "failedAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "expiredAt": { - "type": "integer", - "format": "int64", - "nullable": true - }, - "revokedAt": { - "type": "integer", - "format": "int64", - "nullable": true + "format": "uuid" }, - "lastErrorCode": { + "attemptId": { "type": "string", - "nullable": true - }, - "failedExchangeCount": { - "type": "integer", - "minimum": 0, - "maximum": 5 - }, - "phoneMismatchCount": { - "type": "integer", - "minimum": 0, - "maximum": 3 - }, - "createdAt": { - "type": "integer", - "format": "int64" - }, - "updatedAt": { - "type": "integer", - "format": "int64" + "format": "uuid" } }, - "required": [ - "id", - "orgId", - "projectId", - "customerId", - "expectedPhoneMasked", - "methods", - "locale", - "theme", - "expiresAt", - "status", - "attemptCount", - "maxAttempts", - "pendingSessionId", - "createdBy", - "reservedAt", - "openedAt", - "connectingAt", - "connectedAt", - "failedAt", - "expiredAt", - "revokedAt", - "lastErrorCode", - "failedExchangeCount", - "phoneMismatchCount", - "createdAt", - "updatedAt" + "required": [ + "deliveryId", + "attemptId", + "operationId", + "idempotency" ] }, - "CreatedCustomerPairingLink": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerPairingLink" + "OrganizationWebhookTestReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "operationId": { + "type": "string", + "format": "uuid" }, - { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "nullable": true, - "description": "Returned once; null on an idempotent replay." - } - }, - "required": [ - "url" - ] + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + }, + "eventId": { + "type": "string", + "format": "uuid" + }, + "deliveryId": { + "type": "string", + "format": "uuid" } + }, + "required": [ + "eventId", + "deliveryId", + "operationId", + "idempotency" ] }, - "CreateCustomerPairingLinkRequest": { + "ProjectWebhookTestReceipt": { "type": "object", "additionalProperties": false, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" - }, - "expectedPhone": { + "operationId": { "type": "string", - "nullable": true, - "description": "Optional expected phone, normalized to E.164." + "format": "uuid" }, - "methods": { - "type": "array", - "minItems": 1, - "maxItems": 2, - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ - "qr", - "phone" - ] - } + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" }, - "expiresInSeconds": { - "type": "integer", - "minimum": 900, - "maximum": 604800, - "default": 86400 + "eventId": { + "type": "string", + "format": "uuid" }, - "locale": { + "deliveryId": { "type": "string", - "enum": [ - "en", - "pt-BR" - ], - "nullable": true + "format": "uuid" + } + }, + "required": [ + "eventId", + "deliveryId", + "operationId", + "idempotency" + ] + }, + "OrganizationOperationCancellationReceipt": { + "type": "object", + "additionalProperties": false, + "properties": { + "operation": { + "$ref": "#/components/schemas/OrganizationOperation" }, - "theme": { + "operationId": { "type": "string", - "enum": [ - "light", - "dark", - "system" - ], - "nullable": true + "format": "uuid" + }, + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" } - } + }, + "required": [ + "operation", + "operationId", + "idempotency" + ] }, - "TransferCustomerNumberRequest": { + "ProjectOperationCancellationReceipt": { "type": "object", "additionalProperties": false, "properties": { - "projectId": { - "$ref": "#/components/schemas/ProjectId" + "operation": { + "$ref": "#/components/schemas/ProjectOperation" }, - "sourceCustomerId": { - "$ref": "#/components/schemas/CustomerId" + "operationId": { + "type": "string", + "format": "uuid" }, - "confirm": { - "type": "boolean", + "idempotency": { + "$ref": "#/components/schemas/IdempotencyReceipt" + } + }, + "required": [ + "operation", + "operationId", + "idempotency" + ] + }, + "OperationConflictEnvelope": { + "type": "object", + "additionalProperties": false, + "properties": { + "error": { + "type": "string", "enum": [ - true + "operation_conflict" + ] + }, + "code": { + "type": "string", + "enum": [ + "operation_conflict" + ] + }, + "operation": { + "oneOf": [ + { + "$ref": "#/components/schemas/OrganizationOperation" + }, + { + "$ref": "#/components/schemas/ProjectOperation" + } ] } }, "required": [ - "projectId", - "sourceCustomerId", - "confirm" + "error", + "code", + "operation" ] }, - "CursorPageMetadata": { + "IdempotencyConflictEnvelope": { "type": "object", "additionalProperties": false, "properties": { - "nextCursor": { + "error": { "type": "string", - "nullable": true + "enum": [ + "idempotency_conflict" + ] }, - "hasMore": { - "type": "boolean" + "code": { + "type": "string", + "enum": [ + "idempotency_conflict" + ] } }, "required": [ - "nextCursor", - "hasMore" + "error", + "code" + ] + }, + "OperationCancellationConflictEnvelope": { + "oneOf": [ + { + "$ref": "#/components/schemas/OperationConflictEnvelope" + }, + { + "$ref": "#/components/schemas/IdempotencyConflictEnvelope" + } ] } }, "responses": { "Unauthorized": { "description": "Missing/invalid credential for this audience.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -12072,6 +21546,44 @@ }, "Forbidden": { "description": "Authenticated but lacks the required scope/role/audience.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "Gone": { + "description": "The requested retained payload is no longer available.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -12082,6 +21594,20 @@ }, "BadRequest": { "description": "A path, query, or request-body value did not pass validation.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -12092,6 +21618,20 @@ }, "NotFound": { "description": "The requested resource was not found inside the authenticated organization or project scope.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -12102,6 +21642,44 @@ }, "Conflict": { "description": "The request conflicts with the resource's current state.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "Unprocessable": { + "description": "The retained payload is unavailable or no longer replayable.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -12110,8 +21688,46 @@ } } }, + "OperationCancellationConflict": { + "description": "The operation cannot accept cancellation, or the idempotency key was reused for a different request.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationCancellationConflictEnvelope" + } + } + } + }, "InternalError": { "description": "The session-control publisher or testing-number persistence path is temporarily unavailable.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { @@ -12122,6 +21738,20 @@ }, "NotImplemented": { "description": "Endpoint registered; handler not yet wired to a repository.", + "headers": { + "X-Request-Id": { + "schema": { + "type": "string" + }, + "description": "Stable request correlation identifier." + }, + "Polymorfa-Version": { + "schema": { + "type": "string" + }, + "description": "API version applied to this response." + } + }, "content": { "application/json": { "schema": { diff --git a/docs/superpowers/specs/2026-09-05-calls-ui-design.md b/docs/superpowers/specs/2026-09-05-calls-ui-design.md new file mode 100644 index 00000000..35b43800 --- /dev/null +++ b/docs/superpowers/specs/2026-09-05-calls-ui-design.md @@ -0,0 +1,119 @@ +# Calls UI design + +**Date:** 2026-09-05 +**Status:** Shipped on `dev` in `@polymorfa/react` (`CallSurface`, +`IncomingCallCard`, `CallStage`, `CallControls`, `DialPad`) over the +`@polymorfa/browser` `CallsController`. + +## Outcome + +One call UI for both calling lines — a paired WhatsApp device session +(`linkedDevice`, audio and video) and the WhatsApp Business Calling API +(`cloudApi`, audio only) — behind a single capability-gated call model. +Components read `snapshot.capabilities`; nothing branches on the line name. + +## Design language + +Monochrome Material 3: black identity, white surfaces, tonal fills, hairline +outlines, 8px radius, Roboto with the appearance font as override. The verbs +carry the only color — success green answers or places a call, danger red +declines or hangs up. Cards follow the appearance theme (`light`, `dark`, or +`system`); the call stage, and the incoming card once it shows a camera +preview, is always dark like an OS call screen. + +Colors resolve from the shared `--pmfa-*` appearance variables with the +monochrome palette as fallbacks: `--pmfa-color-foreground` is the identity +accent, `--pmfa-color-background` / `--pmfa-color-border` / `--pmfa-color-muted` +shape cards, `--pmfa-color-success` and `--pmfa-color-danger` are the verbs, +`--pmfa-radius-medium` and `--pmfa-font-family` carry shape and type. The +`call` entry of `appearance.elements` adds a class and inline styles to every +call root. + +## Layout (after the official desktop call windows) + +- **Incoming card** — big centered name, muted "WhatsApp audio/video call" + subtitle, then the media middle: the avatar for audio offers, a mirrored + self-preview with camera and mute toggles and a ⋯ device menu for video + offers. Two labeled verbs close the card: Reject, Answer. Voice offers keep + the same 16/10 footprint as video offers and carry no mute row. +- **Stage** — remote video full-bleed when the far side sends video, the + centered avatar hero otherwise, the local preview as a picture-in-picture + tile; before connecting, an outgoing video call shows the identity above an + inset self-preview. The line under the name reads "Ringing +number…" while + an outgoing call waits, then alternates number ⇄ duration every five seconds. +- **Dock** — split pills `[camera|⌄]` and `[mic|⌄]`: the toggle is the + action, the chevron opens the device panel (camera picks the camera; mic + picks microphone and speaker). The camera pill is disabled until the call + connects and on lines without video. Red hang-up pill on the right. +- **Dial pad** — digits only (no letter sub-labels), E.164 entry, audio call + always, video call only where the line carries video. +- **Pop-out** — `CallSurface` offers a ↗ button that moves the stage and dock + into a separate resizable window; closing it, or the call ending, brings + the call back inline. + +## Behavior rules + +- Video is per direction: enabling the local camera never renegotiates the + peer's stream. Answering a video offer with the camera toggled off still + acquires video muted so the in-call toggle can enable it. +- Device panels overlay the widget and never resize it. Device picks apply + live: a microphone or camera change replaces the outgoing track in place + and mute state carries over; the speaker pick uses `setSinkId` where the + browser supports it. +- No hover growth, no press bounce, no drop shadows. All motion (answer pulse, + dock rise) is disabled under `prefers-reduced-motion: reduce`. +- Avatars: a resolved profile picture wins; otherwise a stable hash of the + caller id picks one of eight muted palette tones so the same caller always + gets the same color. +- Examples and documentation use fictional numbers and personas only. + +## Wire contract + +Signaling is the REST surface on the client token — offer/answer, trickle ICE +submission and polling, idempotent teardown — through +`createSignalingCallsBackend`. Inbound calls arrive by either of two paths, +and an application picks one: `CallsSocket` subscribes to the lifecycle +directly and needs no webhook plumbing (the Transport addendum below), or the +application relays the server's `call.received` webhook into +`IncomingCallRelay`, with `incomingCallFromWebhook` mapping the payload. The +relay is the fallback for deployments that already carry webhooks to the +browser, not a prerequisite. The server mints the browser token +with `MessagingClient.voip.token` after granting the session's client rules the +`voip_place`, `voip_answer`, and `voip_signal` actions. + +### Outbound placement (2026-09-06) + +`voip_place` authorizes browser signaling for an outbound call; it does not +create one. Nothing on the client-token surface dials a destination — `/offer` +carries only SDP, and it reaches a pod through the call's affinity record, so a +call id the browser invents resolves to no pod and its offer is refused as +not-ready for as long as it is retried. Starting a call is a server-key +operation on the Business Calling surface (`action: "connect"`). + +Outbound calls therefore run through the backend's `place` hook: + +1. The browser posts the destination to the application's own route. +2. That route places the call with the server SDK, on the server credential. +3. It answers with the platform's call id. +4. The browser attaches media to that id, and only then does signaling apply. + +Without the hook the placement fails, which the controller reports the way it +reports every call failure — an `error` snapshot with code `place_failed`. +`place()` itself resolves either way, so the snapshot is what an integrator +watches, not a rejected promise — it rejects only when the controller has been +disposed, which a UI holding a shared controller should still absorb. An application that only answers inbound +calls needs no such route. + +## Transport (2026-09-06 addendum) + +The kit no longer depends on the application relaying webhooks. `CallsSocket` +opens the calls WebSocket with a single-use ticket (`POST /api/voip/ws-ticket`) +and is both the backend's lifecycle source (incoming, accepted, ended with the +pod's reason mapped onto the controller vocabulary) and the media factory's +`candidateTransport`. REST candidate polling pauses while the socket is up and +resumes while it reconnects; `IncomingCallRelay` remains for deployments that +prefer their own channel. The camera button on an audio call now calls +`controller.enableVideo()` (camera + re-offer on the same connection), and a +dropped connection renders the existing "Reconnecting" message while the +controller restarts ICE inside a 15 s resumption window. None of this changes +the visual design. diff --git a/examples/react/App.tsx b/examples/react/App.tsx index ae8e9489..6dfb033c 100644 --- a/examples/react/App.tsx +++ b/examples/react/App.tsx @@ -25,3 +25,89 @@ export function App() { ); } + +// ── Calls ───────────────────────────────────────────────────────────── +// +// The browser signs into the signaling surface with a client token minted by +// your server (`MessagingClient.voip.token`). Inbound calls reach your server +// as the `call.received` webhook; relay them to the browser and hand them to +// the relay below. +import { + BrowserTransport, + CallsController, + CallsSignalingClient, + IncomingCallRelay, + WebRtcMediaFactory, + createClientTokenProvider, + createSignalingCallsBackend, + incomingCallFromWebhook, + type CallEndReason, + type CallReceivedWebhookPayload, +} from "@polymorfa/browser"; +import { CallSurface, DialPad } from "@polymorfa/react"; + +const callTransport = new BrowserTransport({ + getClientToken: createClientTokenProvider(), +}); +const callSignaling = new CallsSignalingClient(callTransport); +// Webhook-only integration: the application relays BOTH `call.received` and +// `call.ended` from its own realtime channel. (With `CallsSocket` as the +// backend's `incoming` source neither handler is needed — the socket pushes +// the lifecycle itself.) +export const incomingCalls = new IncomingCallRelay(); +const calls = new CallsController( + createSignalingCallsBackend({ + signaling: callSignaling, + incoming: incomingCalls, + // Outbound calls start on the server: the client token cannot dial a + // destination. This route places the call with the server SDK and answers + // with the platform's call id, which is what media then attaches to. + place: async ({ to, video, line, idempotencyKey }, signal) => { + const response = await fetch("/api/calls/place", { + method: "POST", + headers: { + "content-type": "application/json", + // The controller mints one key per placement. Forward it to the + // server SDK call so a retried request cannot start a second call. + "idempotency-key": idempotencyKey, + }, + body: JSON.stringify({ to, video, line }), + signal, + }); + if (!response.ok) throw new Error("The call could not be placed."); + const { callId } = (await response.json()) as { callId: string }; + return callId; + }, + }), + new WebRtcMediaFactory({ signaling: callSignaling }), +); +calls.initialize(); + +/** Wire this to your realtime channel that forwards the `call.received` webhook payload. */ +export function onCallReceivedWebhook( + payload: CallReceivedWebhookPayload, +): void { + incomingCalls.receive(incomingCallFromWebhook(payload)); +} + +/** Wire this to the same channel for `call.ended`, so a remote hang-up ends the UI promptly. */ +export function onCallEndedWebhook(payload: { + callId: string; + reason?: CallEndReason; +}): void { + incomingCalls.ended(payload.callId, payload.reason); +} + +export function CallsApp() { + return ( + + + + peer === "+12025550123" ? "Casey Rivera" : undefined + } + /> + + ); +} diff --git a/packages/browser/README.md b/packages/browser/README.md index 9080a9a4..c042b306 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -39,9 +39,23 @@ routes directly. Available controller families are QuickLink, conversations and composing, template building, and calls. Calls use `CallsSignalingClient` for the -`voip-v2` offer/answer and trickle ICE REST paths plus `WebRtcMediaFactory` for -browser media. Applications supply `CallsBackend` for lifecycle and call-control -operations that are not part of that REST signaling surface. +`/api/voip/calls/{id}` offer, candidate, renegotiate, and teardown routes and +for socket tickets, `CallsSocket` for the calls WebSocket (pushed `call.*` +lifecycle events and ICE both ways, reconnecting with backoff and falling +back to REST while down), `WebRtcMediaFactory` for the peer connection, +media, device switching, audio→video upgrade and ICE restart, and +`createSignalingCallsBackend` as the `CallsBackend` over that surface. Nothing +on the client-token surface dials a destination, so that backend takes a +`place` hook: it posts the destination to the application's own route, which +starts the call with the server SDK and returns the platform's call id. An +application without the socket can still relay its webhooks through +`IncomingCallRelay`; it must forward both `call.received` (`receive`) and +`call.ended` (`ended`), or a remote hang-up never reaches the controller. Reject and hang-up run through the idempotent +teardown route. A pod-lost (410) or capacity (503) answer to the _media offer_ +ends the call as `pod_lost` or `capacity` instead of erroring; the same codes +from placement or teardown are ordinary failures and stay recoverable, so a +503 from your own `place` route is a `place_failed` the caller can retry. `CallsController` exposes `enableVideo()` and a +`reconnecting` status with a bounded resumption window. ## Template builder diff --git a/packages/browser/src/calls/backend.ts b/packages/browser/src/calls/backend.ts new file mode 100644 index 00000000..eaf016dd --- /dev/null +++ b/packages/browser/src/calls/backend.ts @@ -0,0 +1,175 @@ +import type { + CallEndReason, + CallLifecycleEvent, + CallLine, + CallsBackend, + IncomingCall, + PlaceCallInput, +} from "./controller.js"; +import type { CallsSignaling } from "./signaling.js"; + +/** + * The parts of the `call.received` webhook payload the browser needs. The + * caller is a JID reference; a bare string is accepted for hand-built + * payloads. + */ +export interface CallReceivedWebhookPayload { + readonly callId: string; + readonly from: + | string + | { + readonly id?: string; + readonly phoneNumber?: string; + readonly lid?: string; + }; + readonly hasVideo?: boolean; +} + +/** + * Turn a `call.received` webhook payload into an {@link IncomingCall}. The + * application receives the webhook on its server, relays it to the browser + * over its own realtime channel, and hands it to {@link IncomingCallRelay}. + * The caller is shown by phone number when present, else by LID, else by + * raw JID. + */ +export function incomingCallFromWebhook( + payload: CallReceivedWebhookPayload, + options: { readonly line?: CallLine } = {}, +): IncomingCall { + // Empty strings fall through like absent fields, matching `peerFrom` on the + // socket path — the two inbound routes must agree on the same payload. + const from = + typeof payload.from === "string" + ? payload.from + : (firstNonEmpty( + payload.from.phoneNumber, + payload.from.lid, + payload.from.id, + ) ?? ""); + return { + callId: payload.callId, + from, + video: payload.hasVideo === true, + line: options.line ?? "linkedDevice", + }; +} + +/** + * Application-fed source of inbound call notifications for deployments that + * relay `call.received` (and optionally `call.ended`) webhook events over + * their own realtime channel. {@link CallsSocket} is the push alternative: + * it subscribes to the same lifecycle stream directly from the API. + */ +export class IncomingCallRelay { + readonly #listeners = new Set<(event: CallLifecycleEvent) => void>(); + + /** Announce an inbound call; the controller moves to `incoming`. */ + receive(call: IncomingCall): void { + this.#emit({ type: "incomingCall", call }); + } + + /** Announce that the remote side ended a call (from `call.ended`). */ + ended(callId: string, reason?: CallEndReason): void { + this.#emit( + reason === undefined + ? { type: "ended", callId } + : { type: "ended", callId, reason }, + ); + } + + subscribe(listener: (event: CallLifecycleEvent) => void): () => void { + this.#listeners.add(listener); + return () => this.#listeners.delete(listener); + } + + #emit(event: CallLifecycleEvent): void { + for (const listener of [...this.#listeners]) listener(event); + } +} + +export interface SignalingCallsBackendOptions { + /** REST signaling client for the `/api/voip/calls/{id}` paths. */ + readonly signaling: CallsSignaling; + /** Inbound call notifications: a {@link CallsSocket} or an application-fed {@link IncomingCallRelay}. */ + readonly incoming: Pick; + /** + * Places an outbound call through the application's own server and resolves + * the call id the platform assigned it. + * + * The browser signaling surface has no route that dials a destination: the + * client token can only attach media to a call the platform already owns + * (`/offer` reaches the pod through the call's affinity record, so an id the + * browser invented resolves to no pod). Starting an outbound call is a + * server-key operation, so the application posts the destination to its own + * endpoint, that endpoint places the call with the server SDK, and the + * resulting id comes back here. Without it outbound calling is unavailable: + * this hook throws, and the controller turns that into a `place_failed` + * error snapshot rather than a rejected promise. (`CallsController.place()` + * rejects only when the controller has been disposed.) + */ + readonly place?: ( + input: PlaceCallInput, + signal: AbortSignal, + ) => Promise; +} + +/** + * A {@link CallsBackend} over the REST signaling surface alone. Answering an + * inbound call posts an SDP offer for the announced call id (the media factory + * does that); reject and hang-up release the pod's session with the idempotent + * teardown route. No server API key is involved: everything the browser does + * runs on the client token. + * + * Outbound calls are the exception. Nothing on the client-token surface starts + * a call, so {@link SignalingCallsBackendOptions.place} is what turns them on: + * it hands the destination to the application's server, which places the call + * and returns the platform's call id for media to attach to. + */ +export function createSignalingCallsBackend( + options: SignalingCallsBackendOptions, +): CallsBackend { + return { + subscribe: (listener) => options.incoming.subscribe(listener), + place: async (input: PlaceCallInput, signal: AbortSignal) => { + throwIfAborted(signal); + const placeWith = options.place; + if (placeWith === undefined) + throw new Error( + "Outbound calling needs a `place` hook: the browser signaling surface " + + "cannot dial a destination, so the call must be started by your " + + "server and its call id returned to the browser.", + ); + const placed = await placeWith(input, signal); + throwIfAborted(signal); + // `place` is application code behind a public interface: a response that + // did not match its declared shape must not reach signaling as an + // `undefined` call id. + const callId = + typeof placed === "string" + ? placed + : (placed as { readonly callId?: unknown } | null)?.callId; + if (typeof callId !== "string" || callId === "") + throw new Error("`place` resolved without a call id."); + return { callId }; + }, + answer: async (_callId: string, signal: AbortSignal) => { + throwIfAborted(signal); + }, + reject: (callId: string, signal: AbortSignal) => + options.signaling.teardown(callId, signal), + hangup: (callId: string, signal: AbortSignal) => + options.signaling.teardown(callId, signal), + }; +} + +function firstNonEmpty( + ...values: readonly (string | undefined)[] +): string | undefined { + for (const value of values) + if (typeof value === "string" && value.length > 0) return value; + return undefined; +} + +function throwIfAborted(signal: AbortSignal): void { + if (signal.aborted) throw signal.reason; +} diff --git a/packages/browser/src/calls/controller.ts b/packages/browser/src/calls/controller.ts index 30a73f6d..ca7eea92 100644 --- a/packages/browser/src/calls/controller.ts +++ b/packages/browser/src/calls/controller.ts @@ -12,6 +12,7 @@ export type CallStatus = | "accepted" | "connecting" | "connected" + | "reconnecting" | "ended" | "error"; export type CallEndReason = @@ -23,11 +24,44 @@ export type CallEndReason = | "ice_timeout" | "capacity" | "remote_hangup" + | "pod_lost" | (string & {}); +/** + * Which Polymorfa calling line carries a call: a linked WhatsApp device + * session (audio + video) or the WhatsApp Business Calling API (audio only). + */ +export type CallLine = "linkedDevice" | "cloudApi"; + +export interface CallCapabilities { + readonly video: boolean; + readonly mute: boolean; +} + +export function capabilitiesFor(line: CallLine): CallCapabilities { + return { video: line !== "cloudApi", mute: true }; +} + +export type CallDeviceKind = "audioinput" | "videoinput" | "audiooutput"; + +export interface CallDevice { + readonly deviceId: string; + readonly kind: CallDeviceKind; + readonly label: string; +} + +/** Capture/playback device choices. `audioOutput` needs `setSinkId` support. */ +export interface SelectedCallDevices { + readonly audioInput?: string; + readonly videoInput?: string; + readonly audioOutput?: string; +} + export interface IncomingCall { readonly callId: string; readonly from: string; readonly video: boolean; + /** Defaults to `linkedDevice`. */ + readonly line?: CallLine; } export type CallLifecycleEvent = | { readonly type: "incomingCall"; readonly call: IncomingCall } @@ -48,6 +82,7 @@ export type CallLifecycleEvent = export interface PlaceCallInput { readonly to: string; readonly video: boolean; + readonly line: CallLine; readonly idempotencyKey: string; } export interface CallsBackend { @@ -65,9 +100,15 @@ export interface CallsSnapshot extends ControllerSnapshot { readonly callId?: string; readonly peer?: string; readonly direction?: "incoming" | "outgoing"; + readonly line: CallLine; + readonly capabilities: CallCapabilities; readonly video: boolean; readonly audioMuted: boolean; readonly videoMuted: boolean; + /** Set once media connected; drives the call duration display. */ + readonly connectedAt?: number; + readonly devices: readonly CallDevice[]; + readonly selectedDevices: SelectedCallDevices; readonly endReason?: CallEndReason; readonly error?: { readonly code: string; @@ -78,16 +119,42 @@ export interface CallsSnapshot extends ControllerSnapshot { export interface CallsControllerOptions { readonly createIdempotencyKey?: () => string; readonly now?: () => number; + /** + * Resumption window: how long media may stay disconnected before the call + * is given up (default 15 000 ms), and how long `disconnected` may last + * before an ICE restart is attempted (default 2 000 ms). + */ + readonly resumptionWindowMs?: number; + readonly iceRestartAfterMs?: number; + readonly setTimeout?: typeof globalThis.setTimeout; + readonly clearTimeout?: typeof globalThis.clearTimeout; } +/** HTTP statuses on the offer that mean the call cannot be established at all. */ +const TERMINAL_OFFER_REASONS: Readonly> = { + 410: "pod_lost", + 503: "capacity", +}; + export class CallsController extends ObservableController { readonly #backend: CallsBackend; readonly #mediaFactory: CallMediaFactory; readonly #createKey: () => string; + readonly #now: () => number; + readonly #resumptionWindowMs: number; + readonly #iceRestartAfterMs: number; + readonly #setTimeout: typeof globalThis.setTimeout; + readonly #clearTimeout: typeof globalThis.clearTimeout; #abort = new AbortController(); #unsubscribe: (() => void) | undefined; #media: CallMediaSession | undefined; #operation = 0; + #resumeTimer: ReturnType | undefined; + #giveUpTimer: ReturnType | undefined; + /** The status resumption interrupted, restored when media comes back. */ + #resumedFrom: "connected" | "connecting" | undefined; + /** Per-kind switch counter, so a stale failure cannot undo a newer switch. */ + readonly #deviceSwitches = new Map(); constructor( backend: CallsBackend, @@ -95,13 +162,29 @@ export class CallsController extends ObservableController { options: CallsControllerOptions = {}, ) { super( - { status: "idle", video: false, audioMuted: false, videoMuted: false }, + { + status: "idle", + line: "linkedDevice", + capabilities: capabilitiesFor("linkedDevice"), + video: false, + audioMuted: false, + videoMuted: false, + devices: [], + selectedDevices: {}, + }, options.now, ); this.#backend = backend; this.#mediaFactory = mediaFactory; + this.#now = options.now ?? Date.now; this.#createKey = options.createIdempotencyKey ?? (() => crypto.randomUUID()); + this.#resumptionWindowMs = options.resumptionWindowMs ?? 15_000; + this.#iceRestartAfterMs = options.iceRestartAfterMs ?? 2_000; + this.#setTimeout = + options.setTimeout ?? globalThis.setTimeout.bind(globalThis); + this.#clearTimeout = + options.clearTimeout ?? globalThis.clearTimeout.bind(globalThis); } initialize(): void { @@ -115,28 +198,38 @@ export class CallsController extends ObservableController { async place( to: string, - options: { readonly video?: boolean } = {}, + options: { readonly video?: boolean; readonly line?: CallLine } = {}, ): Promise { const operation = this.#begin(); - const video = options.video ?? false; + const line = options.line ?? "linkedDevice"; + const capabilities = capabilitiesFor(line); + const video = (options.video ?? false) && capabilities.video; + // Only a refused offer carries the pod's terminal hints; a placement that + // fails is the application's own route answering, and its 503 means try + // again, not "the call is over". + let offering = false; try { const { callId } = await this.#backend.place( - { to, video, idempotencyKey: this.#createKey() }, + { to, video, line, idempotencyKey: this.#createKey() }, this.#abort.signal, ); if (operation !== this.#operation) return; this.transition({ + ...this.#deviceFields(), status: "ringing", callId, peer: to, direction: "outgoing", + line, + capabilities, video, audioMuted: false, videoMuted: false, }); + offering = true; await this.#openMedia(callId, video, operation); } catch (cause) { - this.#fail(cause, operation, "place_failed"); + this.#fail(cause, operation, "place_failed", offering); } } @@ -145,14 +238,17 @@ export class CallsController extends ObservableController { if (current.status !== "incoming" || current.callId === undefined) throw new Error("No incoming call is available to answer."); const operation = this.#begin(false); - const video = options.video ?? current.video; + const video = + (options.video ?? current.video) && current.capabilities.video; + let offering = false; try { await this.#backend.answer(current.callId, this.#abort.signal); if (operation !== this.#operation) return; this.transition({ ...callFields(current), status: "accepted", video }); + offering = true; await this.#openMedia(current.callId, video, operation); } catch (cause) { - this.#fail(cause, operation, "answer_failed"); + this.#fail(cause, operation, "answer_failed", offering); } } @@ -183,6 +279,138 @@ export class CallsController extends ObservableController { videoMuted: muted.video ?? current.videoMuted, }); } + /** + * Whether an audio→video upgrade is possible right now: the line carries + * video, the call is not already video, and the media session can + * renegotiate. Surfaces gate the camera-upgrade control on this rather than + * on `capabilities.video` alone, which says nothing about the session. + */ + get canEnableVideo(): boolean { + const current = this.getSnapshot(); + return ( + current.capabilities.video && + !current.video && + this.#media?.enableVideo !== undefined + ); + } + + /** + * Upgrade an audio call to video: acquires the camera, adds the track and + * renegotiates on the same connection. No-op when the line has no video, + * when video is already on, or when the media session cannot renegotiate. + */ + async enableVideo(): Promise { + this.assertActive(); + const current = this.getSnapshot(); + const media = this.#media; + if ( + !current.capabilities.video || + current.video || + media?.enableVideo === undefined + ) + return; + await media.enableVideo( + this.#abort.signal, + this.getSnapshot().selectedDevices, + ); + const after = this.getSnapshot(); + // Disposed while the camera was being acquired: transition would assert + // and reject a direct caller for a call that no longer exists. + if (this.#abort.signal.aborted) return; + // A hang-up or failure while the camera was being acquired keeps the + // callId on the terminal snapshot, so the id alone does not say the call + // is still live — video: true on an ended call would show a camera the + // surface no longer has. + if ( + after.callId !== current.callId || + after.status === "ended" || + after.status === "error" + ) + return; + this.transition({ + ...callFields(after), + status: after.status, + video: true, + videoMuted: false, + }); + } + + /** Choose the devices the next call acquires (and the speaker the UI plays through). */ + setPreferredDevices(devices: SelectedCallDevices): void { + this.assertActive(); + const current = this.getSnapshot(); + this.transition({ + ...callFields(current), + status: current.status, + selectedDevices: { ...current.selectedDevices, ...devices }, + }); + } + + /** + * Switch the live microphone or camera mid-call (the outgoing track is + * replaced in place, mute state carries over) and remember the choice for + * the next call. Without an active media session only the preference is + * stored. + */ + async switchDevice( + kind: "audioInput" | "videoInput", + deviceId: string, + ): Promise { + // Ahead of setPreferredDevices, which asserts the controller is live: the + // UI calls this with `void`, so a selection landing after disposal would + // throw into nothing. + if (this.#abort.signal.aborted) return; + const previous = this.getSnapshot().selectedDevices[kind]; + const generation = (this.#deviceSwitches.get(kind) ?? 0) + 1; + this.#deviceSwitches.set(kind, generation); + this.setPreferredDevices({ [kind]: deviceId }); + const media = this.#media; + if (media?.switchInput === undefined) return; + try { + await media.switchInput( + kind === "audioInput" ? "audio" : "video", + deviceId, + this.#abort.signal, + ); + } catch { + // Device unavailable — the capture kept the old track, so the stored + // preference goes back with it. Left as it was, the device list would + // show a device that is not in use and the next call would open with it. + // + // Unless a newer switch of this kind has since landed: its device is the + // one now live, and restoring this one's would describe the wrong track. + // A disposed controller has no snapshot left to correct, and throwing + // out of here would surface as an unhandled rejection in `void` callers. + if (this.#deviceSwitches.get(kind) !== generation) return; + if (this.#abort.signal.aborted) return; + const current = this.getSnapshot(); + const restored: Record = { + ...current.selectedDevices, + }; + if (previous === undefined) delete restored[kind]; + else restored[kind] = previous; + this.transition({ + ...callFields(current), + status: current.status, + selectedDevices: restored as SelectedCallDevices, + }); + } + } + + /** Re-enumerate capture/playback devices into the snapshot. */ + async refreshDevices(): Promise { + this.assertActive(); + if (this.#mediaFactory.listDevices === undefined) return; + const devices = await this.#mediaFactory.listDevices(); + if (this.#abort.signal.aborted) return; + const current = this.getSnapshot(); + this.transition({ + ...callFields(current), + status: current.status, + devices, + }); + } + get localStream(): MediaStream | undefined { return this.#media?.localStream; } @@ -192,6 +420,7 @@ export class CallsController extends ObservableController { protected override onDispose(): void { this.#operation += 1; + this.#clearResumption(); this.#abort.abort(); this.#unsubscribe?.(); this.#unsubscribe = undefined; @@ -220,6 +449,7 @@ export class CallsController extends ObservableController { video, { onConnectionState: (state) => this.#connection(callId, state), + onIceConnectionState: (state) => this.#ice(callId, state), onRemoteStream: () => { const current = this.getSnapshot(); if (current.callId === callId) @@ -227,25 +457,113 @@ export class CallsController extends ObservableController { }, }, this.#abort.signal, + { devices: this.getSnapshot().selectedDevices }, ); if (operation !== this.#operation) { await media.close(); return; } this.#media = media; + // Mute pressed while the camera and microphone were still being acquired + // only reached the snapshot — there were no tracks to silence yet. Apply + // it now, or the UI would report muted over a live microphone. + const pending = this.getSnapshot(); + if (pending.audioMuted || pending.videoMuted) + media.setMuted({ audio: pending.audioMuted, video: pending.videoMuted }); } #connection(callId: string, state: RTCPeerConnectionState): void { const current = this.getSnapshot(); if (current.callId !== callId) return; - if (state === "connected") - this.transition({ ...callFields(current), status: "connected" }); - else if (state === "failed" || state === "closed") { + // Media teardown is asynchronous, so a queued connection-state callback can + // land after the call ended. A terminal snapshot keeps its callId: a late + // `connected` would revive it, a late `closed` would overwrite a clean + // hangup with `connection_failed`. Same rule #receive and #ice apply. + if (current.status === "ended" || current.status === "error") return; + if (state === "connected") { + this.#clearResumption(); + this.transition({ + ...callFields(current), + status: "connected", + connectedAt: current.connectedAt ?? this.#now(), + }); + } else if (state === "closed") { void this.#closeMedia(); this.transition({ ...callFields(current), status: "ended", endReason: "connection_failed", }); + } else if (state === "failed") { + // One ICE restart within the resumption window before giving up. + this.#beginResumption(callId, 0); + } + } + #ice(callId: string, state: RTCIceConnectionState): void { + const current = this.getSnapshot(); + if (current.callId !== callId) return; + if (state === "disconnected") { + this.#beginResumption(callId, this.#iceRestartAfterMs); + return; + } + if (state !== "connected" && state !== "completed") return; + const resumed = this.#resumedFrom; + this.#clearResumption(); + // A short ICE flap need not move `connectionState`, so recovery cannot + // wait for `#connection` to put the call back: clearing the timers alone + // would strand it in `reconnecting` for the rest of its life. Restore the + // status the flap interrupted — a flap during setup recovers to + // `connecting`, and only a call that was already up gets `connectedAt`. + if (current.status !== "reconnecting") return; + const status = resumed ?? "connected"; + this.transition({ + ...callFields(current), + status, + ...(status === "connected" + ? { connectedAt: current.connectedAt ?? this.#now() } + : {}), + }); + } + /** + * Media dropped on a live call: show `reconnecting`, try an ICE restart + * after `restartAfterMs`, and end the call as `connection_failed` once the + * resumption window closes without media coming back. + */ + #beginResumption(callId: string, restartAfterMs: number): void { + if (this.#giveUpTimer !== undefined) return; + const current = this.getSnapshot(); + if (current.status !== "connected" && current.status !== "connecting") + return; + this.#resumedFrom = current.status; + this.transition({ ...callFields(current), status: "reconnecting" }); + this.#giveUpTimer = this.#setTimeout(() => { + this.#giveUpTimer = undefined; + this.#clearResumption(); + const now = this.getSnapshot(); + if (now.callId !== callId || now.status !== "reconnecting") return; + void this.#closeMedia(); + this.transition({ + ...callFields(now), + status: "ended", + endReason: "connection_failed", + }); + }, this.#resumptionWindowMs); + this.#resumeTimer = this.#setTimeout(() => { + this.#resumeTimer = undefined; + const media = this.#media; + if (media?.restartIce === undefined) return; + if (this.getSnapshot().callId !== callId) return; + void media.restartIce(this.#abort.signal).catch(() => undefined); + }, restartAfterMs); + } + #clearResumption(): void { + this.#resumedFrom = undefined; + if (this.#resumeTimer !== undefined) { + this.#clearTimeout(this.#resumeTimer); + this.#resumeTimer = undefined; + } + if (this.#giveUpTimer !== undefined) { + this.#clearTimeout(this.#giveUpTimer); + this.#giveUpTimer = undefined; } } async #finish( @@ -276,18 +594,37 @@ export class CallsController extends ObservableController { event.callId !== current.callId ) return; + // A terminal snapshot keeps its callId, so the id match above does not + // mean the call is still live. Every branch that moves a call forward is + // therefore refused once it has ended or failed; `incomingCall` starts a + // new call and `ended` is idempotent, so both stay exempt. + if ( + event.type !== "incomingCall" && + event.type !== "ended" && + (current.status === "ended" || current.status === "error") + ) + return; if (event.type === "incomingCall") { if (!["idle", "ready", "ended"].includes(current.status)) return; + const line = event.call.line ?? "linkedDevice"; + const capabilities = capabilitiesFor(line); this.transition({ + ...this.#deviceFields(), status: "incoming", callId: event.call.callId, peer: event.call.from, direction: "incoming", - video: event.call.video, + line, + capabilities, + video: event.call.video && capabilities.video, audioMuted: false, videoMuted: false, }); } else if (event.type === "ended") { + // `ended` is exempt from the terminal guard so a finished call can still + // be closed out, but a second `ended` for a call that already ended + // must not overwrite the reason it ended with. + if (current.status === "ended") return; void this.#closeMedia(); this.transition({ ...callFields(current), @@ -300,10 +637,25 @@ export class CallsController extends ObservableController { status: current.status, video: event.video, }); + } else if (event.type === "connected") { + // The backend can beat the WebRTC callback to this. Without clearing + // the timers here a pending restart would fire an ICE restart on an + // already-connected call, and the give-up timer could still end it. + this.#clearResumption(); + this.transition({ + ...callFields(current), + status: "connected", + connectedAt: current.connectedAt ?? this.#now(), + }); } else { this.transition({ ...callFields(current), status: event.type }); } } + #deviceFields(): Pick { + const { devices, selectedDevices } = this.getSnapshot(); + return { devices, selectedDevices }; + } + #requireIncoming(): CallsSnapshot & { callId: string } { const current = this.getSnapshot(); if (current.status !== "incoming" || current.callId === undefined) @@ -311,13 +663,36 @@ export class CallsController extends ObservableController { return current as CallsSnapshot & { callId: string }; } async #closeMedia(): Promise { + this.#clearResumption(); const media = this.#media; this.#media = undefined; await media?.close(); } - #fail(cause: unknown, operation: number, code: string): void { + #fail( + cause: unknown, + operation: number, + code: string, + fromOffer = false, + ): void { if (operation !== this.#operation || this.#abort.signal.aborted) return; void this.#closeMedia(); + // A pod-lost (410) or capacity/draining (503) answer to the offer is not + // an error to retry: the call is over. Surface it as an ended call. Only + // the offer carries those hints — the same codes from a placement or a + // teardown are ordinary failures and stay recoverable. + const status = fromOffer + ? (cause as { readonly status?: unknown } | null)?.status + : undefined; + const terminal = + typeof status === "number" ? TERMINAL_OFFER_REASONS[status] : undefined; + if (terminal !== undefined) { + this.transition({ + ...callFields(this.getSnapshot()), + status: "ended", + endReason: terminal, + }); + return; + } this.transition({ ...callFields(this.getSnapshot()), status: "error", @@ -340,9 +715,16 @@ function callFields( ...(snapshot.direction === undefined ? {} : { direction: snapshot.direction }), + line: snapshot.line, + capabilities: snapshot.capabilities, video: snapshot.video, audioMuted: snapshot.audioMuted, videoMuted: snapshot.videoMuted, + ...(snapshot.connectedAt === undefined + ? {} + : { connectedAt: snapshot.connectedAt }), + devices: snapshot.devices, + selectedDevices: snapshot.selectedDevices, ...(snapshot.endReason === undefined ? {} : { endReason: snapshot.endReason }), diff --git a/packages/browser/src/calls/index.ts b/packages/browser/src/calls/index.ts index 01ae6b9e..e3500e97 100644 --- a/packages/browser/src/calls/index.ts +++ b/packages/browser/src/calls/index.ts @@ -1,18 +1,42 @@ export { CallsController, + capabilitiesFor, + type CallCapabilities, + type CallDevice, + type CallDeviceKind, type CallEndReason, type CallLifecycleEvent, + type CallLine, type CallStatus, type CallsBackend, type CallsControllerOptions, type CallsSnapshot, type IncomingCall, type PlaceCallInput, + type SelectedCallDevices, } from "./controller.js"; +export { + IncomingCallRelay, + createSignalingCallsBackend, + incomingCallFromWebhook, + type CallReceivedWebhookPayload, + type SignalingCallsBackendOptions, +} from "./backend.js"; +export { + CallsSocket, + lifecycleEventFrom, + parseCallsSocketMessage, + type CallsSocketClientMessage, + type CallsSocketError, + type CallsSocketOptions, + type CallsSocketServerMessage, +} from "./socket.js"; export { WebRtcMediaFactory, type CallMediaCallbacks, + type CandidateTransport, type CallMediaFactory, + type CallMediaPreferences, type CallMediaSession, type WebRtcMediaFactoryOptions, } from "./media.js"; @@ -21,5 +45,6 @@ export { type CallsSignaling, type IceServer, type SdpAnswer, + type SocketTicket, type TrickleCandidate, } from "./signaling.js"; diff --git a/packages/browser/src/calls/media.ts b/packages/browser/src/calls/media.ts index 4f9ad011..1009b884 100644 --- a/packages/browser/src/calls/media.ts +++ b/packages/browser/src/calls/media.ts @@ -3,10 +3,27 @@ import type { SdpAnswer, TrickleCandidate, } from "./signaling.js"; +import type { CallDevice, SelectedCallDevices } from "./controller.js"; export interface CallMediaCallbacks { readonly onConnectionState: (state: RTCPeerConnectionState) => void; readonly onRemoteStream: (stream: MediaStream) => void; + /** ICE connection state (drives the resumption window). Optional. */ + readonly onIceConnectionState?: (state: RTCIceConnectionState) => void; +} + +/** + * A push channel for ICE candidates (the calls WebSocket). When `send` + * returns true the candidate travelled over it and REST is skipped; remote + * candidates arrive through `onCandidate` and REST polling pauses while + * `connected` is true. + */ +export interface CandidateTransport { + readonly connected: boolean; + sendCandidate(callId: string, candidate: TrickleCandidate): boolean; + onCandidate( + listener: (callId: string, candidate: TrickleCandidate) => void, + ): () => void; } export interface CallMediaSession { readonly localStream: MediaStream; @@ -14,18 +31,46 @@ export interface CallMediaSession { setMuted(muted: { readonly audio?: boolean; readonly video?: boolean }): void; audioEnabled(): boolean; videoEnabled(): boolean; + /** + * Add an outgoing camera track to an audio call and renegotiate on the + * same connection. Optional for fakes and for signaling without + * `renegotiate`. + */ + enableVideo?( + signal: AbortSignal, + devices?: SelectedCallDevices, + ): Promise; + /** Re-offer with fresh ICE credentials after a network change. Optional for fakes. */ + restartIce?(signal: AbortSignal): Promise; + /** + * Swap the live capture device: acquires `deviceId` and replaces the + * outgoing track in place (mute state carries over). Optional for fakes. + */ + switchInput?( + kind: "audio" | "video", + deviceId: string, + signal: AbortSignal, + ): Promise; close(): Promise; } +export interface CallMediaPreferences { + readonly devices?: SelectedCallDevices; +} export interface CallMediaFactory { open( callId: string, video: boolean, callbacks: CallMediaCallbacks, signal: AbortSignal, + preferences?: CallMediaPreferences, ): Promise; + /** Enumerate capture/playback devices. Optional for fakes. */ + listDevices?(): Promise; } export interface WebRtcMediaFactoryOptions { readonly signaling: CallsSignaling; + /** Calls WebSocket for ICE; REST stays the fallback while it is down. */ + readonly candidateTransport?: CandidateTransport; readonly mediaDevices?: MediaDevices; readonly createPeerConnection?: ( configuration?: RTCConfiguration, @@ -37,6 +82,7 @@ export interface WebRtcMediaFactoryOptions { export class WebRtcMediaFactory implements CallMediaFactory { readonly #signaling: CallsSignaling; + readonly #candidateTransport: CandidateTransport | undefined; readonly #mediaDevices: MediaDevices; readonly #createPeer: (configuration?: RTCConfiguration) => RTCPeerConnection; readonly #pollIntervalMs: number; @@ -45,13 +91,29 @@ export class WebRtcMediaFactory implements CallMediaFactory { constructor(options: WebRtcMediaFactoryOptions) { this.#signaling = options.signaling; + this.#candidateTransport = options.candidateTransport; this.#mediaDevices = options.mediaDevices ?? navigator.mediaDevices; this.#createPeer = options.createPeerConnection ?? ((configuration) => new RTCPeerConnection(configuration)); this.#pollIntervalMs = options.pollIntervalMs ?? 1_000; - this.#setInterval = options.setInterval ?? globalThis.setInterval; - this.#clearInterval = options.clearInterval ?? globalThis.clearInterval; + this.#setInterval = + options.setInterval ?? globalThis.setInterval.bind(globalThis); + this.#clearInterval = + options.clearInterval ?? globalThis.clearInterval.bind(globalThis); + } + + async listDevices(): Promise { + const devices = await this.#mediaDevices.enumerateDevices(); + return devices + .filter((device) => + ["audioinput", "videoinput", "audiooutput"].includes(device.kind), + ) + .map((device) => ({ + deviceId: device.deviceId, + kind: device.kind as CallDevice["kind"], + label: device.label, + })); } async open( @@ -59,9 +121,12 @@ export class WebRtcMediaFactory implements CallMediaFactory { video: boolean, callbacks: CallMediaCallbacks, signal: AbortSignal, + preferences: CallMediaPreferences = {}, ): Promise { throwIfAborted(signal); - const local = await this.#mediaDevices.getUserMedia({ audio: true, video }); + const local = await this.#mediaDevices.getUserMedia( + constraintsFor(video, preferences.devices), + ); if (signal.aborted) { for (const track of local.getTracks()) track.stop(); throw signal.reason; @@ -69,18 +134,37 @@ export class WebRtcMediaFactory implements CallMediaFactory { const peer = this.#createPeer(); const remote = new MediaStream(); for (const track of local.getTracks()) peer.addTrack(track, local); + const transport = this.#candidateTransport; peer.onicecandidate = (event) => { if (event.candidate === null) return; + const candidate = candidateFrom(event.candidate.toJSON()); + if (transport?.sendCandidate(callId, candidate) === true) return; void this.#signaling - .candidate(callId, candidateFrom(event.candidate.toJSON()), signal) + .candidate(callId, candidate, signal) .catch(() => undefined); }; + // The transport is subscribed before the answer arrives so no pushed + // candidate is missed, but `addIceCandidate` rejects until the remote + // description exists — and REST polling stands down while the socket is + // up, so a rejected candidate would never be retried. Hold them instead. + let remoteDescribed = false; + const pendingCandidates: TrickleCandidate[] = []; + const unsubscribeCandidates = transport?.onCandidate((id, candidate) => { + if (id !== callId) return; + if (!remoteDescribed) { + pendingCandidates.push(candidate); + return; + } + void peer.addIceCandidate(candidate).catch(() => undefined); + }); peer.ontrack = (event) => { remote.addTrack(event.track); callbacks.onRemoteStream(remote); }; peer.onconnectionstatechange = () => callbacks.onConnectionState(peer.connectionState); + peer.oniceconnectionstatechange = () => + callbacks.onIceConnectionState?.(peer.iceConnectionState); try { const offer = await peer.createOffer(); await peer.setLocalDescription(offer); @@ -91,15 +175,154 @@ export class WebRtcMediaFactory implements CallMediaFactory { ); applyIceServers(peer, answer); await peer.setRemoteDescription({ type: "answer", sdp: answer.sdp }); + remoteDescribed = true; + for (const candidate of pendingCandidates.splice(0)) + void peer.addIceCandidate(candidate).catch(() => undefined); } catch (cause) { + unsubscribeCandidates?.(); closePeer(peer, local); throw cause; } let closed = false; - const poll = this.#setInterval( - () => void drainCandidates(this.#signaling, callId, peer, signal), - this.#pollIntervalMs, - ); + let negotiating: Promise = Promise.resolve(); + const renegotiate = ( + options: RTCOfferOptions, + renegotiateSignal: AbortSignal, + ): Promise => { + const run = async () => { + const renegotiateWith = this.#signaling.renegotiate; + if (renegotiateWith === undefined) + throw new Error("Signaling does not support renegotiation."); + throwIfAborted(renegotiateSignal); + const offer = await peer.createOffer(options); + await peer.setLocalDescription(offer); + try { + const answer = await renegotiateWith.call( + this.#signaling, + callId, + peer.localDescription?.sdp ?? offer.sdp ?? "", + renegotiateSignal, + ); + if (closed) return; + await peer.setRemoteDescription({ type: "answer", sdp: answer.sdp }); + } catch (cause) { + // Without this the peer is stranded in `have-local-offer`, and every + // later upgrade or ICE restart fails on the leftover offer rather + // than on its own merits. + if (!closed) + await peer + .setLocalDescription({ type: "rollback" }) + .catch(() => undefined); + throw cause; + } + }; + negotiating = negotiating.catch(() => undefined).then(run); + return negotiating; + }; + const switching = new Map<"audio" | "video", Promise>(); + const swap = async ( + kind: "audio" | "video", + deviceId: string, + switchSignal: AbortSignal, + ): Promise => { + throwIfAborted(switchSignal); + const stream = await this.#mediaDevices.getUserMedia( + kind === "audio" + ? { audio: { deviceId: { ideal: deviceId } } } + : { video: { deviceId: { ideal: deviceId } } }, + ); + // The call may have ended while the device was being acquired. + if (closed || switchSignal.aborted) { + stopTracks(stream); + return; + } + const track = + kind === "audio" + ? stream.getAudioTracks()[0] + : stream.getVideoTracks()[0]; + if (track === undefined) { + stopTracks(stream); + return; + } + const old = local.getTracks().find((t) => t.kind === kind); + const sender = peer.getSenders().find((s) => s.track?.kind === kind); + if (sender === undefined && old === undefined) { + // Nothing of this kind is negotiated — an audio-only call being + // asked to switch camera. Keeping the track would satisfy + // `enableVideo`'s "already have video" guard and permanently block + // the upgrade that would actually negotiate it. + stopTracks(stream); + return; + } + if (old !== undefined) track.enabled = old.enabled; + if (sender !== undefined) { + try { + await sender.replaceTrack(track); + } catch (cause) { + stopTracks(stream); + throw cause; + } + // `close()` can land during the replacement; adopting the track now + // would add it to a stream `closePeer` has already emptied. + if (closed || switchSignal.aborted) { + stopTracks(stream); + return; + } + } + if (old !== undefined) { + old.stop(); + local.removeTrack(old); + } + local.addTrack(track); + }; + + let upgrading: Promise = Promise.resolve(); + const upgrade = async ( + enableSignal: AbortSignal, + devices: SelectedCallDevices | undefined, + ): Promise => { + throwIfAborted(enableSignal); + if (local.getVideoTracks().length > 0) return; + // `preferences` was captured when the call opened. A camera chosen + // since then lives on the controller, so it is passed in here — an + // audio-only call has no video sender for `switchInput` to swap. + const stream = await this.#mediaDevices.getUserMedia( + constraintsFor(true, devices ?? preferences.devices, { + audio: false, + }), + ); + if (closed || enableSignal.aborted) { + stopTracks(stream); + return; + } + const track = stream.getVideoTracks()[0]; + if (track === undefined) { + stopTracks(stream); + return; + } + local.addTrack(track); + const sender = peer.addTrack(track, local); + try { + await renegotiate({}, enableSignal); + } catch (cause) { + // Leaving the track behind would trip the guard above and make the + // upgrade unretryable, so undo it before surfacing the failure. + try { + peer.removeTrack(sender); + } catch { + // The connection may already be closed; the track still goes. + } + local.removeTrack(track); + track.stop(); + throw cause; + } + }; + + const poll = this.#setInterval(() => { + // The socket delivers remote candidates while it is up. + if (transport?.connected === true) return; + void drainCandidates(this.#signaling, callId, peer, signal); + }, this.#pollIntervalMs); return { localStream: local, remoteStream: remote, @@ -111,10 +334,47 @@ export class WebRtcMediaFactory implements CallMediaFactory { }, audioEnabled: () => local.getAudioTracks().some(({ enabled }) => enabled), videoEnabled: () => local.getVideoTracks().some(({ enabled }) => enabled), + // Same-kind switches run one at a time. Concurrently, an older + // acquisition could finish after a newer one and replace the live track + // with the device the user moved away from, leaving the capture behind + // the preference that names it. Serialized, the last request wins. + switchInput: (kind, deviceId, switchSignal) => { + const run = (switching.get(kind) ?? Promise.resolve()).then(() => + swap(kind, deviceId, switchSignal), + ); + switching.set( + kind, + run.catch(() => undefined), + ); + return run; + }, + // Serialized like switchInput: the "already have video" guard sits + // before the getUserMedia await, so two overlapping calls both passed it + // and left the connection with two camera tracks and two senders. Queued, + // the second re-checks the guard after the first has added its track. + // Both need a re-offer, so signaling that cannot renegotiate does not + // get them at all. Present but failing, they left the controller's + // optional-method guards satisfied and the operations rejecting into + // handlers that swallow the reason — a dead camera button. + ...(this.#signaling.renegotiate === undefined + ? {} + : { + enableVideo: ( + enableSignal: AbortSignal, + devices?: SelectedCallDevices, + ) => { + const run = upgrading.then(() => upgrade(enableSignal, devices)); + upgrading = run.catch(() => undefined); + return run; + }, + restartIce: (restartSignal: AbortSignal) => + renegotiate({ iceRestart: true }, restartSignal), + }), close: async () => { if (closed) return; closed = true; this.#clearInterval(poll); + unsubscribeCandidates?.(); closePeer(peer, local); try { await this.#signaling.teardown(callId); @@ -126,6 +386,28 @@ export class WebRtcMediaFactory implements CallMediaFactory { } } +function constraintsFor( + video: boolean, + devices: SelectedCallDevices | undefined, + options: { readonly audio?: boolean } = {}, +): MediaStreamConstraints { + const audioInput = devices?.audioInput; + const videoInput = devices?.videoInput; + return { + audio: + options.audio === false + ? false + : audioInput === undefined + ? true + : { deviceId: { ideal: audioInput } }, + video: video + ? videoInput === undefined + ? true + : { deviceId: { ideal: videoInput } } + : false, + }; +} + function candidateFrom(candidate: RTCIceCandidateInit): TrickleCandidate { return { candidate: candidate.candidate ?? "", @@ -161,6 +443,9 @@ async function drainCandidates( // Polling retries on the next interval. } } +function stopTracks(stream: MediaStream): void { + for (const track of stream.getTracks()) track.stop(); +} function setTracks( tracks: readonly MediaStreamTrack[], enabled: boolean, @@ -171,6 +456,7 @@ function closePeer(peer: RTCPeerConnection, stream: MediaStream): void { peer.onicecandidate = null; peer.ontrack = null; peer.onconnectionstatechange = null; + peer.oniceconnectionstatechange = null; for (const track of stream.getTracks()) track.stop(); peer.close(); } diff --git a/packages/browser/src/calls/signaling.ts b/packages/browser/src/calls/signaling.ts index 34998337..0d6d1443 100644 --- a/packages/browser/src/calls/signaling.ts +++ b/packages/browser/src/calls/signaling.ts @@ -14,8 +14,29 @@ export interface TrickleCandidate { readonly sdpMid?: string; readonly sdpMLineIndex?: number; } +/** A single-use ticket for the calls WebSocket (`POST /api/voip/ws-ticket`). */ +export interface SocketTicket { + readonly ticket: string; + /** Unix epoch milliseconds. */ + readonly expiresAt: number; + /** Root-relative or absolute URL of the socket, ticket included. */ + readonly url: string; +} export interface CallsSignaling { offer(callId: string, sdp: string, signal?: AbortSignal): Promise; + /** + * Re-offer on an established call (audio→video upgrade, ICE restart). The + * pod answers on the same peer connection. Optional for fakes. + */ + renegotiate?( + callId: string, + sdp: string, + signal?: AbortSignal, + ): Promise; + /** Mint a single-use ticket for the calls WebSocket. Optional for fakes. */ + socketTicket?(session?: string, signal?: AbortSignal): Promise; + /** Absolute `ws(s)://` URL for a ticket. Optional for fakes. */ + socketUrl?(ticket: SocketTicket): string; candidate( callId: string, candidate: TrickleCandidate, @@ -51,6 +72,51 @@ export class CallsSignalingClient implements CallsSignaling { }); return response.data.data; } + async renegotiate( + callId: string, + sdp: string, + signal?: AbortSignal, + ): Promise { + const response = await this.#transport.request<{ + readonly data: SdpAnswer; + }>({ + method: "POST", + path: this.#path(callId, "/renegotiate"), + body: { sdp }, + ...(signal === undefined ? {} : { signal }), + }); + return response.data.data; + } + /** + * Mint a socket ticket. The `session` argument is accepted for interface + * compatibility and deliberately not sent: this client authenticates with a + * `pmfa_ct_` token, which the API binds to exactly one session and refuses + * (403) when a request names another. The bound session is used instead. + */ + async socketTicket( + _session?: string, + signal?: AbortSignal, + ): Promise { + const response = await this.#transport.request<{ + readonly data: SocketTicket; + }>({ + method: "POST", + path: `${this.#prefix}/voip/ws-ticket`, + body: {}, + ...(signal === undefined ? {} : { signal }), + }); + return response.data.data; + } + socketUrl(ticket: SocketTicket): string { + const url = new URL(ticket.url, `${this.#transport.baseUrl}/`); + url.protocol = + url.protocol === "https:" + ? "wss:" + : url.protocol === "http:" + ? "ws:" + : url.protocol; + return url.toString(); + } async candidate( callId: string, candidate: TrickleCandidate, diff --git a/packages/browser/src/calls/socket.ts b/packages/browser/src/calls/socket.ts new file mode 100644 index 00000000..35c3142d --- /dev/null +++ b/packages/browser/src/calls/socket.ts @@ -0,0 +1,586 @@ +import type { + CallEndReason, + CallLifecycleEvent, + CallLine, +} from "./controller.js"; +import type { CallsSignaling, TrickleCandidate } from "./signaling.js"; + +/** Server → browser frames on the calls WebSocket. */ +export type CallsSocketServerMessage = + | { readonly type: "ready"; readonly session: string } + | { + readonly type: "event"; + readonly event: string; + readonly callId: string; + readonly payload: unknown; + readonly timestamp: string; + } + | { + readonly type: "candidate"; + readonly callId: string; + readonly candidate: TrickleCandidate; + } + | { readonly type: "error"; readonly code: string; readonly message: string } + | { readonly type: "pong" }; + +/** A server-sent `error` frame, delivered to {@link CallsSocket.onError}. */ +export interface CallsSocketError { + readonly code: string; + readonly message: string; +} + +/** Browser → server frames on the calls WebSocket. */ +export type CallsSocketClientMessage = + | { + readonly type: "candidate"; + readonly callId: string; + readonly candidate: TrickleCandidate; + } + | { readonly type: "teardown"; readonly callId: string } + | { readonly type: "ping" }; + +export interface CallsSocketOptions { + /** Signaling client able to mint socket tickets. */ + readonly signaling: Pick; + /** Session to follow when the credential is a server key. */ + readonly session?: string; + /** + * Calling line the session's inbound calls arrive on. Defaults to + * `linkedDevice`; set `cloudApi` for a Business Calling API session, or the + * controller would offer video controls on an audio-only line. + */ + readonly line?: CallLine; + /** Reconnect backoff bounds in milliseconds. Defaults 1 000 → 30 000. */ + readonly minBackoffMs?: number; + readonly maxBackoffMs?: number; + /** + * Heartbeat period in milliseconds; 0 disables it. Defaults to 15 000. A + * `ping` goes out each period and the socket is dropped when the previous + * one was never answered, so a half-open connection is detected within two + * periods rather than never. + */ + readonly heartbeatMs?: number; + /** + * Handshake deadline in milliseconds; 0 disables it. Defaults to 10 000. + * The WebSocket API has no handshake timeout of its own, so a stalled + * upgrade would otherwise leave `connect()` pending — and, because the + * attempt holds the socket slot, suppress reconnects — for as long as the + * browser cares to wait. The heartbeat cannot see this: it starts on open. + */ + readonly openTimeoutMs?: number; + readonly WebSocket?: typeof globalThis.WebSocket; + readonly setTimeout?: typeof globalThis.setTimeout; + readonly clearTimeout?: typeof globalThis.clearTimeout; + readonly setInterval?: typeof globalThis.setInterval; + readonly clearInterval?: typeof globalThis.clearInterval; + readonly random?: () => number; +} + +/** + * The calls WebSocket: one socket per client, opened with a single-use ticket + * from `POST /api/voip/ws-ticket`. It pushes the session's `call.*` lifecycle + * events (so an incoming call rings without any webhook plumbing), delivers + * the pod's ICE candidates, and carries the browser's candidates and + * teardown. It reconnects with capped exponential backoff — a fresh ticket + * each time — until {@link close}. It is a {@link CallsBackend} lifecycle + * source: pass it as the backend's `incoming`. + */ +export class CallsSocket { + readonly #options: CallsSocketOptions; + readonly #WebSocket: typeof globalThis.WebSocket; + readonly #setTimeout: typeof globalThis.setTimeout; + readonly #clearTimeout: typeof globalThis.clearTimeout; + readonly #setInterval: typeof globalThis.setInterval; + readonly #clearInterval: typeof globalThis.clearInterval; + readonly #random: () => number; + readonly #lifecycle = new Set<(event: CallLifecycleEvent) => void>(); + readonly #candidates = new Set< + (callId: string, candidate: TrickleCandidate) => void + >(); + readonly #state = new Set<(connected: boolean) => void>(); + readonly #errors = new Set<(error: CallsSocketError) => void>(); + #socket: WebSocket | undefined; + #timer: ReturnType | undefined; + #attempt = 0; + #closed = true; + /** Settles the promise of an in-flight `connect()`; cleared once it fires. */ + #settle: (() => void) | undefined; + /** Aborts the in-flight ticket request when `close()` interrupts an attempt. */ + #attemptAbort: AbortController | undefined; + /** Bumped by `close()` so a stale ticket completion never opens a socket. */ + #generation = 0; + #heartbeatTimer: ReturnType | undefined; + /** Handshake deadline for the socket currently opening. */ + #openTimer: ReturnType | undefined; + /** True between sending a `ping` and receiving the matching `pong`. */ + #awaitingPong = false; + + constructor(options: CallsSocketOptions) { + this.#options = options; + this.#WebSocket = options.WebSocket ?? globalThis.WebSocket; + this.#setTimeout = + options.setTimeout ?? globalThis.setTimeout.bind(globalThis); + this.#clearTimeout = + options.clearTimeout ?? globalThis.clearTimeout.bind(globalThis); + this.#setInterval = + options.setInterval ?? globalThis.setInterval.bind(globalThis); + this.#clearInterval = + options.clearInterval ?? globalThis.clearInterval.bind(globalThis); + this.#random = options.random ?? Math.random; + } + + /** True while the socket is open. */ + get connected(): boolean { + return ( + this.#socket !== undefined && + this.#socket.readyState === this.#WebSocket.OPEN + ); + } + + /** + * Open the socket; resolves after the first attempt settles (open, failed, + * or {@link close} called meanwhile). Calling it while a socket exists or + * an attempt is in flight is a no-op. + */ + async connect(): Promise { + this.#closed = false; + if (this.#timer !== undefined) { + // A reconnect is scheduled; run it now instead of waiting. + this.#clearTimeout(this.#timer); + this.#timer = undefined; + } + await this.#open(); + } + + /** Close the socket and stop reconnecting. */ + close(): void { + this.#closed = true; + this.#stopHeartbeat(); + this.#clearOpenTimer(); + if (this.#timer !== undefined) { + this.#clearTimeout(this.#timer); + this.#timer = undefined; + } + const socket = this.#socket; + this.#socket = undefined; + if (socket !== undefined) { + socket.onopen = null; + socket.onmessage = null; + socket.onclose = null; + socket.onerror = null; + try { + socket.close(); + } catch { + // already closed + } + this.#emitState(false); + } + // A connect() awaiting this attempt must not hang on teardown: settle it + // and abort a ticket request still in flight. Any completion that slips + // through belongs to an old generation and is ignored. + this.#generation += 1; + this.#attemptAbort?.abort(); + this.#attemptAbort = undefined; + this.#settle?.(); + } + + /** Lifecycle source for {@link CallsBackend.subscribe}. */ + subscribe(listener: (event: CallLifecycleEvent) => void): () => void { + this.#lifecycle.add(listener); + return () => this.#lifecycle.delete(listener); + } + + /** Remote ICE candidates pushed by the pod. */ + onCandidate( + listener: (callId: string, candidate: TrickleCandidate) => void, + ): () => void { + this.#candidates.add(listener); + return () => this.#candidates.delete(listener); + } + + /** Connection state changes (true = open). */ + onState(listener: (connected: boolean) => void): () => void { + this.#state.add(listener); + return () => this.#state.delete(listener); + } + + /** + * Failures worth surfacing: server-sent `error` frames, a ticket request + * that failed (`ticket_failed`), and a signaling client that cannot mint + * tickets at all (`unsupported`). + * + * Only `unsupported` stops the socket, because retrying it can never + * succeed. The rest keep reconnecting, so a consumer that recognises a + * permanent failure — a revoked credential, a session it may not follow — + * should call {@link close} rather than let the backoff retry it forever. + */ + onError(listener: (error: CallsSocketError) => void): () => void { + this.#errors.add(listener); + return () => this.#errors.delete(listener); + } + + /** Send a local ICE candidate; false when the socket is down (use REST). */ + sendCandidate(callId: string, candidate: TrickleCandidate): boolean { + return this.#send({ type: "candidate", callId, candidate }); + } + + /** Ask the pod to release a call; false when the socket is down. */ + sendTeardown(callId: string): boolean { + return this.#send({ type: "teardown", callId }); + } + + #send(message: CallsSocketClientMessage): boolean { + const socket = this.#socket; + if (socket === undefined || socket.readyState !== this.#WebSocket.OPEN) + return false; + socket.send(JSON.stringify(message)); + return true; + } + + async #open(): Promise { + if ( + this.#closed || + this.#attemptAbort !== undefined || + this.#socket !== undefined + ) + return; + // The whole attempt — ticket request included — is tracked from here so + // close() can settle the caller and abort the request at any point. + const generation = this.#generation; + const abort = new AbortController(); + this.#attemptAbort = abort; + await new Promise((resolve) => { + let settled = false; + const settle = () => { + if (settled) return; + settled = true; + if (this.#settle === settle) this.#settle = undefined; + if (this.#attemptAbort === abort) this.#attemptAbort = undefined; + resolve(); + }; + this.#settle = settle; + void this.#openOnce(generation, abort.signal, settle); + }); + } + + async #openOnce( + generation: number, + signal: AbortSignal, + settle: () => void, + ): Promise { + let url: string; + try { + if ( + this.#options.signaling.socketTicket === undefined || + this.#options.signaling.socketUrl === undefined + ) { + // Not a failure that retrying can fix: this signaling client will + // never mint a ticket. Report it and stop, rather than back off + // against it for the lifetime of the page. + this.#emitError({ + code: "unsupported", + message: "The signaling client cannot mint socket tickets.", + }); + settle(); + return; + } + const ticket = await this.#options.signaling.socketTicket( + this.#options.session, + signal, + ); + url = this.#options.signaling.socketUrl(ticket); + } catch (cause) { + // An ordinary ticket request can fail transiently, so this keeps + // reconnecting — but it no longer does so silently. + this.#emitError({ + code: "ticket_failed", + message: + cause instanceof Error ? cause.message : "Could not mint a ticket.", + }); + settle(); + if (!signal.aborted && !this.#closed) this.#scheduleReconnect(); + return; + } + // Closed (or closed and reopened) while the ticket was in flight: this + // completion is stale and must not create a socket. + if (this.#closed || signal.aborted || generation !== this.#generation) { + settle(); + return; + } + let socket: WebSocket; + try { + socket = new this.#WebSocket(url); + } catch { + // A constructor that throws (a rejected URL, a blocked mixed-content + // upgrade) would otherwise leave `connect()` pending forever, and the + // still-set attempt would make every later `connect()` return it. + settle(); + if (!signal.aborted && !this.#closed && generation === this.#generation) + this.#scheduleReconnect(); + return; + } + this.#socket = socket; + this.#startOpenTimer(socket, settle); + socket.onopen = () => { + this.#clearOpenTimer(); + this.#attempt = 0; + this.#startHeartbeat(socket); + this.#emitState(true); + settle(); + }; + socket.onmessage = (event) => { + const message = parseCallsSocketMessage(event.data); + if (message !== undefined) this.#receive(message); + }; + socket.onerror = () => undefined; + socket.onclose = () => { + this.#clearOpenTimer(); + this.#stopHeartbeat(); + if (this.#socket === socket) this.#socket = undefined; + this.#emitState(false); + settle(); + this.#scheduleReconnect(); + }; + } + + #scheduleReconnect(): void { + if (this.#closed || this.#timer !== undefined) return; + const min = this.#options.minBackoffMs ?? 1_000; + const max = this.#options.maxBackoffMs ?? 30_000; + const delay = + Math.min(max, min * 2 ** this.#attempt) * (0.5 + this.#random() / 2); + this.#attempt = Math.min(this.#attempt + 1, 10); + this.#timer = this.#setTimeout(() => { + this.#timer = undefined; + void this.#open(); + }, delay); + } + + #receive(message: CallsSocketServerMessage): void { + if (message.type === "pong") { + this.#awaitingPong = false; + return; + } + if (message.type === "error") { + this.#emitError({ code: message.code, message: message.message }); + return; + } + if (message.type === "candidate") { + for (const listener of [...this.#candidates]) + listener(message.callId, message.candidate); + return; + } + if (message.type !== "event") return; + const lifecycle = lifecycleEventFrom(message, this.#options.line); + if (lifecycle === undefined) return; + for (const listener of [...this.#lifecycle]) listener(lifecycle); + } + + /** + * A silently dead connection still reports OPEN, so `sendCandidate` keeps + * claiming success and the media factory keeps REST polling disabled: ICE + * stops flowing both ways with nothing to notice it. Pinging turns that into + * an ordinary close, which reconnects. + */ + #startHeartbeat(socket: WebSocket): void { + this.#stopHeartbeat(); + const every = this.#options.heartbeatMs ?? 15_000; + if (every <= 0) return; + this.#heartbeatTimer = this.#setInterval(() => { + if (this.#socket !== socket) return; + if (this.#awaitingPong) { + this.#dropSocket(socket); + return; + } + this.#awaitingPong = this.#send({ type: "ping" }); + }, every); + } + + /** + * Bound the handshake: a socket still not open when the deadline fires is + * dropped like a dead one, which settles the pending `connect()` and + * schedules a reconnect with a fresh ticket. + */ + #startOpenTimer(socket: WebSocket, settle: () => void): void { + this.#clearOpenTimer(); + const after = this.#options.openTimeoutMs ?? 10_000; + if (after <= 0) return; + this.#openTimer = this.#setTimeout(() => { + this.#openTimer = undefined; + if (this.#socket !== socket) return; + settle(); + this.#dropSocket(socket); + }, after); + } + + #clearOpenTimer(): void { + if (this.#openTimer !== undefined) { + this.#clearTimeout(this.#openTimer); + this.#openTimer = undefined; + } + } + + #stopHeartbeat(): void { + if (this.#heartbeatTimer !== undefined) { + this.#clearInterval(this.#heartbeatTimer); + this.#heartbeatTimer = undefined; + } + this.#awaitingPong = false; + } + + /** + * Tear down a socket the heartbeat found dead. `close()` on a half-open + * connection may never fire `onclose`, so the close path runs here directly, + * with the handlers detached so it cannot run twice. + */ + #dropSocket(socket: WebSocket): void { + this.#clearOpenTimer(); + this.#stopHeartbeat(); + if (this.#socket === socket) this.#socket = undefined; + socket.onopen = null; + socket.onmessage = null; + socket.onclose = null; + socket.onerror = null; + try { + socket.close(); + } catch { + // already gone + } + this.#emitState(false); + this.#scheduleReconnect(); + } + + #emitError(error: CallsSocketError): void { + for (const listener of [...this.#errors]) listener(error); + } + + #emitState(connected: boolean): void { + for (const listener of [...this.#state]) listener(connected); + } +} + +/** Parse one server frame; unknown or malformed frames yield `undefined`. */ +export function parseCallsSocketMessage( + data: unknown, +): CallsSocketServerMessage | undefined { + if (typeof data !== "string") return undefined; + let parsed: unknown; + try { + parsed = JSON.parse(data); + } catch { + return undefined; + } + if (parsed === null || typeof parsed !== "object") return undefined; + const frame = parsed as Record; + switch (frame["type"]) { + case "ready": + return isString(frame["session"]) + ? (parsed as CallsSocketServerMessage) + : undefined; + case "event": + return isString(frame["event"]) && + isString(frame["callId"]) && + isString(frame["timestamp"]) && + "payload" in frame + ? (parsed as CallsSocketServerMessage) + : undefined; + case "candidate": + return isString(frame["callId"]) && isCandidate(frame["candidate"]) + ? (parsed as CallsSocketServerMessage) + : undefined; + case "error": + return isString(frame["code"]) && isString(frame["message"]) + ? (parsed as CallsSocketServerMessage) + : undefined; + case "pong": + return parsed as CallsSocketServerMessage; + default: + return undefined; + } +} + +function isString(value: unknown): value is string { + return typeof value === "string"; +} + +function isCandidate(value: unknown): value is TrickleCandidate { + if (value === null || typeof value !== "object") return false; + const candidate = value as Record; + // `null` is what a serialized `RTCIceCandidate` carries for an absent + // `sdpMid` / `sdpMLineIndex`, so it is tolerated rather than treated as a + // malformed frame. + return ( + isString(candidate["candidate"]) && + (candidate["sdpMid"] == null || isString(candidate["sdpMid"])) && + (candidate["sdpMLineIndex"] == null || + typeof candidate["sdpMLineIndex"] === "number") + ); +} + +/** + * Map a pushed `call.*` event onto the controller's lifecycle vocabulary. The + * runner identifies the remote party as a JID reference; the number wins, + * then the LID, then the raw JID. Outgoing offers are the browser's own. + */ +export function lifecycleEventFrom( + message: Extract, + line: CallLine = "linkedDevice", +): CallLifecycleEvent | undefined { + const payload = (message.payload ?? {}) as Record; + switch (message.event) { + case "call.received": { + if (payload.direction === "outgoing") return undefined; + return { + type: "incomingCall", + call: { + callId: message.callId, + from: peerFrom(payload.from), + video: payload.hasVideo === true || payload.has_video === true, + line, + }, + }; + } + case "call.accepted": + return { type: "accepted", callId: message.callId }; + case "call.ended": { + const reason = endReasonFrom(payload.reason); + return reason === undefined + ? { type: "ended", callId: message.callId } + : { type: "ended", callId: message.callId, reason }; + } + case "call.missed": + return { type: "ended", callId: message.callId, reason: "missed" }; + case "call.rejected": + return { type: "ended", callId: message.callId, reason: "rejected" }; + default: + return undefined; + } +} + +function peerFrom(value: unknown): string { + if (typeof value === "string") return value; + if (value === null || typeof value !== "object") return ""; + const ref = value as { + readonly phoneNumber?: unknown; + readonly lid?: unknown; + readonly id?: unknown; + }; + for (const candidate of [ref.phoneNumber, ref.lid, ref.id]) + if (typeof candidate === "string" && candidate.length > 0) return candidate; + return ""; +} + +function endReasonFrom(value: unknown): CallEndReason | undefined { + if (typeof value !== "string" || value.length === 0) return undefined; + switch (value) { + case "user_hangup": + return "remote_hangup"; + case "setup_timeout": + case "media_timeout": + return "ice_timeout"; + case "lost_connection": + return "connection_failed"; + default: + return value; + } +} diff --git a/packages/browser/src/quicklink/controller.ts b/packages/browser/src/quicklink/controller.ts index 0cefd2ad..753bf892 100644 --- a/packages/browser/src/quicklink/controller.ts +++ b/packages/browser/src/quicklink/controller.ts @@ -89,8 +89,10 @@ export class QuickLinkController extends ObservableController super({ status: "idle" }, now); this.#transport = transport; this.#now = now; - this.#setTimeout = options.setTimeout ?? globalThis.setTimeout; - this.#clearTimeout = options.clearTimeout ?? globalThis.clearTimeout; + this.#setTimeout = + options.setTimeout ?? globalThis.setTimeout.bind(globalThis); + this.#clearTimeout = + options.clearTimeout ?? globalThis.clearTimeout.bind(globalThis); } async launch(): Promise { diff --git a/packages/browser/src/transport.ts b/packages/browser/src/transport.ts index c3ac4715..8fe835f5 100644 --- a/packages/browser/src/transport.ts +++ b/packages/browser/src/transport.ts @@ -72,6 +72,11 @@ export class BrowserTransport { readonly #now: () => number; readonly #onDiagnostic: BrowserDiagnosticSink | undefined; + /** The API origin requests are resolved against (no trailing slash). */ + get baseUrl(): string { + return this.#baseUrl; + } + constructor(options: BrowserTransportOptions) { this.#baseUrl = (options.baseUrl ?? "https://api.polymorfa.com").replace( /\/+$/, diff --git a/packages/browser/test/calls-backend.test.ts b/packages/browser/test/calls-backend.test.ts new file mode 100644 index 00000000..ebab1bce --- /dev/null +++ b/packages/browser/test/calls-backend.test.ts @@ -0,0 +1,412 @@ +import { describe, expect, it, vi } from "vitest"; +import { + CallsController, + IncomingCallRelay, + capabilitiesFor, + createSignalingCallsBackend, + incomingCallFromWebhook, + type CallMediaFactory, + type CallMediaSession, + type CallsSignaling, +} from "../src/index.js"; + +function signaling(): CallsSignaling & { teardown: ReturnType } { + return { + offer: vi.fn(async () => ({ sdp: "v=0", iceServers: [] })), + candidate: vi.fn(async () => undefined), + candidates: vi.fn(async () => []), + teardown: vi.fn(async () => undefined), + }; +} + +function media(): { + factory: CallMediaFactory; + session: CallMediaSession & { switchInput: ReturnType }; + listDevices: ReturnType; +} { + const session = { + localStream: {} as MediaStream, + remoteStream: {} as MediaStream, + setMuted: vi.fn(), + audioEnabled: () => true, + videoEnabled: () => true, + switchInput: vi.fn(async () => undefined), + close: vi.fn(async () => undefined), + }; + const listDevices = vi.fn(async () => [ + { deviceId: "mic-2", kind: "audioinput" as const, label: "Desk mic" }, + { deviceId: "cam-1", kind: "videoinput" as const, label: "Webcam" }, + ]); + const factory: CallMediaFactory = { + open: vi.fn(async () => session), + listDevices, + }; + return { factory, session, listDevices }; +} + +describe("incomingCallFromWebhook", () => { + it("maps a call.received payload onto an IncomingCall", () => { + expect( + incomingCallFromWebhook({ + callId: "CALL-1", + from: { + id: "12025550123@s.whatsapp.net", + phoneNumber: "+12025550123", + lid: "5550123@lid", + }, + hasVideo: true, + }), + ).toEqual({ + callId: "CALL-1", + from: "+12025550123", + video: true, + line: "linkedDevice", + }); + expect( + incomingCallFromWebhook({ callId: "c", from: { lid: "5550123@lid" } }) + .from, + ).toBe("5550123@lid"); + // An empty field falls through like an absent one, as it does on the + // socket path — both routes feed the same IncomingCall.from. + expect( + incomingCallFromWebhook({ + callId: "c", + from: { phoneNumber: "", lid: "5550123@lid" }, + }).from, + ).toBe("5550123@lid"); + expect( + incomingCallFromWebhook( + { callId: "c", from: "+12025550123", hasVideo: true }, + { line: "cloudApi" }, + ).line, + ).toBe("cloudApi"); + }); +}); + +describe("createSignalingCallsBackend", () => { + it("relays inbound calls, places through the hook, and tears down on reject/hangup", async () => { + const relay = new IncomingCallRelay(); + const s = signaling(); + const backend = createSignalingCallsBackend({ + signaling: s, + incoming: relay, + place: async () => "call-out-1", + }); + const m = media(); + const controller = new CallsController(backend, m.factory); + controller.initialize(); + + relay.receive({ callId: "CALL-1", from: "+12025550123", video: false }); + expect(controller.getSnapshot()).toMatchObject({ + status: "incoming", + peer: "+12025550123", + capabilities: { video: true, mute: true }, + }); + await controller.reject(); + expect(s.teardown).toHaveBeenCalledWith("CALL-1", expect.any(AbortSignal)); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "rejected", + }); + + await controller.place("+12025550199", { video: true }); + expect(controller.getSnapshot()).toMatchObject({ + status: "connecting", + callId: "call-out-1", + direction: "outgoing", + video: true, + }); + expect(m.factory.open).toHaveBeenCalledWith( + "call-out-1", + true, + expect.any(Object), + expect.any(AbortSignal), + { devices: {} }, + ); + await controller.hangup(); + expect(s.teardown).toHaveBeenLastCalledWith( + "call-out-1", + expect.any(AbortSignal), + ); + + relay.receive({ callId: "CALL-2", from: "+12025550123", video: false }); + relay.ended("CALL-2", "missed"); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "missed", + }); + }); + + it("carries the destination to the place hook and adopts its call id", async () => { + const place = vi.fn(async () => ({ callId: "server-call-9" })); + const controller = new CallsController( + createSignalingCallsBackend({ + signaling: signaling(), + incoming: new IncomingCallRelay(), + place, + }), + media().factory, + ); + controller.initialize(); + + await controller.place("+12025550199", { video: true }); + expect(place).toHaveBeenCalledWith( + { + to: "+12025550199", + video: true, + line: "linkedDevice", + idempotencyKey: expect.any(String), + }, + expect.any(AbortSignal), + ); + expect(controller.getSnapshot()).toMatchObject({ + status: "connecting", + callId: "server-call-9", + peer: "+12025550199", + }); + }); + + it("rejects a placement result that carries no usable call id", async () => { + for (const placed of [ + undefined, + null, + {}, + { callId: 7 }, + { callId: "" }, + "", + ]) { + const s = signaling(); + const controller = new CallsController( + createSignalingCallsBackend({ + signaling: s, + incoming: new IncomingCallRelay(), + place: async () => placed as never, + }), + media().factory, + ); + controller.initialize(); + await controller.place("+12025550199"); + // An undefined id would otherwise reach the offer path as `undefined`. + expect(controller.getSnapshot().status).toBe("error"); + expect(s.offer).not.toHaveBeenCalled(); + controller.dispose(); + } + }); + + it("refuses to place a call when no placement hook is configured", async () => { + // Inventing a call id here would post an offer for a call no pod owns, + // which the platform answers with a permanent not-ready. + const s = signaling(); + const controller = new CallsController( + createSignalingCallsBackend({ + signaling: s, + incoming: new IncomingCallRelay(), + }), + media().factory, + ); + controller.initialize(); + + await controller.place("+12025550199"); + const snapshot = controller.getSnapshot(); + expect(snapshot.status).toBe("error"); + expect(snapshot.error?.code).toBe("place_failed"); + expect(snapshot.error?.message).toMatch(/place/i); + expect(s.offer).not.toHaveBeenCalled(); + }); + + it("puts the device preference back when the switch fails", async () => { + const relay = new IncomingCallRelay(); + const backend = createSignalingCallsBackend({ + signaling: signaling(), + incoming: relay, + place: async () => "call-dev", + }); + const m = media(); + const controller = new CallsController(backend, m.factory); + controller.initialize(); + controller.setPreferredDevices({ audioInput: "mic-1" }); + await controller.place("+12025550123"); + + m.session.switchInput.mockRejectedValueOnce(new Error("device in use")); + await controller.switchDevice("audioInput", "mic-2"); + // The capture kept mic-1, so the stored preference has to say so — or the + // list shows a device that is not in use and the next call opens with it. + expect(controller.getSnapshot().selectedDevices).toEqual({ + audioInput: "mic-1", + }); + + // With nothing chosen before, the key goes away rather than becoming a + // preference for a device that failed. + const fresh = new CallsController( + createSignalingCallsBackend({ + signaling: signaling(), + incoming: new IncomingCallRelay(), + place: async () => "call-dev-2", + }), + m.factory, + ); + fresh.initialize(); + await fresh.place("+12025550123"); + m.session.switchInput.mockRejectedValueOnce(new Error("device in use")); + await fresh.switchDevice("videoInput", "cam-9"); + expect(fresh.getSnapshot().selectedDevices).toEqual({}); + fresh.dispose(); + controller.dispose(); + }); + + it("does not let a stale switch failure undo a newer one", async () => { + const m = media(); + const controller = new CallsController( + createSignalingCallsBackend({ + signaling: signaling(), + incoming: new IncomingCallRelay(), + place: async () => "call-dev", + }), + m.factory, + ); + controller.initialize(); + await controller.place("+12025550123"); + + let failFirst: (reason: Error) => void = () => undefined; + m.session.switchInput + .mockImplementationOnce( + () => + new Promise((_resolve, reject) => { + failFirst = reject; + }), + ) + .mockImplementationOnce(async () => undefined); + + const first = controller.switchDevice("audioInput", "mic-2"); + await controller.switchDevice("audioInput", "mic-3"); + expect(controller.getSnapshot().selectedDevices).toEqual({ + audioInput: "mic-3", + }); + // mic-3 is the live capture now; the older failure must not describe it + // as anything else. + failFirst(new Error("device in use")); + await first; + expect(controller.getSnapshot().selectedDevices).toEqual({ + audioInput: "mic-3", + }); + controller.dispose(); + }); + + it("ignores a device selection made after disposal", async () => { + const m = media(); + const controller = new CallsController( + createSignalingCallsBackend({ + signaling: signaling(), + incoming: new IncomingCallRelay(), + place: async () => "call-dev", + }), + m.factory, + ); + controller.initialize(); + await controller.place("+12025550123"); + controller.dispose(); + + // setPreferredDevices asserts the controller is live, and the device + // panel calls this with `void` — so this used to throw into nothing. + await expect( + controller.switchDevice("audioInput", "mic-2"), + ).resolves.toBeUndefined(); + expect(m.session.switchInput).not.toHaveBeenCalled(); + }); + + it("stays quiet when a device switch fails after disposal", async () => { + const m = media(); + const controller = new CallsController( + createSignalingCallsBackend({ + signaling: signaling(), + incoming: new IncomingCallRelay(), + place: async () => "call-dev", + }), + m.factory, + ); + controller.initialize(); + await controller.place("+12025550123"); + + let fail: (reason: Error) => void = () => undefined; + m.session.switchInput.mockImplementationOnce( + () => + new Promise((_resolve, reject) => { + fail = reject; + }), + ); + const switching = controller.switchDevice("audioInput", "mic-2"); + controller.dispose(); + fail(new Error("device in use")); + // Callers invoke this with `void`, so a throw out of the catch block + // would surface as an unhandled rejection when a pop-out closes. + await expect(switching).resolves.toBeUndefined(); + }); + + it("keeps the Business Calling API line audio-only", async () => { + const relay = new IncomingCallRelay(); + const backend = createSignalingCallsBackend({ + signaling: signaling(), + incoming: relay, + }); + const m = media(); + const controller = new CallsController(backend, m.factory); + controller.initialize(); + expect(capabilitiesFor("cloudApi")).toEqual({ video: false, mute: true }); + + relay.receive({ + callId: "CALL-3", + from: "+12025550123", + video: true, + line: "cloudApi", + }); + expect(controller.getSnapshot()).toMatchObject({ + line: "cloudApi", + video: false, + capabilities: { video: false }, + }); + await controller.answer({ video: true }); + expect(m.factory.open).toHaveBeenCalledWith( + "CALL-3", + false, + expect.any(Object), + expect.any(AbortSignal), + { devices: {} }, + ); + }); + + it("stores device preferences, switches live inputs, and lists devices", async () => { + const relay = new IncomingCallRelay(); + const backend = createSignalingCallsBackend({ + signaling: signaling(), + incoming: relay, + place: async () => "call-dev", + }); + const m = media(); + const controller = new CallsController(backend, m.factory, { + now: () => 1_000, + }); + controller.initialize(); + controller.setPreferredDevices({ audioInput: "mic-1", audioOutput: "spk" }); + await controller.refreshDevices(); + expect(controller.getSnapshot().devices).toHaveLength(2); + + await controller.place("+12025550123"); + expect(m.factory.open).toHaveBeenLastCalledWith( + "call-dev", + false, + expect.any(Object), + expect.any(AbortSignal), + { devices: { audioInput: "mic-1", audioOutput: "spk" } }, + ); + await controller.switchDevice("audioInput", "mic-2"); + expect(m.session.switchInput).toHaveBeenCalledWith( + "audio", + "mic-2", + expect.any(AbortSignal), + ); + expect(controller.getSnapshot().selectedDevices).toEqual({ + audioInput: "mic-2", + audioOutput: "spk", + }); + }); +}); diff --git a/packages/browser/test/calls-media.test.ts b/packages/browser/test/calls-media.test.ts new file mode 100644 index 00000000..bbe6185e --- /dev/null +++ b/packages/browser/test/calls-media.test.ts @@ -0,0 +1,443 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + WebRtcMediaFactory, + type CallMediaCallbacks, + type CallsSignaling, + type CandidateTransport, + type TrickleCandidate, +} from "../src/index.js"; + +// The factory builds its own remote `MediaStream`; everything else is injected. +class FakeStream { + #tracks: FakeTrack[]; + constructor(tracks: FakeTrack[] = []) { + this.#tracks = tracks; + } + getTracks(): FakeTrack[] { + return [...this.#tracks]; + } + getAudioTracks(): FakeTrack[] { + return this.#tracks.filter((t) => t.kind === "audio"); + } + getVideoTracks(): FakeTrack[] { + return this.#tracks.filter((t) => t.kind === "video"); + } + addTrack(track: FakeTrack): void { + if (!this.#tracks.includes(track)) this.#tracks.push(track); + } + removeTrack(track: FakeTrack): void { + this.#tracks = this.#tracks.filter((t) => t !== track); + } +} +class FakeTrack { + enabled = true; + readonly stop = vi.fn(); + constructor(readonly kind: "audio" | "video") {} +} + +beforeEach(() => { + (globalThis as { MediaStream?: unknown }).MediaStream = FakeStream; +}); + +function peerConnection(overrides: Partial> = {}) { + const senders: { + track: FakeTrack | null; + replaceTrack?: (track: FakeTrack) => Promise; + }[] = []; + const peer = { + connectionState: "new", + iceConnectionState: "new", + senders, + addIceCandidate: vi.fn(async () => undefined), + addTrack: vi.fn((track: FakeTrack) => { + const sender = { + track, + replaceTrack: async (next: FakeTrack) => { + sender.track = next; + }, + }; + senders.push(sender); + return sender; + }), + removeTrack: vi.fn((sender: { track: FakeTrack | null }) => { + const index = senders.indexOf(sender); + if (index >= 0) senders.splice(index, 1); + }), + getSenders: () => [...senders], + createOffer: vi.fn(async () => ({ type: "offer", sdp: "v=0" })), + setLocalDescription: vi.fn(async () => undefined), + setRemoteDescription: vi.fn(async () => undefined), + localDescription: { sdp: "v=0" }, + setConfiguration: vi.fn(), + close: vi.fn(), + ...overrides, + }; + return peer as unknown as RTCPeerConnection & typeof peer; +} + +function signaling( + overrides: Partial = {}, +): CallsSignaling & Record> { + return { + offer: vi.fn(async () => ({ sdp: "v=0", iceServers: [] })), + renegotiate: vi.fn(async () => ({ sdp: "v=0", iceServers: [] })), + candidate: vi.fn(async () => undefined), + candidates: vi.fn(async () => []), + teardown: vi.fn(async () => undefined), + ...overrides, + } as never; +} + +function transport(): CandidateTransport & { + deliver: (callId: string, candidate: TrickleCandidate) => void; + listeners: number; +} { + const listeners = new Set< + (callId: string, candidate: TrickleCandidate) => void + >(); + return { + connected: true, + sendCandidate: () => true, + onCandidate: (listener) => { + listeners.add(listener); + return () => listeners.delete(listener); + }, + deliver: (callId, candidate) => { + for (const listener of [...listeners]) listener(callId, candidate); + }, + get listeners() { + return listeners.size; + }, + }; +} + +const callbacks: CallMediaCallbacks = { + onConnectionState: () => undefined, + onRemoteStream: () => undefined, +}; + +const CANDIDATE: TrickleCandidate = { + candidate: "candidate:1 1 udp", + sdpMid: "0", +}; + +function factoryFor(options: { + peer: RTCPeerConnection; + signaling: CallsSignaling; + candidateTransport?: CandidateTransport; + stream?: FakeStream; + getUserMedia?: ReturnType; +}) { + const getUserMedia = + options.getUserMedia ?? + vi.fn( + async () => options.stream ?? new FakeStream([new FakeTrack("audio")]), + ); + return new WebRtcMediaFactory({ + signaling: options.signaling, + ...(options.candidateTransport === undefined + ? {} + : { candidateTransport: options.candidateTransport }), + mediaDevices: { getUserMedia } as unknown as MediaDevices, + createPeerConnection: () => options.peer, + setInterval: (() => 0) as never, + clearInterval: (() => undefined) as never, + }); +} + +describe("WebRtcMediaFactory candidate handling", () => { + it("holds pushed candidates until the answer is applied, then drains them", async () => { + const peer = peerConnection(); + const t = transport(); + // The answer lands only once released, so the candidate necessarily + // arrives while there is still no remote description. + let releaseAnswer: () => void = () => undefined; + const gate = new Promise((resolve) => { + releaseAnswer = resolve; + }); + const s = signaling({ + offer: vi.fn(async () => { + t.deliver("call-1", CANDIDATE); + await gate; + return { sdp: "v=0", iceServers: [] }; + }), + } as never); + const opening = factoryFor({ + peer, + signaling: s, + candidateTransport: t, + }).open("call-1", false, callbacks, new AbortController().signal); + + // Let setup run up to the gated answer, so the candidate has certainly + // been delivered while no remote description exists. + for (let i = 0; i < 20; i += 1) await Promise.resolve(); + expect(s["offer"]).toHaveBeenCalled(); + expect(peer.setRemoteDescription).not.toHaveBeenCalled(); + expect(peer.addIceCandidate).not.toHaveBeenCalled(); + releaseAnswer(); + await opening; + expect(peer.addIceCandidate).toHaveBeenCalledWith(CANDIDATE); + + t.deliver("call-1", CANDIDATE); + expect(peer.addIceCandidate).toHaveBeenCalledTimes(2); + }); + + it("drops the transport subscription when setup fails", async () => { + const t = transport(); + const peer = peerConnection(); + await expect( + factoryFor({ + peer, + signaling: signaling({ + offer: vi.fn(async () => { + throw new Error("offer rejected"); + }), + } as never), + candidateTransport: t, + }).open("call-1", false, callbacks, new AbortController().signal), + ).rejects.toThrow("offer rejected"); + expect(t.listeners).toBe(0); + expect(peer.close).toHaveBeenCalled(); + }); +}); + +describe("WebRtcMediaFactory track negotiation", () => { + it("does not keep a camera track that no sender carries", async () => { + const audio = new FakeTrack("audio"); + const local = new FakeStream([audio]); + const camera = new FakeTrack("video"); + const getUserMedia = vi + .fn() + .mockResolvedValueOnce(local) + .mockResolvedValueOnce(new FakeStream([camera])); + const peer = peerConnection(); + const session = await factoryFor({ + peer, + signaling: signaling(), + getUserMedia, + }).open("call-1", false, callbacks, new AbortController().signal); + + await session.switchInput?.("video", "cam-1", new AbortController().signal); + // Retaining it would satisfy enableVideo's guard and strand the upgrade. + expect(camera.stop).toHaveBeenCalled(); + expect(local.getVideoTracks()).toHaveLength(0); + }); + + it("rolls the local offer back when renegotiation fails", async () => { + const audio = new FakeTrack("audio"); + const local = new FakeStream([audio]); + const getUserMedia = vi.fn().mockResolvedValue(local); + const peer = peerConnection(); + const s = signaling({ + renegotiate: vi.fn(async () => { + throw new Error("re-offer rejected"); + }), + } as never); + const session = await factoryFor({ + peer, + signaling: s, + getUserMedia, + }).open("call-1", false, callbacks, new AbortController().signal); + + await expect( + session.restartIce?.(new AbortController().signal), + ).rejects.toThrow("re-offer rejected"); + // Left in have-local-offer, every later upgrade or restart would fail on + // the stale offer rather than on its own merits. + expect(peer.setLocalDescription).toHaveBeenCalledWith({ + type: "rollback", + }); + }); + + it("serializes same-kind switches so the last request wins", async () => { + const audio = new FakeTrack("audio"); + const local = new FakeStream([audio]); + const second = new FakeTrack("audio"); + const first = new FakeTrack("audio"); + let releaseFirst: (stream: FakeStream) => void = () => undefined; + const getUserMedia = vi + .fn() + .mockResolvedValueOnce(local) + // The older acquisition resolves last, which is the ordering that used + // to leave the older device as the live track. + .mockImplementationOnce( + () => + new Promise((resolve) => { + releaseFirst = resolve as (s: FakeStream) => void; + }), + ) + .mockResolvedValueOnce(new FakeStream([second])); + const peer = peerConnection(); + const session = await factoryFor({ + peer, + signaling: signaling(), + getUserMedia, + }).open("call-1", false, callbacks, new AbortController().signal); + + const a = session.switchInput?.( + "audio", + "mic-2", + new AbortController().signal, + ); + const b = session.switchInput?.( + "audio", + "mic-3", + new AbortController().signal, + ); + // The chain defers each swap by a microtask; let the first one start. + for (let i = 0; i < 5; i += 1) await Promise.resolve(); + // The second switch must not have started while the first is in flight: + // one call for open, one for the first switch, none yet for the second. + expect(getUserMedia).toHaveBeenCalledTimes(2); + releaseFirst(new FakeStream([first])); + await a; + await b; + expect(getUserMedia).toHaveBeenCalledTimes(3); + expect(local.getAudioTracks()).toEqual([second]); + expect(first.stop).toHaveBeenCalled(); + }); + + it("releases the acquired stream when the track swap fails", async () => { + const audio = new FakeTrack("audio"); + const local = new FakeStream([audio]); + const replacement = new FakeTrack("audio"); + const getUserMedia = vi + .fn() + .mockResolvedValueOnce(local) + .mockResolvedValueOnce(new FakeStream([replacement])); + const peer = peerConnection(); + const session = await factoryFor({ + peer, + signaling: signaling(), + getUserMedia, + }).open("call-1", false, callbacks, new AbortController().signal); + const sender = peer.getSenders()[0] as unknown as { + replaceTrack: (track: unknown) => Promise; + }; + sender.replaceTrack = async () => { + throw new Error("swap rejected"); + }; + + await expect( + session.switchInput?.("audio", "mic-2", new AbortController().signal), + ).rejects.toThrow("swap rejected"); + expect(replacement.stop).toHaveBeenCalled(); + // The original capture is untouched, so the call keeps its microphone. + expect(audio.stop).not.toHaveBeenCalled(); + expect(local.getAudioTracks()).toEqual([audio]); + }); + + it("acquires the camera the caller selected, not the one captured at open", async () => { + const audio = new FakeTrack("audio"); + const camera = new FakeTrack("video"); + const getUserMedia = vi + .fn() + .mockResolvedValueOnce(new FakeStream([audio])) + .mockResolvedValueOnce(new FakeStream([camera])); + const peer = peerConnection(); + const session = await factoryFor({ + peer, + signaling: signaling(), + getUserMedia, + }).open("call-1", false, callbacks, new AbortController().signal, { + devices: { videoInput: "cam-at-open" }, + }); + + await session.enableVideo?.(new AbortController().signal, { + videoInput: "cam-chosen-later", + }); + expect(getUserMedia).toHaveBeenLastCalledWith({ + audio: false, + video: { deviceId: { ideal: "cam-chosen-later" } }, + }); + }); + + it("adds one camera track when two upgrades overlap", async () => { + const audio = new FakeTrack("audio"); + const local = new FakeStream([audio]); + const first = new FakeTrack("video"); + const second = new FakeTrack("video"); + let release: (stream: FakeStream) => void = () => undefined; + const getUserMedia = vi + .fn() + .mockResolvedValueOnce(local) + .mockImplementationOnce( + () => + new Promise((resolve) => { + release = resolve as (s: FakeStream) => void; + }), + ) + .mockResolvedValueOnce(new FakeStream([second])); + const peer = peerConnection(); + const session = await factoryFor({ + peer, + signaling: signaling(), + getUserMedia, + }).open("call-1", false, callbacks, new AbortController().signal); + + const a = session.enableVideo?.(new AbortController().signal); + const b = session.enableVideo?.(new AbortController().signal); + // The queue defers each turn by a microtask, so let the first one reach + // its acquisition before releasing it. + for (let i = 0; i < 5; i += 1) await Promise.resolve(); + release(new FakeStream([first])); + await a; + await b; + // Both calls cleared the "already have video" guard before either had + // acquired, which left two tracks and two senders on one connection. + expect(local.getVideoTracks()).toEqual([first]); + expect( + peer.getSenders().filter((s) => s.track?.kind === "video"), + ).toHaveLength(1); + // The queued call re-checks the guard and returns without acquiring, so + // the second camera is never opened at all. + expect(getUserMedia).toHaveBeenCalledTimes(2); + void second; + }); + + it("omits the re-offer operations when signaling cannot renegotiate", async () => { + const audio = new FakeTrack("audio"); + const basic = signaling(); + delete (basic as { renegotiate?: unknown }).renegotiate; + const session = await factoryFor({ + peer: peerConnection(), + signaling: basic, + getUserMedia: vi.fn().mockResolvedValue(new FakeStream([audio])), + }).open("call-1", false, callbacks, new AbortController().signal); + + // Present but rejecting, these satisfied the controller's optional-method + // guards and failed into handlers that swallow the reason. + expect(session.enableVideo).toBeUndefined(); + expect(session.restartIce).toBeUndefined(); + }); + + it("rolls the camera back when the upgrade re-offer fails", async () => { + const audio = new FakeTrack("audio"); + const local = new FakeStream([audio]); + const camera = new FakeTrack("video"); + const getUserMedia = vi + .fn() + .mockResolvedValueOnce(local) + .mockResolvedValueOnce(new FakeStream([camera])); + const peer = peerConnection(); + const s = signaling({ + renegotiate: vi.fn(async () => { + throw new Error("re-offer rejected"); + }), + } as never); + const session = await factoryFor({ + peer, + signaling: s, + getUserMedia, + }).open("call-1", false, callbacks, new AbortController().signal); + + await expect( + session.enableVideo?.(new AbortController().signal), + ).rejects.toThrow("re-offer rejected"); + expect(camera.stop).toHaveBeenCalled(); + expect(local.getVideoTracks()).toHaveLength(0); + expect(peer.removeTrack).toHaveBeenCalled(); + // The audio call survives and the upgrade can be tried again. + expect(local.getAudioTracks()).toHaveLength(1); + }); +}); diff --git a/packages/browser/test/calls-socket.test.ts b/packages/browser/test/calls-socket.test.ts new file mode 100644 index 00000000..817bb6d3 --- /dev/null +++ b/packages/browser/test/calls-socket.test.ts @@ -0,0 +1,585 @@ +// @vitest-environment happy-dom +import { describe, expect, it, vi } from "vitest"; +import { + CallsController, + CallsSocket, + createSignalingCallsBackend, + lifecycleEventFrom, + parseCallsSocketMessage, + type CallMediaFactory, + type CallMediaSession, + type CallsSignaling, + type SocketTicket, +} from "../src/index.js"; + +class FakeWebSocket { + static readonly OPEN = 1; + static readonly CLOSED = 3; + static instances: FakeWebSocket[] = []; + readyState = 0; + sent: string[] = []; + onopen: ((event: unknown) => void) | null = null; + onmessage: ((event: { data: unknown }) => void) | null = null; + onclose: ((event: unknown) => void) | null = null; + onerror: ((event: unknown) => void) | null = null; + constructor(readonly url: string) { + FakeWebSocket.instances.push(this); + } + open(): void { + this.readyState = FakeWebSocket.OPEN; + this.onopen?.({}); + } + receive(message: unknown): void { + this.onmessage?.({ data: JSON.stringify(message) }); + } + drop(): void { + this.readyState = FakeWebSocket.CLOSED; + this.onclose?.({ code: 1006 }); + } + send(data: string): void { + this.sent.push(data); + } + close(): void { + this.readyState = FakeWebSocket.CLOSED; + } +} + +function signaling(): CallsSignaling & { + socketTicket: ReturnType; +} { + return { + offer: vi.fn(async () => ({ sdp: "v=0", iceServers: [] })), + candidate: vi.fn(async () => undefined), + candidates: vi.fn(async () => []), + teardown: vi.fn(async () => undefined), + socketTicket: vi.fn(async () => ({ + ticket: "pmfa_wst_abc", + expiresAt: Date.now() + 60_000, + url: "/voip/ws?ticket=pmfa_wst_abc", + })), + socketUrl: (ticket) => `wss://api.example${ticket.url}`, + }; +} + +function socketWith( + options: Partial[0]> = {}, +) { + FakeWebSocket.instances = []; + const intervals: Array<{ fn: () => void; cleared?: boolean }> = []; + const timers: Array<{ + fn: () => void; + ms: number; + cleared?: boolean; + fired?: boolean; + }> = []; + const socket = new CallsSocket({ + signaling: signaling(), + WebSocket: FakeWebSocket as unknown as typeof globalThis.WebSocket, + setTimeout: ((fn: () => void, ms: number) => { + const entry = { + ms, + fn: () => { + entry.fired = true; + fn(); + }, + } as (typeof timers)[number]; + timers.push(entry); + return timers.length as unknown as ReturnType; + }) as unknown as typeof globalThis.setTimeout, + clearTimeout: ((handle: number) => { + const entry = timers[handle - 1]; + if (entry !== undefined) entry.cleared = true; + }) as unknown as typeof globalThis.clearTimeout, + setInterval: ((fn: () => void) => { + intervals.push({ fn }); + return intervals.length as unknown as ReturnType; + }) as unknown as typeof globalThis.setInterval, + clearInterval: ((handle: number) => { + const entry = intervals[handle - 1]; + if (entry !== undefined) entry.cleared = true; + }) as unknown as typeof globalThis.clearInterval, + random: () => 0.5, + ...options, + }); + return { + socket, + timers, + intervals, + // Fire every live heartbeat tick. + beat: () => { + for (const entry of [...intervals]) + if (entry.cleared !== true) entry.fn(); + }, + ws: () => FakeWebSocket.instances.at(-1)!, + }; +} + +describe("CallsSocket", () => { + it("settles and retries when the WebSocket constructor throws", async () => { + class Throwing { + static readonly OPEN = 1; + constructor() { + throw new Error("insecure url"); + } + } + const { socket, timers } = socketWith({ + WebSocket: Throwing as unknown as typeof globalThis.WebSocket, + }); + // A constructor that throws must not leave connect() pending forever, or + // every later connect() would return that same dead promise. + await socket.connect(); + expect(socket.connected).toBe(false); + expect(timers).toHaveLength(1); + await socket.connect(); + socket.close(); + }); + + it("stops on a signaling client that cannot mint tickets, retries on a failed request", async () => { + const noTickets = socketWith({ + signaling: { socketTicket: undefined, socketUrl: undefined } as never, + }); + const unsupported: { code: string; message: string }[] = []; + noTickets.socket.onError((error) => unsupported.push(error)); + await noTickets.socket.connect(); + // Retrying this can never succeed, so it must report and stop rather than + // back off against it for the life of the page. + expect(unsupported.map((e) => e.code)).toEqual(["unsupported"]); + expect(noTickets.timers).toHaveLength(0); + noTickets.socket.close(); + + const failing = signaling(); + failing.socketTicket = vi.fn(async () => { + throw new Error("ticket route down"); + }); + const transient = socketWith({ signaling: failing }); + const errors: { code: string; message: string }[] = []; + transient.socket.onError((error) => errors.push(error)); + await transient.socket.connect(); + // A ticket route that is merely down is worth retrying — but not silently. + expect(errors).toEqual([ + { code: "ticket_failed", message: "ticket route down" }, + ]); + expect(transient.timers).toHaveLength(1); + transient.socket.close(); + }); + + it("carries the configured line onto socket-delivered incoming calls", async () => { + const { socket, ws } = socketWith({ line: "cloudApi" }); + const backend = createSignalingCallsBackend({ + signaling: signaling(), + incoming: socket, + }); + const session: CallMediaSession = { + localStream: {} as MediaStream, + remoteStream: {} as MediaStream, + setMuted: vi.fn(), + audioEnabled: () => true, + videoEnabled: () => false, + close: vi.fn(async () => undefined), + }; + const controller = new CallsController(backend, { + open: vi.fn(async () => session), + }); + controller.initialize(); + const connecting = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + ws().open(); + await connecting; + + ws().receive({ + type: "event", + event: "call.received", + callId: "CALL-9", + payload: { callId: "CALL-9", from: "+15550100", hasVideo: true }, + timestamp: "", + }); + // Hardcoding linkedDevice here offered video controls on a line that has + // no video at all. + expect(controller.getSnapshot()).toMatchObject({ + status: "incoming", + line: "cloudApi", + video: false, + capabilities: { video: false }, + }); + controller.dispose(); + socket.close(); + }); + + it("pings, and drops a socket that stops answering", async () => { + const h = socketWith({ heartbeatMs: 1_000 }); + const states: boolean[] = []; + h.socket.onState((connected) => states.push(connected)); + const connecting = h.socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + const first = h.ws(); + first.open(); + await connecting; + + h.beat(); + expect(JSON.parse(first.sent.at(-1) ?? "{}")).toEqual({ type: "ping" }); + // Answered every tick: the connection stays up. + first.receive({ type: "pong" }); + h.beat(); + first.receive({ type: "pong" }); + expect(h.socket.connected).toBe(true); + expect(states).toEqual([true]); + + // Unanswered: a half-open socket still reports OPEN and fires no onclose, + // so nothing else would ever notice ICE had stopped flowing. The tick + // after the unanswered ping drops it. + h.beat(); + expect(h.socket.connected).toBe(true); + h.beat(); + expect(states).toEqual([true, false]); + expect(h.socket.connected).toBe(false); + expect(h.timers.length).toBeGreaterThan(0); + expect(h.intervals.every((t) => t.cleared === true)).toBe(true); + h.socket.close(); + }); + + it("drops a socket whose handshake never completes", async () => { + const h = socketWith({ openTimeoutMs: 5_000, minBackoffMs: 100 }); + const states: boolean[] = []; + h.socket.onState((connected) => states.push(connected)); + const connecting = h.socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + const stalled = h.ws(); + // The browser never fires onopen or onclose for a stalled upgrade, so + // only the handshake deadline can move this attempt on. + const open = h.timers.find((t) => t.ms === 5_000); + expect(open).toBeDefined(); + open?.fn(); + await connecting; + expect(h.socket.connected).toBe(false); + expect(stalled.readyState).toBe(FakeWebSocket.CLOSED); + expect(states).toEqual([false]); + // A reconnect is scheduled with backoff, not another bare handshake. + const backoff = h.timers.filter( + (t) => t.cleared !== true && t.fired !== true, + ); + expect(backoff).toHaveLength(1); + // A second connect() is a fresh attempt rather than the dead one. + backoff[0]?.fn(); + await Promise.resolve(); + await Promise.resolve(); + expect(FakeWebSocket.instances).toHaveLength(2); + h.socket.close(); + }); + + it("clears the handshake deadline once the socket opens", async () => { + const h = socketWith({ openTimeoutMs: 5_000 }); + const connecting = h.socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + h.ws().open(); + await connecting; + const open = h.timers.find((t) => t.ms === 5_000); + expect(open?.cleared).toBe(true); + expect(h.socket.connected).toBe(true); + h.socket.close(); + }); + + it("surfaces server error frames to onError", async () => { + const { socket, ws } = socketWith(); + const seen: { code: string; message: string }[] = []; + socket.onError((error) => seen.push(error)); + const connecting = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + ws().open(); + await connecting; + + ws().receive({ type: "error", code: "ticket_expired", message: "Expired" }); + // Dropped silently, the socket would keep reconnecting against a + // permanent failure with nothing for the consumer to act on. + expect(seen).toEqual([{ code: "ticket_expired", message: "Expired" }]); + ws().receive({ type: "pong" }); + ws().receive({ type: "error", code: "x" }); + expect(seen).toHaveLength(1); + socket.close(); + }); + + it("mints a ticket, opens the socket, and feeds lifecycle events to the backend", async () => { + const { socket, ws } = socketWith(); + const connecting = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + expect(ws().url).toBe("wss://api.example/voip/ws?ticket=pmfa_wst_abc"); + ws().open(); + await connecting; + expect(socket.connected).toBe(true); + + const backend = createSignalingCallsBackend({ + signaling: signaling(), + incoming: socket, + }); + const session: CallMediaSession = { + localStream: {} as MediaStream, + remoteStream: {} as MediaStream, + setMuted: vi.fn(), + audioEnabled: () => true, + videoEnabled: () => false, + close: vi.fn(async () => undefined), + }; + const media: CallMediaFactory = { open: vi.fn(async () => session) }; + const controller = new CallsController(backend, media); + controller.initialize(); + + ws().receive({ + type: "event", + event: "call.received", + callId: "CALL-1", + payload: { + callId: "CALL-1", + from: { id: "15550100@s.whatsapp.net", phoneNumber: "+15550100" }, + hasVideo: true, + }, + timestamp: "2026-09-06T12:00:00.000Z", + }); + expect(controller.getSnapshot()).toMatchObject({ + status: "incoming", + callId: "CALL-1", + peer: "+15550100", + video: true, + }); + + ws().receive({ + type: "event", + event: "call.ended", + callId: "CALL-1", + payload: { callId: "CALL-1", reason: "user_hangup" }, + timestamp: "", + }); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "remote_hangup", + }); + controller.dispose(); + }); + + it("carries candidates both ways and reports false while down", async () => { + const { socket, ws } = socketWith(); + const remote: Array<[string, unknown]> = []; + socket.onCandidate((callId, candidate) => remote.push([callId, candidate])); + expect(socket.sendCandidate("CALL-1", { candidate: "c" })).toBe(false); + const connecting = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + ws().open(); + await connecting; + expect( + socket.sendCandidate("CALL-1", { candidate: "candidate:1", sdpMid: "0" }), + ).toBe(true); + expect(JSON.parse(ws().sent[0] ?? "{}")).toEqual({ + type: "candidate", + callId: "CALL-1", + candidate: { candidate: "candidate:1", sdpMid: "0" }, + }); + ws().receive({ + type: "candidate", + callId: "CALL-1", + candidate: { candidate: "candidate:9" }, + }); + expect(remote).toEqual([["CALL-1", { candidate: "candidate:9" }]]); + }); + + it("reconnects with capped backoff and a fresh ticket until closed", async () => { + const { socket, timers, ws } = socketWith({ + minBackoffMs: 100, + maxBackoffMs: 1_000, + }); + const states: boolean[] = []; + socket.onState((connected) => states.push(connected)); + const connecting = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + ws().open(); + await connecting; + ws().drop(); + expect(states).toEqual([true, false]); + // Pending timers only: the open-handshake deadline is cleared on open and + // a fired backoff stays in the list. + const live = () => + timers.filter((t) => t.cleared !== true && t.fired !== true); + expect(live()).toHaveLength(1); + expect(live()[0]?.ms).toBe(75); // 100ms × (0.5 + 0.5/2) + live()[0]?.fn(); + await Promise.resolve(); + await Promise.resolve(); + expect(FakeWebSocket.instances).toHaveLength(2); + ws().drop(); + expect(live()[0]?.ms).toBe(150); // doubled + socket.close(); + live()[0]?.fn(); + await Promise.resolve(); + await Promise.resolve(); + expect(FakeWebSocket.instances).toHaveLength(2); // no reconnect after close + }); +}); + +describe("CallsSocket lifecycle", () => { + it("settles a pending connect() when close() is called mid-attempt", async () => { + const { socket } = socketWith(); + const connecting = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + // The fake socket never opens; teardown must not leave connect() hanging. + socket.close(); + await expect( + Promise.race([ + connecting, + new Promise((_, reject) => + setTimeout(() => reject(new Error("connect() hung")), 200), + ), + ]), + ).resolves.toBeUndefined(); + expect(socket.connected).toBe(false); + }); + + it("settles connect() and aborts the ticket request when closed during acquisition", async () => { + let resolveTicket: ((t: SocketTicket) => void) | undefined; + let ticketSignal: AbortSignal | undefined; + const sig = signaling(); + sig.socketTicket = vi.fn((_session?: string, signal?: AbortSignal) => { + ticketSignal = signal; + return new Promise((resolve) => { + resolveTicket = resolve; + }); + }); + FakeWebSocket.instances = []; + const socket = new CallsSocket({ + signaling: sig, + WebSocket: FakeWebSocket as unknown as typeof globalThis.WebSocket, + }); + const connecting = socket.connect(); + await Promise.resolve(); + socket.close(); + await expect( + Promise.race([ + connecting, + new Promise((_, reject) => + setTimeout(() => reject(new Error("connect() hung")), 200), + ), + ]), + ).resolves.toBeUndefined(); + expect(ticketSignal?.aborted).toBe(true); + // The stale ticket completing later must not open a socket. + resolveTicket?.({ + ticket: "pmfa_wst_late", + expiresAt: Date.now() + 60_000, + url: "/voip/ws?ticket=pmfa_wst_late", + }); + await Promise.resolve(); + await Promise.resolve(); + expect(FakeWebSocket.instances).toHaveLength(0); + // A fresh connect() after close works again. + sig.socketTicket = vi.fn(async () => ({ + ticket: "pmfa_wst_new", + expiresAt: Date.now() + 60_000, + url: "/voip/ws?ticket=pmfa_wst_new", + })); + const again = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + expect(FakeWebSocket.instances).toHaveLength(1); + FakeWebSocket.instances[0]?.open(); + await again; + expect(socket.connected).toBe(true); + }); + + it("ignores a second connect() while a socket exists", async () => { + const { socket, ws } = socketWith(); + const first = socket.connect(); + await Promise.resolve(); + await Promise.resolve(); + ws().open(); + await first; + await socket.connect(); + expect(FakeWebSocket.instances).toHaveLength(1); + expect(socket.connected).toBe(true); + }); +}); + +describe("socket frame mapping", () => { + it("drops unknown frames and maps pod reasons onto the controller vocabulary", () => { + expect(parseCallsSocketMessage("not json")).toBeUndefined(); + expect( + parseCallsSocketMessage(JSON.stringify({ type: "offer" })), + ).toBeUndefined(); + expect(parseCallsSocketMessage(JSON.stringify({ type: "pong" }))).toEqual({ + type: "pong", + }); + // A known `type` is not enough: an incomplete frame would otherwise reach + // the receiver with missing fields. + expect( + parseCallsSocketMessage(JSON.stringify({ type: "event", event: "x" })), + ).toBeUndefined(); + expect( + parseCallsSocketMessage( + JSON.stringify({ type: "candidate", callId: "c" }), + ), + ).toBeUndefined(); + expect( + parseCallsSocketMessage( + JSON.stringify({ + type: "candidate", + callId: "c", + candidate: { sdpMid: "0" }, + }), + ), + ).toBeUndefined(); + expect( + parseCallsSocketMessage(JSON.stringify({ type: "ready" })), + ).toBeUndefined(); + expect( + parseCallsSocketMessage(JSON.stringify({ type: "error", code: "nope" })), + ).toBeUndefined(); + // A serialized RTCIceCandidate carries null for an absent sdpMid. + expect( + parseCallsSocketMessage( + JSON.stringify({ + type: "candidate", + callId: "c", + candidate: { + candidate: "candidate:1", + sdpMid: null, + sdpMLineIndex: null, + }, + }), + ), + ).toMatchObject({ type: "candidate", callId: "c" }); + const ev = (event: string, payload: unknown) => + lifecycleEventFrom({ + type: "event", + event, + callId: "CALL-1", + payload, + timestamp: "", + }); + expect(ev("call.ended", { reason: "media_timeout" })).toEqual({ + type: "ended", + callId: "CALL-1", + reason: "ice_timeout", + }); + expect(ev("call.ended", { reason: "pod_draining" })).toEqual({ + type: "ended", + callId: "CALL-1", + reason: "pod_draining", + }); + expect(ev("call.missed", {})).toEqual({ + type: "ended", + callId: "CALL-1", + reason: "missed", + }); + expect( + ev("call.received", { from: "+15550100", direction: "outgoing" }), + ).toBeUndefined(); + expect(ev("call.received", { from: { lid: "5550100@lid" } })).toMatchObject( + { type: "incomingCall", call: { from: "5550100@lid", video: false } }, + ); + expect(ev("call.telemetry", {})).toBeUndefined(); + }); +}); diff --git a/packages/browser/test/calls.test.ts b/packages/browser/test/calls.test.ts index 9683f7cc..2f224821 100644 --- a/packages/browser/test/calls.test.ts +++ b/packages/browser/test/calls.test.ts @@ -77,7 +77,12 @@ describe("CallsController (voip-v2 contract)", () => { controller.initialize(); await controller.place("+12025550123", { video: true }); expect(f.backend.place).toHaveBeenCalledWith( - { to: "+12025550123", video: true, idempotencyKey: "idem-1" }, + { + to: "+12025550123", + video: true, + line: "linkedDevice", + idempotencyKey: "idem-1", + }, expect.any(AbortSignal), ); controller.setMuted({ audio: true, video: true }); @@ -108,6 +113,39 @@ describe("CallsController (voip-v2 contract)", () => { f.emit({ type: "connected", callId: "old-call" }); expect(controller.getSnapshot().status).toBe("ended"); }); + it("applies a mute pressed while the microphone was still being acquired", async () => { + let release: () => void = () => undefined; + const gate = new Promise((resolve) => { + release = resolve; + }); + const f = fixture(); + (f.media.open as ReturnType).mockImplementation(async () => { + await gate; + return f.session; + }); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + f.emit({ + type: "incomingCall", + call: { callId: "call-1", from: "+12025550123", video: false }, + }); + + const answering = controller.answer(); + await Promise.resolve(); + // Mute now reaches only the snapshot — there are no tracks yet. If it were + // dropped here the dock would report muted over a live microphone. + controller.setMuted({ audio: true }); + expect(f.session.setMuted).not.toHaveBeenCalled(); + release(); + await answering; + + expect(f.session.setMuted).toHaveBeenCalledWith({ + audio: true, + video: false, + }); + expect(controller.getSnapshot().audioMuted).toBe(true); + controller.dispose(); + }); }); describe("CallsSignalingClient", () => { @@ -143,3 +181,416 @@ describe("CallsSignalingClient", () => { ]); }); }); + +describe("CallsSignalingClient socket tickets", () => { + it("never names a session: the client token is already bound to one", async () => { + const fetch = vi.fn( + async () => + new Response( + JSON.stringify({ + data: { ticket: "pmfa_wst_a", expiresAt: 1, url: "/voip/ws?t=a" }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + const client = new CallsSignalingClient( + new BrowserTransport({ + baseUrl: "https://api.example", + getClientToken: async () => "pmfa_ct_x", + fetch: fetch as unknown as typeof globalThis.fetch, + }), + ); + await client.socketTicket("support"); + // The API answers 403 "client token cannot follow another session" when a + // client token names one, so the bound session must be left implicit. + const [, init] = ( + fetch.mock.calls as unknown as [string, RequestInit][] + )[0] ?? ["", {}]; + expect(JSON.parse(String(init.body))).toEqual({}); + }); +}); + +describe("CallsController resumption and terminal offers", () => { + // Fake timers that honour cancellation: a cleared or already-fired entry is + // never run again, so a missing clearTimeout in the controller shows up as a + // failing test rather than passing silently. + function timers() { + const queue: Array<{ fn: () => void; ms: number; cancelled?: boolean }> = + []; + return { + queue, + setTimeout: ((fn: () => void, ms: number) => { + queue.push({ fn, ms }); + return queue.length as unknown as ReturnType; + }) as unknown as typeof globalThis.setTimeout, + clearTimeout: ((handle: number) => { + const entry = queue[handle - 1]; + if (entry !== undefined) entry.cancelled = true; + }) as unknown as typeof globalThis.clearTimeout, + fire: (ms: number) => { + // Re-check cancellation inside the loop: a callback may clear another + // timer queued at the same delay, and the pre-filtered snapshot would + // otherwise still run it. + for (const entry of [...queue]) { + if (entry.ms !== ms || entry.cancelled === true) continue; + entry.cancelled = true; + entry.fn(); + } + }, + pending: () => queue.filter((t) => t.cancelled !== true).length, + }; + } + + it("clears the recovery timers when the backend reports connected first", async () => { + const f = fixture(); + const restartIce = vi.fn(async () => undefined); + Object.assign(f.session, { restartIce }); + let ice: ((state: RTCIceConnectionState) => void) | undefined; + (f.media.open as ReturnType).mockImplementation( + async (_id, _video, callbacks) => { + ice = callbacks.onIceConnectionState; + return f.session; + }, + ); + const t = timers(); + const controller = new CallsController(f.backend, f.media, { + setTimeout: t.setTimeout, + clearTimeout: t.clearTimeout, + now: () => 5_000, + }); + controller.initialize(); + await controller.place("+12025550123"); + ice?.("disconnected"); + expect(controller.getSnapshot().status).toBe("reconnecting"); + + // The pod can beat the WebRTC callback to this. If it only set the status, + // the pending restart would fire an ICE restart on a connected call and + // the give-up timer could still end it. + f.emit({ type: "connected", callId: "call-1" }); + expect(controller.getSnapshot()).toMatchObject({ + status: "connected", + connectedAt: 5_000, + }); + expect(t.pending()).toBe(0); + t.fire(2_000); + t.fire(15_000); + expect(restartIce).not.toHaveBeenCalled(); + expect(controller.getSnapshot().status).toBe("connected"); + controller.dispose(); + }); + + it("keeps a placement failure recoverable when the pod hints are absent", async () => { + const f = fixture(); + // 503 from the application's own placement route means retry; only the + // pod's answer to an offer carries a terminal meaning. + (f.backend.place as ReturnType).mockRejectedValue( + Object.assign(new Error("Service Unavailable"), { status: 503 }), + ); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + expect(controller.getSnapshot()).toMatchObject({ + status: "error", + error: { code: "place_failed", recoverable: true }, + }); + controller.dispose(); + }); + + it("shows reconnecting, restarts ICE, and recovers when media returns", async () => { + const f = fixture(); + const restartIce = vi.fn(async () => undefined); + Object.assign(f.session, { restartIce }); + let ice: ((state: RTCIceConnectionState) => void) | undefined; + (f.media.open as ReturnType).mockImplementation( + async (_id, _video, callbacks) => { + ice = callbacks.onIceConnectionState; + return f.session; + }, + ); + const t = timers(); + const controller = new CallsController(f.backend, f.media, { + setTimeout: t.setTimeout, + clearTimeout: t.clearTimeout, + }); + controller.initialize(); + await controller.place("+12025550123"); + ice?.("connected"); + // The connection state machine, not ICE, marks connected. + expect(controller.getSnapshot().status).toBe("connecting"); + ice?.("disconnected"); + expect(controller.getSnapshot().status).toBe("reconnecting"); + t.fire(2_000); + expect(restartIce).toHaveBeenCalledTimes(1); + ice?.("connected"); + // A flap that never moved `connectionState` still has to end: recovery + // restores the call itself rather than waiting for a state change that + // may never come. This one flapped during setup, so it recovers to + // `connecting` — reporting `connected` here would start the duration + // counter on a call whose media never came up. + expect(controller.getSnapshot().status).toBe("connecting"); + expect(controller.getSnapshot().connectedAt).toBeUndefined(); + // ICE recovery must cancel the give-up timer, or the call would still be + // dropped mid-conversation once the window elapsed. + expect(t.pending()).toBe(0); + t.fire(15_000); + expect(controller.getSnapshot().status).toBe("connecting"); + controller.dispose(); + expect(t.pending()).toBe(0); + }); + + it("recovers an established call back to connected", async () => { + const f = fixture(); + Object.assign(f.session, { restartIce: vi.fn(async () => undefined) }); + let ice: ((state: RTCIceConnectionState) => void) | undefined; + let connection: ((state: RTCPeerConnectionState) => void) | undefined; + (f.media.open as ReturnType).mockImplementation( + async (_id, _video, callbacks) => { + ice = callbacks.onIceConnectionState; + connection = callbacks.onConnectionState; + return f.session; + }, + ); + const t = timers(); + const controller = new CallsController(f.backend, f.media, { + setTimeout: t.setTimeout, + clearTimeout: t.clearTimeout, + now: () => 7_000, + }); + controller.initialize(); + await controller.place("+12025550123"); + connection?.("connected"); + expect(controller.getSnapshot()).toMatchObject({ + status: "connected", + connectedAt: 7_000, + }); + + ice?.("disconnected"); + expect(controller.getSnapshot().status).toBe("reconnecting"); + ice?.("connected"); + // This call was up before the flap, so it goes back to connected and + // keeps the original connectedAt. + expect(controller.getSnapshot()).toMatchObject({ + status: "connected", + connectedAt: 7_000, + }); + controller.dispose(); + expect(t.pending()).toBe(0); + }); + + it("gives the call up after the resumption window", async () => { + const f = fixture(); + let ice: ((state: RTCIceConnectionState) => void) | undefined; + (f.media.open as ReturnType).mockImplementation( + async (_id, _video, callbacks) => { + ice = callbacks.onIceConnectionState; + return f.session; + }, + ); + const t = timers(); + const controller = new CallsController(f.backend, f.media, { + setTimeout: t.setTimeout, + clearTimeout: t.clearTimeout, + resumptionWindowMs: 500, + }); + controller.initialize(); + await controller.place("+12025550123"); + ice?.("disconnected"); + t.fire(500); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "connection_failed", + }); + expect(f.session.close).toHaveBeenCalled(); + }); + + it("ends (not errors) when the offer is answered pod-lost or at capacity", async () => { + for (const [status, reason] of [ + [410, "pod_lost"], + [503, "capacity"], + ] as const) { + const f = fixture(); + (f.media.open as ReturnType).mockRejectedValueOnce( + Object.assign(new Error("gone"), { status }), + ); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: reason, + }); + } + }); + + it("drops a video upgrade that lands after the call ended", async () => { + const f = fixture(); + let release: () => void = () => undefined; + const gate = new Promise((resolve) => { + release = resolve; + }); + Object.assign(f.session, { + enableVideo: vi.fn(async () => { + await gate; + }), + }); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + + const upgrading = controller.enableVideo(); + f.emit({ type: "ended", callId: "call-1", reason: "remote_hangup" }); + expect(controller.getSnapshot().status).toBe("ended"); + release(); + await upgrading; + // The terminal snapshot keeps the callId, so the id alone cannot say the + // call is still live — this would otherwise show a camera on an ended call. + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + video: false, + }); + controller.dispose(); + }); + + it("ignores a backend connected event after the call ended", async () => { + const f = fixture(); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + f.emit({ type: "ended", callId: "call-1", reason: "remote_hangup" }); + expect(controller.getSnapshot().status).toBe("ended"); + + // The ended snapshot keeps the callId, so matching on it alone would + // bring the call back to life. + f.emit({ type: "connected", callId: "call-1" }); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "remote_hangup", + }); + expect(controller.getSnapshot().connectedAt).toBeUndefined(); + controller.dispose(); + }); + + it("ignores every non-terminal lifecycle event once the call has ended", async () => { + for (const event of [ + { type: "connected" as const, callId: "call-1" }, + { type: "accepted" as const, callId: "call-1" }, + { type: "ringing" as const, callId: "call-1" }, + { type: "videostate" as const, callId: "call-1", video: true }, + ]) { + const f = fixture(); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + f.emit({ type: "ended", callId: "call-1", reason: "remote_hangup" }); + + // The ended snapshot keeps its callId, so matching on the id alone let + // any of these put a finished call back into a live status — or turn + // video on after it was over. + f.emit(event); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "remote_hangup", + video: false, + }); + controller.dispose(); + } + }); + + it("keeps the first end reason when a duplicate ended arrives", async () => { + const f = fixture(); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + f.emit({ type: "ended", callId: "call-1", reason: "remote_hangup" }); + // Both the relay and the socket can deliver a late duplicate with its own + // reason; the call ended once, for the first reason. + f.emit({ type: "ended", callId: "call-1", reason: "missed" }); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "remote_hangup", + }); + controller.dispose(); + }); + + it("still accepts a new incoming call after one ended", async () => { + const f = fixture(); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + f.emit({ type: "ended", callId: "call-1", reason: "remote_hangup" }); + // The terminal guard must not wall off the next call. + f.emit({ + type: "incomingCall", + call: { callId: "call-2", from: "+15550100", video: false }, + }); + expect(controller.getSnapshot()).toMatchObject({ + status: "incoming", + callId: "call-2", + }); + controller.dispose(); + }); + + it("ignores connection-state callbacks that land after the call ended", async () => { + const f = fixture(); + const controller = new CallsController(f.backend, f.media, { + now: () => 9_000, + }); + controller.initialize(); + await controller.place("+12025550123"); + await controller.hangup(); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "hangup", + }); + // Media teardown is asynchronous, so these can still fire afterwards. A + // late `connected` would revive the call; a late `closed` would rewrite a + // clean hangup as a connection failure. + f.connect("connected"); + f.connect("closed"); + expect(controller.getSnapshot()).toMatchObject({ + status: "ended", + endReason: "hangup", + }); + expect(controller.getSnapshot().connectedAt).toBeUndefined(); + controller.dispose(); + }); + + it("reports whether a video upgrade is possible", async () => { + const f = fixture(); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + expect(controller.canEnableVideo).toBe(false); + await controller.place("+12025550123"); + // The session cannot renegotiate: capability alone is not enough. + expect(controller.canEnableVideo).toBe(false); + Object.assign(f.session, { enableVideo: vi.fn(async () => undefined) }); + expect(controller.canEnableVideo).toBe(true); + await controller.enableVideo(); + expect(controller.canEnableVideo).toBe(false); // already video + controller.dispose(); + }); + + it("upgrades an audio call to video through the media session", async () => { + const f = fixture(); + const enableVideo = vi.fn(async () => undefined); + Object.assign(f.session, { enableVideo }); + const controller = new CallsController(f.backend, f.media); + controller.initialize(); + await controller.place("+12025550123"); + expect(controller.getSnapshot().video).toBe(false); + // A camera chosen on an audio-only call never reaches a sender, so the + // upgrade has to carry the current selection rather than the one captured + // when the call opened. + controller.setPreferredDevices({ videoInput: "cam-9" }); + await controller.enableVideo(); + expect(enableVideo).toHaveBeenCalledWith(expect.any(AbortSignal), { + videoInput: "cam-9", + }); + expect(controller.getSnapshot()).toMatchObject({ + video: true, + videoMuted: false, + }); + await controller.enableVideo(); // idempotent + expect(enableVideo).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/elements/src/calls.ts b/packages/elements/src/calls.ts index 096046a8..6a6a0573 100644 --- a/packages/elements/src/calls.ts +++ b/packages/elements/src/calls.ts @@ -1,49 +1,146 @@ import type { CallsController, CallsSnapshot } from "@polymorfa/browser"; +import type { Locale } from "@polymorfa/ui"; import { PolymorfaElement, button, element, textElement } from "./base.js"; +/** + * Run a synchronous controller call from a DOM handler. The controller's + * mutators throw once it is disposed, and a disposed controller keeps its last + * snapshot — so this element stays rendered with clickable buttons. The call + * they belonged to is gone; there is nothing to report. + */ +function ignoreDisposed(run: () => void): void { + try { + run(); + } catch { + // Disposed controller — no snapshot left to change. + } +} + +const HEADINGS: Partial< + Record +> = { + incoming: "calls.incoming", + ringing: "calls.ringing", + accepted: "calls.connecting", + connecting: "calls.connecting", + connected: "calls.connected", + reconnecting: "calls.reconnecting", + ended: "calls.ended", + error: "calls.failed", +}; + +const ACTIVE = new Set([ + "ringing", + "accepted", + "connecting", + "connected", + "reconnecting", +]); + +/** + * Portable call surface: incoming card (answer/reject) and the active call + * controls (mute, camera when the line carries video, hang up). The React + * package carries the full official-window layout; this element keeps the + * same controller contract for React-free hosts. + */ export class PolymorfaCallElement extends PolymorfaElement { protected renderContent( snapshot: CallsSnapshot | undefined, + locale: Locale, ): readonly Node[] { + const messages = locale.messages; const panel = element("section", "panel call"); - panel.setAttribute("aria-live", "assertive"); - panel.append(textElement("h2", snapshot?.status ?? "ready", "status")); - if (snapshot?.status === "incoming") + const status = snapshot?.status ?? "ready"; + // idle and ready are absent on purpose: nothing to announce, and this + // region is assertive — a raw status identifier would be read out. + const key = HEADINGS[status]; + const heading = key === undefined ? undefined : messages[key]; + if (heading !== undefined) { + // Assertive on the heading, not the panel: this element re-renders + // wholesale on every snapshot, so a panel-wide live region made a mute + // toggle re-announce the peer number and every control with it. + const status = textElement("h2", heading, "status"); + status.setAttribute("aria-live", "assertive"); + panel.append(status); + } + if (snapshot?.peer !== undefined) + panel.append(textElement("p", snapshot.peer, "peer")); + const controller = this.configuredController(); + if (status === "incoming") panel.append( + // Both reject when a remote hang-up lands between the render and the + // click, because the call is no longer incoming. The snapshot already + // says so, so this only keeps the rejection from going unhandled — + // the same handling the React card uses. button( - "Answer", + messages["calls.answer"], "primary answer", - () => void this.configuredController()?.answer(), + () => void controller?.answer().catch(() => undefined), ), button( - "Reject", + messages["calls.reject"], "reject", - () => void this.configuredController()?.reject(), + () => void controller?.reject().catch(() => undefined), ), ); - if ( - ["connected", "accepted", "connecting", "ringing"].includes( - snapshot?.status ?? "", + if (snapshot !== undefined && ACTIVE.has(status)) { + // Gated like the camera: the element takes any controller, and one that + // reports a line without mute must not be offered the control. + if (snapshot.capabilities.mute) + panel.append( + button( + snapshot.audioMuted + ? messages["calls.unmute"] + : messages["calls.mute"], + "mute", + () => + ignoreDisposed(() => + controller?.setMuted({ audio: !snapshot.audioMuted }), + ), + ), + ); + // On a video call the button mutes the outgoing track; on an audio call + // over a video-capable line it upgrades, as the React dock's does. + // Without the second case an audio call could never reach video here. + // The upgrade needs a media session, which exists only once the call is + // connected — offered earlier the button would do nothing at all. The + // mute branch is unaffected, and `connectedAt` is no use as the test + // because it survives `reconnecting`. + if ( + snapshot.capabilities.video && + (snapshot.video || + (status === "connected" && controller?.canEnableVideo === true)) ) - ) { + panel.append( + snapshot.video + ? button( + snapshot.videoMuted + ? messages["calls.cameraOn"] + : messages["calls.cameraOff"], + "camera", + () => + ignoreDisposed(() => + controller?.setMuted({ video: !snapshot.videoMuted }), + ), + ) + : button(messages["calls.cameraOn"], "camera", () => { + // Rejects on a denied camera or a failed re-offer; the upgrade + // rolls itself back, so the audio call carries on. + void controller?.enableVideo?.().catch(() => undefined); + }), + ); panel.append( - button(snapshot?.audioMuted ? "Unmute" : "Mute", "mute", () => - this.configuredController()?.setMuted({ - audio: !(snapshot?.audioMuted ?? false), - }), - ), button( - "Hang up", + messages["calls.hangup"], "hangup", - () => void this.configuredController()?.hangup(), + () => void controller?.hangup().catch(() => undefined), ), ); } - if (snapshot?.peer !== undefined) - panel.append(textElement("p", snapshot.peer, "peer")); return [panel]; } } + export function defineCallElements( registry: CustomElementRegistry = customElements, ): void { diff --git a/packages/elements/test/elements.test.ts b/packages/elements/test/elements.test.ts index 77d1eb83..d2e1412d 100644 --- a/packages/elements/test/elements.test.ts +++ b/packages/elements/test/elements.test.ts @@ -76,6 +76,215 @@ describe("portable elements", () => { node.remove(); expect(fixture.listeners.size).toBe(0); }); + it("renders the call element's status heading as an assertive region", () => { + const base = { + status: "connected", + revision: 0, + updatedAt: 0, + line: "linkedDevice", + capabilities: { video: true, mute: true }, + video: true, + audioMuted: false, + videoMuted: false, + selectedDevices: {}, + devices: [], + peer: "+12025550123", + }; + const fixture = fixtureController(base); + const node = document.createElement("pmfa-call"); + (node as unknown as { controller: unknown }).controller = + fixture.controller; + document.body.append(node); + const root = node.shadowRoot; + try { + // The element re-renders wholesale, so an assertive panel re-announced + // the peer number and every control each time a mute label changed. + expect(root?.querySelector("section[aria-live]")).toBeNull(); + expect( + root?.querySelector('h2[part="status"]')?.getAttribute("aria-live"), + ).toBe("assertive"); + expect(root?.querySelector('h2[part="status"]')?.textContent).toBe( + "Connected", + ); + + // idle and ready have nothing to announce and no message to announce it + // with; a raw status identifier must not reach the DOM. + fixture.update({ ...base, status: "ready", revision: 1 }); + expect(root?.querySelector('h2[part="status"]')).toBeNull(); + } finally { + node.remove(); + } + }); + + it("offers the camera controls only where they can act", () => { + const base = { + status: "connected", + revision: 0, + updatedAt: 0, + line: "linkedDevice", + capabilities: { video: true, mute: true }, + video: true, + audioMuted: false, + videoMuted: false, + selectedDevices: {}, + devices: [], + peer: "+12025550123", + }; + const fixture = fixtureController(base); + const enableVideo = vi.fn(async () => undefined); + const setMuted = vi.fn(); + const ctl = fixture.controller as unknown as { + enableVideo: unknown; + setMuted: unknown; + canEnableVideo: boolean; + }; + Object.assign(ctl, { enableVideo, setMuted, canEnableVideo: true }); + const node = document.createElement("pmfa-call"); + (node as unknown as { controller: unknown }).controller = + fixture.controller; + document.body.append(node); + const root = node.shadowRoot; + try { + // The upgrade needs a media session, so it is not offered before the + // call connects — the button would have done nothing there. + fixture.update({ + ...base, + revision: 1, + video: false, + status: "connecting", + }); + expect(root?.querySelector('[part="camera"]')).toBeNull(); + + // Nor when the session cannot renegotiate: the element takes any + // controller, and one that reports no upgrade must not show the button. + ctl.canEnableVideo = false; + fixture.update({ ...base, revision: 2, video: false }); + expect(root?.querySelector('[part="camera"]')).toBeNull(); + + // An audio call on a video-capable line offers the upgrade, matching + // the React dock; without it the element could never reach video. + ctl.canEnableVideo = true; + fixture.update({ ...base, revision: 3, video: false }); + const camera = root?.querySelector( + '[part="camera"]', + ) as HTMLButtonElement; + expect(camera.textContent).toBe("Turn camera on"); + camera.click(); + expect(enableVideo).toHaveBeenCalledTimes(1); + expect(setMuted).not.toHaveBeenCalled(); + + // On a video call the same button mutes the outgoing track, throughout. + fixture.update({ ...base, revision: 4, status: "connecting" }); + expect(root?.querySelector('[part="camera"]')).not.toBeNull(); + fixture.update({ ...base, revision: 5 }); + (root?.querySelector('[part="camera"]') as HTMLButtonElement).click(); + expect(setMuted).toHaveBeenCalledWith({ video: true }); + expect(enableVideo).toHaveBeenCalledTimes(1); + + // A line without mute must not be offered the control. + fixture.update({ ...base, revision: 6 }); + expect(root?.querySelector('[part="mute"]')).not.toBeNull(); + fixture.update({ + ...base, + revision: 7, + capabilities: { video: true, mute: false }, + }); + expect(root?.querySelector('[part="mute"]')).toBeNull(); + expect(root?.querySelector('[part="hangup"]')).not.toBeNull(); + } finally { + node.remove(); + } + }); + it("absorbs answer and reject rejections on the call element", async () => { + const rejections: unknown[] = []; + // Node's hook, not the DOM event: happy-dom does not dispatch + // `unhandledrejection`, so listening for that would assert nothing. + const onRejection = (reason: unknown) => rejections.push(reason); + process.on("unhandledRejection", onRejection); + const node = document.createElement("pmfa-call"); + // Detached even when an assertion fails: left attached it would go on + // swallowing Node's reporting and could hide a later test's failure. + try { + const fixture = fixtureController({ + status: "incoming", + revision: 0, + updatedAt: 0, + line: "linkedDevice", + capabilities: { video: true, mute: true }, + video: false, + audioMuted: false, + videoMuted: false, + selectedDevices: {}, + devices: [], + peer: "+12025550123", + }); + // A remote hang-up between render and click leaves both rejecting. + Object.assign(fixture.controller, { + answer: async () => { + throw new Error("No incoming call is available to answer."); + }, + reject: async () => { + throw new Error("No incoming call is available to reject."); + }, + }); + (node as unknown as { controller: unknown }).controller = + fixture.controller; + document.body.append(node); + + ( + node.shadowRoot?.querySelector('[part~="answer"]') as HTMLButtonElement + ).click(); + ( + node.shadowRoot?.querySelector('[part="reject"]') as HTMLButtonElement + ).click(); + // Node reports these after the microtask queue drains, so wait a macrotask. + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(rejections).toEqual([]); + } finally { + node.remove(); + process.off("unhandledRejection", onRejection); + } + }); + + it("absorbs mute clicks on the call element after disposal", () => { + const base = { + status: "connected", + revision: 0, + updatedAt: 0, + line: "linkedDevice", + capabilities: { video: true, mute: true }, + video: true, + audioMuted: false, + videoMuted: false, + selectedDevices: {}, + devices: [], + peer: "+12025550123", + }; + const fixture = fixtureController(base); + // A disposed controller throws synchronously from setMuted while the + // element stays rendered on its last snapshot. + Object.assign(fixture.controller, { + setMuted: () => { + throw new Error("CallsController is disposed."); + }, + }); + const node = document.createElement("pmfa-call"); + (node as unknown as { controller: unknown }).controller = + fixture.controller; + document.body.append(node); + try { + for (const part of ["mute", "camera"]) { + const button = node.shadowRoot?.querySelector( + `[part="${part}"]`, + ) as HTMLButtonElement | null; + expect(button).not.toBeNull(); + expect(() => button?.click()).not.toThrow(); + } + } finally { + node.remove(); + } + }); + it("restores drawer focus and emits a composed close event", () => { const opener = document.createElement("button"); document.body.append(opener); diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md index 53cd682a..28f46255 100644 --- a/packages/nextjs/README.md +++ b/packages/nextjs/README.md @@ -32,6 +32,19 @@ export const POST = createClientTokenRoute({ }); ``` +For browser calls, this same application route can mint through +`MessagingClient.voip` instead, which returns the same token shape. Only the +upstream platform call changes (the server SDK posts `/api/voip/token`); the +path the browser posts to is still your own route, which +`createClientTokenProvider` defaults to `/api/polymorfa/token`: + +```ts +mint: createMessagingClientTokenMint({ + clientTokens: { mint: (input, options) => messaging.voip.token(input, options) }, + resolve: async (subject) => ({ session: "support", ephemeralId: subject.userId }), +}), +``` + `createTemplateBuilderRoute` pairs the browser template transport with `MessagingClient.templates`. The application authorizes every request and resolves both project scope and the Cloud API submission session on the server. diff --git a/packages/react/README.md b/packages/react/README.md index 2fb1ce42..e46c1fb4 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -21,3 +21,80 @@ carousel card bodies, and variable examples. It keeps “Save draft” and “Su to Meta” as separate actions and renders structured previews. Use `renderPreview` to replace the preview region without replacing controller state. + +## Calls + +`CallSurface` is the whole call experience: the incoming card while a call +rings, the stage and control dock during the call, and a ↗ button that pops +the call into its own resizable window. `IncomingCallCard`, `CallStage`, +`CallControls`, and `DialPad` compose individually. All of them render from a +`CallsController` and follow the provider's appearance and locale. + +```tsx +import { + BrowserTransport, + CallsController, + CallsSignalingClient, + CallsSocket, + WebRtcMediaFactory, + createClientTokenProvider, + createSignalingCallsBackend, +} from "@polymorfa/browser"; +import { CallSurface, DialPad, PolymorfaProvider } from "@polymorfa/react"; + +const transport = new BrowserTransport({ + getClientToken: createClientTokenProvider(), // POST /api/polymorfa/token → pmfa_ct_… +}); +const signaling = new CallsSignalingClient(transport); +// The calls WebSocket: incoming calls ring without webhook plumbing, remote +// hang-ups land immediately, ICE trickles over the socket (REST is the +// fallback while it reconnects). +const socket = new CallsSocket({ signaling }); +const calls = new CallsController( + createSignalingCallsBackend({ + signaling, + incoming: socket, + // Outbound calls start on the server: the client token cannot dial a + // destination. Omit this hook if you only answer inbound calls (and drop + // `DialPad`, which places them). + place: async ({ to, video, line, idempotencyKey }, signal) => { + const response = await fetch("/api/calls/place", { + method: "POST", + headers: { + "content-type": "application/json", + // One key per placement: forward it to the server SDK call so a + // retried request cannot start a second call. + "idempotency-key": idempotencyKey, + }, + body: JSON.stringify({ to, video, line }), + signal, + }); + if (!response.ok) throw new Error("The call could not be placed."); + return ((await response.json()) as { callId: string }).callId; + }, + }), + new WebRtcMediaFactory({ signaling, candidateTransport: socket }), +); +calls.initialize(); +void socket.connect(); + + + + +; +``` + +The incoming card mirrors the official desktop call window: name, "WhatsApp +audio/video call" subtitle, avatar or a mirrored self-preview with camera and +mute toggles and a ⋯ device menu, then Reject and Answer. Answering a video +offer with the camera off still acquires video muted, so the in-call camera +toggle can enable it. The dock carries split pills — `[camera|⌄]` and +`[mic|⌄]` whose dropdowns pick devices live — and a red hang-up pill. Video is +per direction; on an audio call the camera button upgrades to video +(`controller.enableVideo()`, a re-offer on the same connection) and +`disableVideo` hides it. A dropped connection shows "Reconnecting…" while the +controller restarts ICE, and gives up as `connection_failed` after the +resumption window. Avatars come from +`resolveAvatar` (a URL or a promise of one; `BrowserMessagingClient.contacts +.picture` works when the token carries `read_contact`); without one a stable +hash of the caller id picks one of eight palette tones. diff --git a/packages/react/src/calls.tsx b/packages/react/src/calls.tsx new file mode 100644 index 00000000..d80a1cf2 --- /dev/null +++ b/packages/react/src/calls.tsx @@ -0,0 +1,1584 @@ +// Polymorfa Calls UI — React components over `CallsController`. +// +// Shaped after the official WhatsApp desktop call windows in a monochrome +// Material-3 voice: black identity, tonal fills, hairline outlines, 8px +// radius; the verbs carry the only color (success green answers, danger red +// declines/hangs up). Cards follow the appearance theme; the call stage — and +// the incoming card once it carries a camera preview — is always dark, like an +// OS call screen. Widgets are fluid and never resize when a panel opens; +// device panels overlay them. Every affordance gates on the snapshot's +// `capabilities`, never on the line name. + +import { + capabilitiesFor, + type CallDevice, + type CallLine, + type CallsController, + type CallsSnapshot, +} from "@polymorfa/browser"; +import { appearanceToCssVariables, type Locale } from "@polymorfa/ui"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type CSSProperties, + type ReactNode, + type SVGProps, +} from "react"; +import { createPortal } from "react-dom"; +import { usePolymorfa } from "./context.js"; +import { useController, useResolvedController } from "./hooks.js"; + +type ControllerProps = { + readonly controller?: CallsController; + readonly createController?: () => CallsController; +}; + +/** + * Run a synchronous controller call from a DOM handler. + * + * The controller's mutators assert it is live and throw once it is disposed, + * and a disposed controller keeps its last snapshot — so a surface stays + * mounted and its buttons stay clickable. React does not catch throws from + * event handlers, so without this the error reaches window.onerror. There is + * nothing to report: the call these controls belonged to is gone. + */ +function ignoreDisposed(run: () => void): void { + try { + run(); + } catch { + // Disposed controller — no snapshot left to change. + } +} + +const ACTIVE_STATUSES = new Set([ + "ringing", + "accepted", + "connecting", + "connected", + "reconnecting", +]); + +// ── Icons (Feather, MIT — inline so the package ships zero assets) ──── + +type IconProps = SVGProps; + +function Icon({ children, ...props }: IconProps) { + return ( + + ); +} + +const PhoneIcon = (p: IconProps) => ( + + + +); + +/** The end-call handset: the phone glyph rotated to point down. */ +const HangupIcon = ({ style, ...p }: IconProps) => ( + +); + +const MicIcon = (p: IconProps) => ( + + + + + + +); + +const MicOffIcon = (p: IconProps) => ( + + + + + + + +); + +const VideoIcon = (p: IconProps) => ( + + + + +); + +const VideoOffIcon = (p: IconProps) => ( + + + + +); + +const EraseIcon = (p: IconProps) => ( + + + + + +); + +const UserIcon = (p: IconProps) => ( + + + + +); + +const ChevronDownIcon = (p: IconProps) => ( + + + +); + +const MoreIcon = (p: IconProps) => ( + + + + + +); + +const PopoutIcon = (p: IconProps) => ( + + + + + +); + +// ── Root / theming ──────────────────────────────────────────────────── + +const STYLE_TAG_ID = "pmfa-calls-styles"; + +/** + * Inject the calls stylesheet once per document. SSR-safe (no-op without a + * `document`) and idempotent. + */ +export function injectCallsStyles( + target: Document | undefined = globalThis.document, +): void { + if (target === undefined) return; + if (target.getElementById(STYLE_TAG_ID)) return; + const tag = target.createElement("style"); + tag.id = STYLE_TAG_ID; + tag.textContent = CALLS_STYLES; + target.head.appendChild(tag); +} + +function useCallsRoot(): { + readonly className: string; + readonly style: CSSProperties; + readonly dir: "ltr" | "rtl"; + readonly locale: Locale; +} { + const configuration = usePolymorfa(); + useEffect(() => injectCallsStyles(), []); + const call = configuration.appearance.elements["call"]; + const theme = configuration.appearance.theme; + const className = [ + "pmfa-calls", + theme === "dark" + ? "pmfa-calls-dark" + : theme === "system" + ? "pmfa-calls-auto" + : "", + call?.className ?? "", + ] + .filter(Boolean) + .join(" "); + const style = useMemo( + () => + ({ + ...appearanceToCssVariables(configuration.appearance), + ...call?.styles, + }) as CSSProperties, + [configuration.appearance, call], + ); + return { + className, + style, + dir: configuration.locale.direction, + locale: configuration.locale, + }; +} + +function t( + locale: Locale, + key: keyof Locale["messages"], + values: Record = {}, +): string { + return locale.messages[key].replace( + /\{(\w+)\}/g, + (_, name: string) => values[name] ?? "", + ); +} + +// ── Building blocks ─────────────────────────────────────────────────── + +function initialsOf(name: string | undefined): string | null { + if (name === undefined) return null; + const words = name.trim().split(/\s+/).filter(Boolean); + const first = words[0]; + if (first === undefined || /^[+\d(]/.test(first)) return null; + const last = words.length > 1 ? words[words.length - 1] : undefined; + const initials = `${first[0] ?? ""}${last?.[0] ?? ""}`.toUpperCase(); + return initials.length > 0 ? initials : null; +} + +/** + * Deterministic per-caller avatar color: a stable hash of the peer id picks + * one of 8 palette tones, so the same caller always renders the same color. + * A resolved profile picture wins. + */ +function avatarColorClass(peer: string | undefined): string { + if (peer === undefined) return ""; + let h = 5381; + for (let i = 0; i < peer.length; i += 1) + h = ((h * 33) ^ peer.charCodeAt(i)) >>> 0; + return ` pmfa-calls-avatar-c${(h % 8) + 1}`; +} + +function CallAvatar({ + displayName, + avatarUrl, + peer, +}: { + readonly displayName: string | undefined; + readonly avatarUrl: string | undefined; + readonly peer: string | undefined; +}) { + const initials = initialsOf(displayName); + return ( +
+
+ {avatarUrl !== undefined ? ( + + ) : initials !== null ? ( + initials + ) : ( + + )} +
+
+ ); +} + +/** Resolve a peer's picture: a URL, nothing, or a promise of either. */ +export type AvatarResolver = ( + peer: string, +) => string | undefined | Promise; + +function usePeerAvatar( + peer: string | undefined, + resolveAvatar: AvatarResolver | undefined, + explicit?: string, +): string | undefined { + const [resolved, setResolved] = useState>( + {}, + ); + useEffect(() => { + if (peer === undefined || resolveAvatar === undefined || peer in resolved) + return; + let cancelled = false; + void Promise.resolve() + .then(() => resolveAvatar(peer)) + .catch(() => undefined) + .then((url) => { + if (!cancelled) + setResolved((previous) => ({ ...previous, [peer]: url })); + }); + return () => { + cancelled = true; + }; + }, [peer, resolveAvatar, resolved]); + return explicit ?? (peer === undefined ? undefined : resolved[peer]); +} + +/** + * A muted, mirrored self-view for the pre-answer preview on video offers. + * Null while the camera warms up or when access was denied — callers fall + * back to the dark block. + */ +function useSelfPreview( + enabled: boolean, + videoInput: string | undefined, +): MediaStream | null { + const [stream, setStream] = useState(null); + useEffect(() => { + const devices = globalThis.navigator?.mediaDevices; + if ( + !enabled || + devices === undefined || + typeof devices.getUserMedia !== "function" + ) + return; + let cancelled = false; + let acquired: MediaStream | null = null; + void devices + .getUserMedia({ + video: + videoInput === undefined ? true : { deviceId: { ideal: videoInput } }, + audio: false, + }) + .then((s) => { + acquired = s; + if (cancelled) for (const track of s.getTracks()) track.stop(); + else setStream(s); + }) + .catch(() => undefined); + return () => { + cancelled = true; + if (acquired !== null) + for (const track of acquired.getTracks()) track.stop(); + setStream(null); + }; + }, [enabled, videoInput]); + return stream; +} + +function attach( + element: HTMLMediaElement | null, + stream: MediaStream | undefined | null, +): void { + if (element !== null && stream != null && element.srcObject !== stream) + element.srcObject = stream; +} + +// ── Device panels ───────────────────────────────────────────────────── + +const canSetSink = + typeof HTMLMediaElement !== "undefined" && + "setSinkId" in HTMLMediaElement.prototype; + +type DeviceFieldKind = "audioInput" | "videoInput" | "audioOutput"; + +const DEVICE_KIND: Record = { + audioInput: "audioinput", + videoInput: "videoinput", + audioOutput: "audiooutput", +}; + +const DEVICE_LABEL: Record< + DeviceFieldKind, + "calls.microphone" | "calls.camera" | "calls.speaker" +> = { + audioInput: "calls.microphone", + videoInput: "calls.camera", + audioOutput: "calls.speaker", +}; + +/** + * One-panel-at-a-time open state plus the label unlock: device labels are + * empty until the page holds a media permission, so opening a panel takes + * (and immediately releases) a throwaway audio capture so real names show. + */ +function useDevicePanels(controller: CallsController, snapshot: CallsSnapshot) { + const [open, setOpen] = useState(null); + const toggle = (panel: string) => { + const next = open === panel ? null : panel; + setOpen(next); + if (next === null) return; + void (async () => { + const devices = globalThis.navigator?.mediaDevices; + if ( + snapshot.devices.every((d) => d.label.length === 0) && + devices !== undefined && + typeof devices.getUserMedia === "function" + ) { + try { + const s = await devices.getUserMedia({ audio: true }); + for (const track of s.getTracks()) track.stop(); + } catch { + // Denied — enumerate anyway; generic labels render. + } + } + await controller.refreshDevices().catch(() => undefined); + })(); + }; + return { open, toggle }; +} + +/** + * Device selects, scoped per panel: the mic dropdown carries microphone + + * speaker, the camera dropdown carries the camera only (the official + * split-button layout). Selections apply live mid-call. + */ +function DeviceFields({ + controller, + snapshot, + fields, + locale, +}: { + readonly controller: CallsController; + readonly snapshot: CallsSnapshot; + readonly fields: readonly DeviceFieldKind[]; + readonly locale: Locale; +}) { + const select = (kind: DeviceFieldKind, deviceId: string) => { + ignoreDisposed(() => { + if (kind === "audioOutput") + controller.setPreferredDevices({ audioOutput: deviceId }); + else if (deviceId.length > 0) + void controller.switchDevice(kind, deviceId); + else controller.setPreferredDevices({ [kind]: undefined }); + }); + }; + return ( + <> + {fields.map((kind) => { + if (kind === "audioOutput" && !canSetSink) return null; + const group = snapshot.devices.filter( + (d) => d.kind === DEVICE_KIND[kind], + ); + return ( + + ); + })} + + ); +} + +// ── IncomingCallCard ────────────────────────────────────────────────── + +export interface IncomingCallCardProps extends ControllerProps { + /** Human name to show instead of the raw number, if the app knows one. */ + readonly displayName?: string; + readonly avatarUrl?: string; + readonly resolveAvatar?: AvatarResolver; + readonly className?: string; +} + +/** + * The incoming-call window: name, "WhatsApp audio/video call" subtitle, the + * media middle (avatar for audio; a mirrored self-preview with camera/mic + * toggles and a ⋯ device menu for video), then Decline / Accept. Answering a + * video offer with the camera toggled off still acquires video muted, so the + * in-call camera toggle can enable it later. + */ +export function IncomingCallCard({ + controller, + createController, + displayName, + avatarUrl, + resolveAvatar, + className, +}: IncomingCallCardProps) { + const resolved = useResolvedController(controller, createController); + const snapshot = useController(resolved); + const root = useCallsRoot(); + const { locale } = root; + const incoming = snapshot.status === "incoming"; + const offersVideo = incoming && snapshot.video && snapshot.capabilities.video; + const [cameraOn, setCameraOn] = useState(true); + const [preMuted, setPreMuted] = useState(false); + // The card returns null between calls instead of unmounting, so these + // toggles would otherwise carry into the next call and mute it with nothing + // on screen to explain why. Reset during render, before the preview reads + // them, so the camera is never acquired against a stale choice. + const [toggledFor, setToggledFor] = useState(snapshot.callId); + if (snapshot.callId !== toggledFor) { + setToggledFor(snapshot.callId); + setCameraOn(true); + setPreMuted(false); + } + const preview = useSelfPreview( + offersVideo && cameraOn, + snapshot.selectedDevices.videoInput, + ); + const previewRef = useRef(null); + const peerAvatar = usePeerAvatar(snapshot.peer, resolveAvatar, avatarUrl); + const { open, toggle } = useDevicePanels(resolved, snapshot); + + useEffect(() => attach(previewRef.current, preview), [preview]); + + if (!incoming) return null; + const peer = snapshot.peer ?? ""; + const name = displayName ?? peer; + + // A remote hang-up between the click and the call reaching the controller + // makes both of these reject: the call is no longer incoming. The snapshot + // already shows the call ended, so the rejection only needs absorbing. + const accept = () => + void resolved + .answer({ video: offersVideo }) + .then(() => { + if (preMuted || (offersVideo && !cameraOn)) + resolved.setMuted({ + ...(preMuted ? { audio: true } : {}), + ...(offersVideo && !cameraOn ? { video: true } : {}), + }); + }) + .catch(() => undefined); + + const micToggle = ( + + ); + + return ( +
+
+
{name}
+
+ {t(locale, offersVideo ? "calls.videoCall" : "calls.audioCall")} +
+ + {offersVideo ? ( +
+ {cameraOn && preview !== null ? ( +
+ ) : ( +
+ +
+ )} + + {open === "more" && ( +
+ +
+ )} + +
+
+ + {t(locale, "calls.reject")} +
+
+ + {t(locale, "calls.answer")} +
+
+
+
+ ); +} + +// ── Duration ────────────────────────────────────────────────────────── + +/** + * Seconds since the call connected, ticking once a second (0 before that). + * A call that drops to `reconnecting` keeps counting: the controller holds + * `connectedAt` across the flap, and the call it measures is the same one. + */ +export function useCallDuration( + snapshot: Pick, +): number { + const connectedAt = + snapshot.status === "connected" || snapshot.status === "reconnecting" + ? snapshot.connectedAt + : undefined; + const [seconds, setSeconds] = useState(0); + useEffect(() => { + if (connectedAt === undefined) { + setSeconds(0); + return; + } + const tick = () => + setSeconds(Math.max(0, Math.floor((Date.now() - connectedAt) / 1000))); + tick(); + const timer = setInterval(tick, 1000); + return () => clearInterval(timer); + }, [connectedAt]); + return seconds; +} + +/** Format seconds as `m:ss` (or `h:mm:ss` past an hour). */ +export function formatDuration(totalSeconds: number): string { + const h = Math.floor(totalSeconds / 3600); + const m = Math.floor((totalSeconds % 3600) / 60); + const s = totalSeconds % 60; + const mm = h > 0 ? String(m).padStart(2, "0") : String(m); + const ss = String(s).padStart(2, "0"); + return h > 0 ? `${h}:${mm}:${ss}` : `${mm}:${ss}`; +} + +/** + * Name with the line under it: "Ringing +number…" while an outgoing call + * rings, the peer number until an incoming call connects, then alternating + * number ⇄ duration on the 5-second cadence (crossfaded via keyed remount). + */ +function CallIdentity({ + name, + peer, + live, + ringingLine, + flashPeer, + seconds, +}: { + readonly name: string; + readonly peer: string | null; + readonly live: boolean; + readonly ringingLine: string | null; + readonly flashPeer: boolean; + readonly seconds: number; +}) { + const line = live + ? peer !== null && flashPeer + ? peer + : formatDuration(seconds) + : (ringingLine ?? peer); + const lineKey = live && !(peer !== null && flashPeer) ? "duration" : "peer"; + return ( +
+
{name}
+ {line !== null && ( +
+ {line} +
+ )} +
+ ); +} + +// ── CallStage ───────────────────────────────────────────────────────── + +export interface CallStageProps extends ControllerProps { + readonly displayName?: string; + readonly avatarUrl?: string; + readonly resolveAvatar?: AvatarResolver; + readonly className?: string; + /** Replace the media region (remote/local streams) without forking state. */ + readonly renderMedia?: (streams: { + readonly local?: MediaStream; + readonly remote?: MediaStream; + }) => ReactNode; +} + +/** + * The in-call stage: remote video full-bleed when the far side sends video, + * the centered avatar hero otherwise, the local preview as a + * picture-in-picture tile, and the identity (name + flashing number/duration + * line). Always dark by design. + */ +export function CallStage({ + controller, + createController, + displayName, + avatarUrl, + resolveAvatar, + className, + renderMedia, +}: CallStageProps) { + const resolved = useResolvedController(controller, createController); + const snapshot = useController(resolved); + const root = useCallsRoot(); + const { locale } = root; + const remoteRef = useRef(null); + const localRef = useRef(null); + const [remoteHasVideo, setRemoteHasVideo] = useState(false); + const seconds = useCallDuration(snapshot); + const peerAvatar = usePeerAvatar(snapshot.peer, resolveAvatar, avatarUrl); + const live = snapshot.status === "connected"; + const localStream = resolved.localStream; + const remoteStream = resolved.remoteStream; + + const [flashPeer, setFlashPeer] = useState(false); + useEffect(() => { + if (!live) { + setFlashPeer(false); + return; + } + const timer = setInterval(() => setFlashPeer((v) => !v), 5000); + return () => clearInterval(timer); + }, [live]); + + useEffect(() => { + attach(remoteRef.current, remoteStream); + if (remoteStream === undefined) return; + const update = () => + setRemoteHasVideo(remoteStream.getVideoTracks().length > 0); + update(); + remoteStream.addEventListener("addtrack", update); + remoteStream.addEventListener("removetrack", update); + return () => { + remoteStream.removeEventListener("addtrack", update); + remoteStream.removeEventListener("removetrack", update); + }; + }, [remoteStream, snapshot.revision]); + + useEffect( + () => attach(localRef.current, localStream), + [localStream, snapshot.revision], + ); + + const sink = snapshot.selectedDevices.audioOutput; + useEffect(() => { + const element = remoteRef.current as + (HTMLVideoElement & { setSinkId?: (id: string) => Promise }) | null; + if (element?.setSinkId !== undefined && sink !== undefined) + void element.setSinkId(sink).catch(() => undefined); + }, [sink, snapshot.revision]); + + if (!ACTIVE_STATUSES.has(snapshot.status)) return null; + const peer = snapshot.peer ?? ""; + const showLocalVideo = snapshot.video && !snapshot.videoMuted; + // `live` is false while reconnecting, but the call is established and its + // layout should stay put — the pre-answer hero preview belongs to a call + // that has not connected yet. + const preAccept = + showLocalVideo && !live && snapshot.status !== "reconnecting"; + const ringingLine = + snapshot.status === "reconnecting" + ? `${t(locale, "calls.reconnecting")}…` + : !live && snapshot.direction === "outgoing" + ? t(locale, "calls.ringingTo", { peer }) + : null; + const identity = ( + + ); + + return ( +
+
+ {renderMedia !== undefined ? ( + renderMedia({ + ...(localStream === undefined ? {} : { local: localStream }), + ...(remoteStream === undefined ? {} : { remote: remoteStream }), + }) + ) : ( + <> +
+
+ ); +} + +// ── CallControls ────────────────────────────────────────────────────── + +export interface CallControlsProps extends ControllerProps { + readonly className?: string; + /** Force-hide the camera control even when the line could carry video. */ + readonly disableVideo?: boolean; +} + +/** + * The in-call dock, after the official window: the camera split group + * ([toggle|⌄] — the dropdown picks the camera) and the mic split group + * ([toggle|⌄] — the dropdown picks microphone + speaker) on the left, the + * red hang-up pill on the right. The camera group renders whenever the line + * can carry video (unless `disableVideo`) and is disabled until the call + * connects. Video is per direction: the camera controls only the outgoing + * stream. + */ +export function CallControls({ + controller, + createController, + className, + disableVideo, +}: CallControlsProps) { + const resolved = useResolvedController(controller, createController); + const snapshot = useController(resolved); + const root = useCallsRoot(); + const { locale } = root; + const { open, toggle } = useDevicePanels(resolved, snapshot); + if (!ACTIVE_STATUSES.has(snapshot.status)) return null; + + const live = snapshot.status === "connected"; + const showCamera = snapshot.capabilities.video && disableVideo !== true; + // The affordance stays visible on an audio call (the settled design) and is + // disabled until it can act: connected, and either already a video call or + // an upgrade the media session can actually perform. A session that cannot + // renegotiate therefore shows the same disabled button as a call that has + // not connected yet, instead of a live one that does nothing. + const cameraReady = live && (snapshot.video || resolved.canEnableVideo); + const cameraOff = !snapshot.video || snapshot.videoMuted; + const onCamera = () => { + // On a video call the button mutes/unmutes the outgoing track; on an + // audio call it upgrades to video (camera + re-offer on the same + // connection) through the controller. + if (snapshot.video) + ignoreDisposed(() => resolved.setMuted({ video: !snapshot.videoMuted })); + // A denied camera, a failed re-offer, or a call that ended mid-upgrade all + // reject here. The upgrade rolls itself back, so the audio call carries on + // and the button stays live for another try; swallowing the rejection just + // keeps a declined camera from surfacing as an unhandled error. + else void resolved.enableVideo().catch(() => undefined); + }; + + return ( +
+
+ {open === "mic" && ( +
+ +
+ )} + {open === "cam" && ( +
+ +
+ )} +
+ {showCamera && ( +
+ + +
+ )} + {snapshot.capabilities.mute && ( +
+ + +
+ )} + +
+
+
+ ); +} + +// ── DialPad ─────────────────────────────────────────────────────────── + +const KEYS = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "0", + "#", +] as const; + +export interface DialPadProps extends ControllerProps { + /** Prefill the number field. */ + readonly defaultValue?: string; + /** Calling line the call is placed over. Defaults to `linkedDevice`. */ + readonly line?: CallLine; + /** Called once the call is actually placed, not when a placement fails. */ + readonly onPlaced?: (to: string) => void; + readonly className?: string; +} + +/** + * A dial pad card: E.164 entry plus place-call buttons — audio always, and a + * video-call button when the line can carry video (linked device; the + * Business Calling API line is audio-only, so the button never renders there). + */ +export function DialPad({ + controller, + createController, + defaultValue = "", + line = "linkedDevice", + onPlaced, + className, +}: DialPadProps) { + const resolved = useResolvedController(controller, createController); + const snapshot = useController(resolved); + const root = useCallsRoot(); + const { locale } = root; + const [value, setValue] = useState(defaultValue); + const busy = + snapshot.status === "incoming" || ACTIVE_STATUSES.has(snapshot.status); + const canVideo = capabilitiesFor(line).video; + const dial = (video: boolean) => { + const to = value.trim(); + // The buttons are disabled while a call is up, but the input keeps focus + // and its value, so Enter would otherwise place a second call — and + // `place()` aborts the current operation, tearing down the live call. + if (busy || to.length === 0) return; + // `place()` resolves even when the placement failed — the controller + // reports that through the snapshot — so the snapshot is what says + // whether there is now a call to announce. + void resolved + .place(to, { video, line }) + .then(() => { + if (ACTIVE_STATUSES.has(resolved.getSnapshot().status)) onPlaced?.(to); + }) + // A placement failure lands in the snapshot, not here, but `place()` + // does reject on a disposed controller — which a mounted pad holding a + // shared one can still be dialled against. + .catch(() => undefined); + }; + + return ( +
+
+ setValue(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Enter") dial(false); + }} + aria-label={t(locale, "calls.dialPlaceholder")} + /> +
+ {KEYS.map((digit) => ( + + ))} +
+
+ + {canVideo && ( + + )} + +
+
+
+ ); +} + +// ── Pop-out call window ─────────────────────────────────────────────── + +export interface CallPopoutHandle { + /** True while the separate call window is open. */ + readonly popped: boolean; + /** Open the call window (no-op if already open or the popup was blocked). */ + readonly open: () => void; + /** Close the call window (popping the call back inline). */ + readonly close: () => void; + /** Portal target inside the call window; null while not popped. */ + readonly container: HTMLElement | null; +} + +/** + * Owns a separate, freely resizable call window (like the desktop client's) + * and hands back a container to portal the stage + controls into. Media + * streams keep playing across same-origin windows, so portal'd `