> ## 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.

# Communicate

> Send messages, update conversations, handle calls, presence, and media.

Use these methods for one-to-one and group conversation activity after a number is connected.

<CardGroup cols={2}>
  <Card title="Send a message" icon="message" href="/guides/communicate/api/send-message?playground=open">
    Send text, media, interactive, location, contact, or native Flow content.
  </Card>

  <Card title="Message state" icon="check-double" href="/guides/communicate/api/send-seen?playground=open">
    Mark messages as seen, react, edit, delete, archive, or show typing state.
  </Card>

  <Card title="Media" icon="photo-film" href="/guides/communicate/api/get-media-info?playground=open">
    Inspect, upload, download, or persist media.
  </Card>

  <Card title="Presence and calls" icon="phone" href="/guides/communicate/api/get-chat-presence?playground=open">
    Observe presence where enabled and control supported call actions.
  </Card>
</CardGroup>

## Message content

Send `conversation` and a `content` object containing exactly one message kind. Do not send
`type` or place message fields at the request root.

```json theme={null}
{
  "conversation": { "phoneNumber": "+15550001111" },
  "content": {
    "location": { "lat": 33.89, "long": 35.50, "address": "Beirut" }
  }
}
```

For text, use `"content": { "text": "Hello" }`. For media, put its URL or
base64 bytes and caption inside `content.image`, `content.video`, `content.file`,
or `content.voice`. Supply exactly one media source.

`mentions`, `quotedMessage`, and `isForwarded` remain alongside `conversation` and `content`.
Requests with missing content, multiple message kinds, or misplaced fields fail
validation. `requestPhoneNumber` requires the contact's `conversation.id`.

## Interactive message request rules

Use one of `content.product`, `content.productList`, `content.order`,
`content.list`, `content.buttons`, `content.addressMessage`, or `content.flow`.

Products and product lists use `businessOwnerId`; orders use `sellerId`.
These are public identity IDs for a business with a known phone number.
Catalog responses expose the owner's public identity ID in `belongsTo`.
Product and collection IDs remain their original commerce identifiers.

| Input | Accepted values |
| - | - |
| Product-list sections | At most 30 product IDs across all sections; every product ID must be unique. |
| Native-list sections | At most 30 rows across all sections; every row ID must be unique. |
| `content.flow.dataJson` | A non-null JSON object encoded as a string, at most 16 KiB (16,384 UTF-8 bytes). Arrays, scalar JSON values, and invalid JSON are rejected. |

For `content.addressMessage`, requirements depend on the connected backend. Meta Cloud
API sessions require `content.addressMessage.body` and a two-letter uppercase
`content.addressMessage.country` code. Linked Device API sessions require
`content.addressMessage.body` and a nonblank `content.addressMessage.buttonText` call to action.

The method page is the request contract. Open its playground for parameters, schemas, responses, and raw cURL. Verified SDK tabs appear only when the corresponding client method exists.

## Conversation identity

Send with `conversation.id`, `conversation.phoneNumber`, or `conversation.bsuid`.
At least one is required. If you supply several selectors, they must already
identify the same conversation. Supplying aliases does not link accounts.

IDs are opaque decimal strings scoped to the connected Number. Keep them as
strings. BSUIDs are scoped to the connected business portfolio. A Linked Device
connection requires an already known routable identity for a BSUID. Phone
numbers use E.164, including `+`. A username alone is not a send destination.

On a Meta Cloud API number, a conversation known only by its BSUID is sent to
that BSUID. When a phone number is also known for the conversation, the message
goes to the phone number. Reply to a user who messaged you without a visible
phone number by sending to the received `conversation.id`. WhatsApp does not
deliver one-tap, zero-tap, or copy-code authentication templates to a BSUID;
those sends fail with `invalid_parameter`.

Received messages identify their conversation with `id` and any available
`phoneNumber`, `bsuid`, or `username`. Received group messages include the author under
`conversation.sender`, with `id` and any available `phoneNumber`, `bsuid`, or `username`.
Send requests and send results omit `sender`. A hidden phone number is omitted;
a BSUID is never converted into a Linked Device user ID.

Message actions such as marking a message seen, reacting, and starring use `id`
for the message identifier and `conversation` for its destination:

```json theme={null}
{
  "conversation": { "id": "739182640518203" },
  "id": "739182640518204"
}
```

## Message identifiers

Message responses contain a Polymorfa `id` and a `whatsapp_ids` object with
exact provider references. `linked_devices` contains the Linked Device reference;
`official_api` contains the Official API reference. Unknown references are omitted.
Use `id` for message actions and `quotedMessage.id` for replies. Keep every ID as
a string. The Polymorfa ID does not encode test mode or connection type.
Acknowledgements return `messages`, an array of
`{ id, whatsapp_ids, whatsapp_id? }`. The singular field is a temporary alias
and is omitted when its provider reference is unavailable.
The earlier `whatsapp_id` field remains in native message responses and events
during the API version migration. Read `whatsapp_ids` for new integrations.

Polymorfa retains these message and routing identifiers with Hosted Message
Storage disabled. This does not enable message-content storage.

Cloud requests return their result synchronously, including when you send
`Prefer: respond-async`. Linked-device requests that accept the preference
return `202` with `Preference-Applied: respond-async`; their result arrives in
`command.result`. Do not assume a preference was applied without that header.

When a message action fails, its `docs` URL points to the matching failure
section on that method's reference page. `result_unknown` means the command's
result was not received; it does not prove the action failed. Check resulting
events before retrying. A dispatch failure returns `command_dispatch_failed`
with status `503`.

## Retry sends safely

Send an `Idempotency-Key` header with a message write to make it safe to
retry. Use a new random value, such as a UUID, for each message or action, and
send the same value when you retry it. The key is 1 to 255 bytes of UTF-8.

These methods accept the header:

| Method | Request |
| - | - |
| [Send message](/guides/communicate/api/send-message) | `POST /messaging/{session}/messages/send` |
| [Send reaction](/guides/communicate/api/send-reaction) | `POST /messaging/{session}/messages/react` |
| [Edit message](/guides/communicate/api/edit-message) | `PUT /messaging/{session}/chats/{conversation}/messages/{messageId}` |
| [Delete message](/guides/communicate/api/delete-message) | `DELETE /messaging/{session}/chats/{conversation}/messages/{messageId}` |
| [React to channel message](/guides/channels/api/react-to-channel-message) | `POST /messaging/{session}/channels/{id}/messages/{messageId}/reaction` |
| [Create project campaign](/guides/engage-at-scale/api/create-project-campaign) | `POST /messaging/projects/{projectSlug}/campaigns` |
| [Launch project campaign](/guides/engage-at-scale/api/launch-project-campaign) | `POST /messaging/projects/{projectSlug}/campaigns/{id}/launch` |

Other messaging methods ignore the header.

```bash theme={null}
curl https://api.polymorfa.com/messaging/support/messages/send \
  -H "Authorization: Bearer $POLYMORFA_API_KEY" \
  -H "Idempotency-Key: 5f0c2b8e-4f7a-4f5e-9d1b-1c2d3e4f5a6b" \
  -H "Content-Type: application/json" \
  -d '{"conversation":{"phoneNumber":"+15550001111"},"content":{"text":"Your order shipped."}}'
```

For 24 hours after the first request, Polymorfa answers a request that reuses
the key as follows:

| Retry | Result |
| - | - |
| Same request, first request succeeded | `409` [`idempotency_completed`](/api/errors#idempotency-completed) with `Idempotent-Replayed: true`. The message is not sent again. |
| Same request, first request returned a server error | The same status and error code, with `Idempotent-Replayed: true`. Nothing runs again. |
| Same request, first request still running | `409` [`idempotency_in_progress`](/api/errors#idempotency-in-progress) with `Retry-After`. |
| Same request, first request's outcome not recorded | `409` [`idempotency_outcome_unknown`](/api/errors#idempotency-outcome-unknown). Nothing runs again. |
| Different method, path, body, or `Prefer: respond-async` choice | `409` [`idempotency_conflict`](/api/errors#idempotency-conflict). Nothing runs. |

Keys belong to the credential and API version that sent them. Retry with the
same credential and `Polymorfa-Version`: the same key sent with another API
key, project token, client token, or API version is a separate request.

Polymorfa records successful results and server errors. A server error such as
`result_unknown` or `command_dispatch_failed` means the message may have been
sent, so a retry with the same key returns that error again instead of sending
a second copy. Check message events for the outcome, and use a new key only
when you have confirmed that the message was not sent. A `4xx` error,
`session_not_ready`, or `credential_verification_unavailable` means nothing
ran; retry with the same key after you fix the cause. A server error from
the catalog check on a Graph product or product-list send also means nothing
was sent; a retry with the same key runs the send again.

Polymorfa keeps the key, a hash of the request, and the response status and
error code for 24 hours. It does not keep message content, message IDs,
recipients, or the response body, so a retry after a success can't return the
original response. Keep the first response when you receive it, and use
message events or webhooks to find a message whose response was lost.

If the first request started but its outcome wasn't recorded, the message might
have been sent. The key then returns `idempotency_outcome_unknown` for 24 hours
instead of sending again. Check message events, and use a new key only after you
confirm that the message was not sent.

In the TypeScript SDK, pass the key in the request options:
`messages.send(session, body, { idempotencyKey })`. The SDK retries a request
that carries a key after a network failure or a retryable status, using the
same key. If the first attempt succeeded but its response was lost, the retry
fails with `idempotency_completed`. See [SDKs](/sdks/overview).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.