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

# Send and receive messages

> Send text, media, and templates, reply and react, and download media with MessagingClient.

Send messages through a connected Number with `messaging.messages`. Messages you
receive arrive as webhooks.

| Task | Permission |
| - | - |
| Send, react, star, mark seen, show typing | `messages:write` |
| Edit and delete sent messages | `chats:manage` |
| Download media | `media:read` |
| Read stored history (beta) | `chats:read`, `messages:read` |

## Send a text message

```typescript theme={null}
const sent = await messaging.messages.send("<number-id>", {
  conversation: { phoneNumber: "+15550001111" },
  content: { text: "Your order has shipped." },
});

console.log(sent.data.data.id, sent.data.data.status);
```

`conversation` picks the recipient with one of these fields:

* `phoneNumber`: an E.164 phone number.
* `id`: the Polymorfa conversation ID from an earlier message or webhook.
* `bsuid`: a business-scoped user ID.

If you pass more than one, they must point to the same conversation. A username
alone is not a destination.

`send` creates an idempotency key for you, so its automatic retries never send
twice. Pass your own `idempotencyKey` to stay safe across process restarts. See
[Retry a write safely](/sdks/typescript/errors#retry-a-write-safely).

## Send an image or a file

```typescript theme={null}
await messaging.messages.send("<number-id>", {
  conversation: { phoneNumber: "+15550001111" },
  content: {
    image: { url: "https://example.com/receipt.png", caption: "Your receipt" },
  },
});

await messaging.messages.send("<number-id>", {
  conversation: { phoneNumber: "+15550001111" },
  content: {
    file: { url: "https://example.com/invoice.pdf", filename: "invoice.pdf" },
  },
});
```

Each media kind (`image`, `video`, `file`, `voice`) takes either `url` or
`base64`, not both. Set `voice.ptt` to `true` to send a voice note.

## Send a template

```typescript theme={null}
await messaging.messages.send("<number-id>", {
  conversation: { phoneNumber: "+15550001111" },
  content: {
    template: {
      name: "order_ready",
      language: "en_US",
      components: [
        { type: "body", parameters: [{ type: "text", text: "Ada" }] },
      ],
    },
  },
});
```

The template must be approved in that language, or the send fails with
`template_not_approved`. On an Official API Number, a free-form message sent
more than 24 hours after the customer last wrote fails with
`conversation_window_closed`; send a template instead. See
[Templates](/sdks/typescript/templates).

## Other message kinds

`content` holds exactly one message kind. Besides text, media, and templates, the
SDK types cover `poll`, `location`, `contact`, `buttons`, `list`, `product`,
`productList`, `order`, `requestPhoneNumber`, `addressMessage`, `flow`, and
`callPermissionRequest`. Your editor lists the fields of each kind from the
`SendMessageRequest` type.

```typescript theme={null}
await messaging.messages.send("<number-id>", {
  conversation: { phoneNumber: "+15550001111" },
  content: {
    buttons: {
      body: "Confirm your appointment?",
      buttons: [
        { type: "reply", text: "Confirm", id: "confirm" },
        { type: "reply", text: "Reschedule", id: "reschedule" },
      ],
    },
  },
});
```

## Reply to a message

```typescript theme={null}
await messaging.messages.send("<number-id>", {
  conversation: { id: "<conversation-id>" },
  content: { text: "Thanks, we got it." },
  quotedMessage: { id: "<message-id>" },
});
```

Set `isForwarded: true` to mark a message as forwarded.

## React, mark as read, and show typing

```typescript theme={null}
const conversation = { id: "<conversation-id>" };

await messaging.messages.react("<number-id>", { conversation, id: "<message-id>", reaction: "👍" });
await messaging.messages.markSeen("<number-id>", { conversation, id: "<message-id>" });
await messaging.messages.setTyping("<number-id>", { conversation, state: "typing", id: "<message-id>" });
```

Official API Numbers need the `id` of an inbound message for `setTyping`. They
mark it read and show typing for up to 25 seconds or until you reply. They
support only the `typing` state. Linked-device Numbers also accept `recording`
and `paused`.

## Edit or delete a sent message

```typescript theme={null}
await messaging.chats.editMessage("<number-id>", "<conversation-id>", "<message-id>", {
  text: "Your order ships tomorrow.",
});

await messaging.chats.deleteMessage("<number-id>", "<conversation-id>", "<message-id>");
```

## Message IDs

Every message has a Polymorfa `id`. Use it for replies, reactions, edits, and
deletes. Keep it as a string, even when it contains only digits.

`whatsapp_ids` holds the WhatsApp references Polymorfa observed, under
`linked_devices`, `official_api`, or both. They are not interchangeable with
the Polymorfa `id`. The older `whatsapp_id` field is deprecated; read
`whatsapp_ids` instead. See
[Message identifiers](/guides/messaging/overview#message-identifiers).

## Receive messages

Incoming messages arrive as `message.received` webhooks. Verify the signature,
then narrow the event:

```typescript theme={null}
import { isEvent, webhooks } from "@polymorfa/sdk";

export async function handleWebhook(body: ArrayBuffer, signature: string, secret: string) {
  const event = await webhooks.verify({ body, signature, secret });

  if (isEvent(event, "message.received")) {
    console.log(event.session, event.payload);
  }
}
```

See [Webhooks](/sdks/typescript/webhooks) to create the endpoint and verify
requests. To read events without a public endpoint, see
[Events](/sdks/typescript/events).

## Download media

Stream a media file by its Polymorfa media ID:

```typescript theme={null}
const download = await messaging.media.downloadStream("<media-id>");
console.log(download.contentType, download.filename);
// download.body is a ReadableStream<Uint8Array>. Read it once.
```

In Node.js, write it straight to a file:

```typescript theme={null}
import { downloadMediaToFile } from "@polymorfa/sdk/node";

await downloadMediaToFile(messaging.media, "<media-id>", "./attachment.bin", {
  maxBytes: 50 * 1024 * 1024,
});
```

The helper writes to a temporary file and moves it into place only after the
download finishes. Treat `filename` as a display name, not a path.

When a project does not store media, inbound media webhooks carry an encrypted
`media` descriptor instead. `messaging.media.downloadFromWhatsApp(event.payload)`
downloads and decrypts the file from WhatsApp without calling the Polymorfa API.
The descriptor contains a decryption key, so never log stored webhook bodies.

## Read stored message history

<Note>
  Message history is a beta. It works only for Numbers with hosted message storage
  on, in teams enrolled in the beta. See [Read message history](/guides/messaging/message-history).
</Note>

```typescript theme={null}
const page = await messaging.chats.listMessages("<number-id>", "+15550001111", {
  limit: 50,
  order: "desc",
});

for (const message of page.data.data) console.log(message.id, message.text);

if (page.data.hasMore && page.data.nextCursor) {
  const older = await messaging.chats.listMessages("<number-id>", "+15550001111", {
    cursor: page.data.nextCursor,
    order: "desc",
  });
  console.log(older.data.data.length);
}
```

`chats.list` lists stored conversations. `chats.downloadMessageMediaStream`
downloads the stored copy of a message's media and also needs `media:read`.


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