> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polymorfa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Linked-device controls

> Manage privacy, presence observation, disappearing messages, contacts, groups, channels, and incoming calls through the native Messaging API.

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](/guides/communicate/api/subscribe-presence).

## Presence observation

Presence and typing observation default to `off`. Set the maximum policy for a
project with `PUT /messaging/projects/{projectId}/observation-policy`:

```json theme={null}
{
  "presenceMode": "events",
  "typingMode": "cache",
  "labelMode": "events",
  "quickReplyMode": "events"
}
```

Each setting accepts one of these modes:

| Mode     | Webhooks | In-memory lookup cache |
| -------- | -------- | ---------------------- |
| `off`    | No       | No                     |
| `events` | Yes      | No                     |
| `cache`  | Yes      | Yes                    |

`labelMode` accepts one further mode, `project`, which adds a durable
project-scoped projection. See [Label observation](#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.

| Mode      | `labels.update` webhooks | Synchronous reads | Retention                         |
| --------- | ------------------------ | ----------------- | --------------------------------- |
| `off`     | No                       | Disabled          | None                              |
| `events`  | Yes                      | Unknown           | None                              |
| `cache`   | Yes                      | Best-effort       | Bounded session memory            |
| `project` | Yes                      | Best-effort       | Project-scoped durable projection |

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.

| Mode     | `business.quick_reply.update` webhooks | Synchronous reads | Retention              |
| -------- | -------------------------------------- | ----------------- | ---------------------- |
| `off`    | No                                     | Disabled          | None                   |
| `events` | Yes                                    | Unknown           | None                   |
| `cache`  | Yes                                    | Best-effort       | Bounded session memory |

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}`:

```json theme={null}
{
  "historySync": {
    "mode": "metadata_only",
    "requestFull": false
  }
}
```

`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}`:

```bash theme={null}
curl -X PUT "https://api.polymorfa.com/messaging/$SESSION/privacy/readreceipts" \
  -H "Authorization: Bearer $POLYMORFA_PROJECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"value":"none"}'
```

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`:

```json theme={null}
{ "durationSeconds": 86400 }
```

`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:

| Operation                                                                  | Path                                                                     |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Read live status, stable user ID, aliases, picture ID, and device metadata | `GET /contacts/{contactId}/info`                                         |
| Resolve a phone number or username to a stable user ID                     | `GET /identities/resolve`                                                |
| Read the identity verification code for a stable user ID                   | `GET /users/{id}/security-code`                                          |
| List linked devices                                                        | `GET /contacts/{contactId}/devices`                                      |
| Read a WhatsApp business profile                                           | `GET /contacts/{contactId}/business-profile`                             |
| Read blocked contact IDs                                                   | `GET /contacts/blocked`                                                  |
| Block or unblock a contact                                                 | `POST /contacts/{contactId}/block`, `POST /contacts/{contactId}/unblock` |

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](/api/user-identities).

Catalog, product, collection, and order reads are documented under
[Catalogs and commerce](/guides/waba-features/catalog-commerce).

## Group settings

Use these operations after creating or joining a group:

| Setting                        | Path                                      | Request                   |
| ------------------------------ | ----------------------------------------- | ------------------------- |
| Who can edit group information | `PUT /groups/{id}/settings/info-edit`     | `{ "adminsOnly": true }`  |
| Who can send messages          | `PUT /groups/{id}/settings/messages`      | `{ "adminsOnly": true }`  |
| Who can add members            | `PUT /groups/{id}/settings/member-add`    | `{ "mode": "admin_add" }` |
| Require join approval          | `PUT /groups/{id}/settings/join-approval` | `{ "required": true }`    |

`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`:

```json theme={null}
{
  "success": true,
  "data": { "status": "DELETED" }
}
```

After creating or following a channel, you can read and interact with its
messages:

| Operation                             | Path                                                                               |
| ------------------------------------- | ---------------------------------------------------------------------------------- |
| List messages                         | `GET /channels/{id}/messages?count=50&before={serverId}`                           |
| List view and reaction updates        | `GET /channels/{id}/message-updates?count=50&since={unixSeconds}&after={serverId}` |
| Mark a message viewed                 | `POST /channels/{id}/messages/{serverId}/viewed`                                   |
| Add or remove your reaction           | `POST /channels/{id}/messages/{serverId}/reaction`                                 |
| Subscribe temporarily to live updates | `POST /channels/{id}/live-updates`                                                 |

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:

```bash theme={null}
curl -X POST "https://api.polymorfa.com/messaging/$SESSION/calls/$CALL_ID/reject" \
  -H "Authorization: Bearer $POLYMORFA_PROJECT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from":"15551234567@s.whatsapp.net"}'
```

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.
