Skip to main content
Use the native Messaging API to control features of a paired WhatsApp account. Session paths below start with https://api.polymorfa.com/messaging/{session} and require the scope shown in the generated reference. Most session paths accept an team credential. The presence and observation-policy paths also accept a project credential. A credential kind that is not accepted for a path returns 403 before the request reaches the session, and holding the right scope does not change that. Client tokens are authorized by their recorded actions rather than by scopes. The per-operation credential kinds and client actions for presence are listed in SDK availability.

Presence observation

Presence and typing observation default to off. Set the maximum policy for a project with PUT /messaging/projects/{projectId}/observation-policy:
Each setting accepts one of these modes: labelMode accepts one further mode, project, which adds a durable project-scoped projection. See Label observation. Read the project ceiling with GET /messaging/projects/{projectId}/observation-policy. A session can inherit that ceiling or select an equally restrictive mode with GET|PUT /messaging/{session}/observation-policy. A session override accepts inherit, off, events, or cache, and project for labelMode; selecting a mode above the project ceiling returns 400. Policy updates require presence:observe. Changing labelMode additionally requires labels:manage; an older client can omit labelMode without changing the existing label policy. Presence subscriptions require presence:observe. Reading the policy or cached state requires presence:read, while changing the linked account’s own presence requires presence:write. Subscribe to one user for 120 seconds with POST /messaging/{session}/presence/{conversation}/subscribe. Then read the latest allowed observation with GET /messaging/{session}/presence/{conversation}. A GET never subscribes and never performs a live WhatsApp lookup. It returns status as unknown, fresh, or stale; unknownReason is disabled, not_observed, or suspended. Cached records include observedAt, may include lastSeen, and become stale after 180 seconds. GET /messaging/{session}/presence reports the session’s last requested and last successfully sent self-presence values. Its authoritative field is always false; the linked-device protocol does not provide a synchronous server read of the current session presence. Observation is bounded per session to 256 cached identities and 64 active targets. Within ten minutes, a session can request at most 128 unique targets, 240 total subscriptions, and 12 renewals for one target. Exceeding a limit returns 429, clears cached presence records and active subscriptions, and suspends presence observation for 15 minutes. Subscription attempts during that window also return 429. Typing observation is unaffected: its policy, its webhooks, and any cached chat state continue under typingMode. Messaging remains connected. Presence observations are ephemeral and are not written to PostgreSQL.

Label observation

labelMode controls whether Polymorfa retains label definitions and chat-label associations. It accepts off, events, cache, or project. The default is off: label observation is opt-in, like presence and typing observation. A session override also accepts inherit and cannot exceed the project setting. Changing project to a lower-retention mode removes the durable projection, including projections for disconnected sessions. Removal runs in the background. Re-enabling project while that removal is still in progress returns 409; retry once it completes. If 409 persists, contact support. Read definitions with GET /messaging/{session}/labels and one chat’s assignments with GET /messaging/{session}/labels/chats/{conversation}. These existing routes return the label array by default. Add ?includeObservation=true to receive policy, status, and labels. status is disabled, unknown, partial, or fresh. An unknown response includes unknownReason as observation_disabled, not_retained, not_observed, or expired. Cache results include observedAt and expiresAt; cache observations expire after 15 minutes. Durable results include observedAt without a cache expiry. A full linked-device sync replaces the durable projection as one generation. Incremental events received before that snapshot return partial, so clients can distinguish remembered data from a complete snapshot. PUT /messaging/{session}/labels/chats/{conversation} has replacement semantics and accepts an empty labels array to remove all assignments. Replacement requires an authoritative cached or durable view of the chat. If that view is unavailable, the endpoint returns 409 without sending a partial update. Label updates that omit name or color have the same requirement so omitted fields are never overwritten with empty values.

Quick-reply observation

quickReplyMode controls observation of WhatsApp Business App quick replies. It accepts off, events, or cache and defaults to events. A session override also accepts inherit and cannot exceed the project setting. Read remembered entries with GET /messaging/{session}/business/quick-replies. The response includes policy, status, and quickReplies. status is disabled, unknown, partial, or fresh. An unknown response includes unknownReason as observation_disabled, not_retained, or not_observed. A full linked-device sync replaces the in-memory projection atomically. Incremental changes received before that snapshot return partial. The cache retains at most 256 entries within a 256 KiB payload budget per session and is cleared when observation is disabled. A GET never performs a live WhatsApp lookup, and quick-reply content is not written to PostgreSQL. During a full sync, the incoming 256 KiB payload budget is staged separately until it replaces the previous snapshot or the sync fails.

History sync processing

History sync defaults to metadata-only processing. Configure a linked-device session with PUT /messaging/sessions/{session}:
mode accepts metadata_only or deliver. metadata_only commits a durable, project-scoped record containing the chunk identity, sync type, order, progress, file length, item counts, outcome, and timestamps. It does not marshal the history protobuf, publish message bodies, write per-message history secrets, or create temporary files. deliver publishes the compressed protobuf through the durable history.sync webhook path before acknowledging the chunk. The linked device sends hist_sync only after the selected acceptance point. Duplicate notifications reuse the stored outcome and event identity, including a chunk the device re-uploads under a new identifier after a failure: it resolves to the original record and is not delivered twice. A chunk whose delivery attempt fails keeps its metadata row unacknowledged so it is retried. A chunk whose metadata write itself fails leaves no row at all, so it is neither acknowledged nor retried — the linked device resends it on the next sync. Metadata rows contain no protobuf, message body, contact body, media key, or download path. Acknowledged metadata is retained for 30 days. Unacknowledged or failed metadata is retained for 90 days so delivery failures remain diagnosable without allowing the table to grow without bound. deliver refuses a chunk larger than the event transport carries, records the outcome as payload_too_large, and does not acknowledge it. The chunk’s metadata row is still written, so the sync type, item counts, and size stay visible in metadata_only terms. Keep requestFull disabled if you need every chunk of a large history delivered as a webhook payload. requestFull defaults to false. When enabled, it applies on the next pairing registration and requests at most 30 days, 64 MB per full sync, and 128 MB of declared storage quota. A change made while the session is waiting for a QR scan still reaches that pairing. Updating the processing mode applies to a connected runner immediately; changing requestFull does not initiate a live lookup or download. Deleting a session deletes its history-sync metadata with it, before the retention windows above elapse.

Privacy and disappearing messages

Read all privacy categories with GET /privacy. Update one category with PUT /privacy/{setting}:
The available setting names and values are defined by the SetPrivacySettingRequest schema in the generated reference. Invalid setting/value combinations return 400 without changing the account. Set the default timer for new conversations with PUT /privacy/disappearing/default. Set a timer for one direct or group chat with PUT /chats/{conversation}/disappearing:
durationSeconds accepts 0, 86400, 604800, or 7776000, representing off, 24 hours, 7 days, or 90 days. A direct-chat timer update uses the same encrypted message transport as other outgoing direct-chat messages.

Contact metadata and blocklist

The contact endpoints expose cached contact names and live account metadata: Live metadata comes from the linked account. Empty profile fields remain empty strings or arrays; they do not imply that the contact is a business account. Contact and participant objects use the stable id as their primary key; phone numbers and usernames are optional aliases. See User identities. Catalog, product, collection, and order reads are documented under Catalogs and commerce.

Group settings

Use these operations after creating or joining a group: member-add accepts admin_add or all_member_add.

Channels

Create a channel with POST /channels, and delete a channel you own with DELETE /channels/{id}. Deletion is permanent. The delete request returns success only after WhatsApp confirms that the channel state is DELETED:
After creating or following a channel, you can read and interact with its messages: Send an empty reaction string to remove your existing reaction. The live updates response contains durationSeconds; subscribe again after that period if you still need live channel updates.

Reject an incoming call

Use the callId and caller ID from the incoming-call webhook:
The operation rejects an incoming call. It does not initiate or answer calls.

Unsupported operations

An operation that cannot be performed by the linked-device protocol returns 501. Treat 501 as a capability boundary rather than retrying it. The generated native endpoint reference lists the exact request and response schemas for every operation.