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

# Message types

> Every send content variant in zapo — the typed builders discriminated by `type`, and the raw Proto.IMessage fields the library recognizes on receive.

Everything you send goes through `client.message.send(to, content, options?)`. The `content` argument is a `WaSendMessageContent`:

```ts theme={null}
type WaSendMessageContent =
  | string                        // shorthand for a text message
  | WaSendTextMessage             // type: 'text'
  | WaSendReactionMessage         // type: 'reaction'
  | WaSendRevokeMessage           // type: 'revoke'
  | WaSendPinMessage              // type: 'pin' | 'unpin'
  | WaSendKeepMessage             // type: 'keep' | 'unkeep'
  | WaSendPollMessage             // type: 'poll'
  | WaSendPollVoteMessage         // type: 'poll-vote'
  | WaSendEventMessage            // type: 'event'
  | WaSendEventResponseMessage    // type: 'event-response'
  | WaSendMediaMessage            // type: 'image' | 'video' | 'ptv' | 'audio' | 'document' | 'sticker' | 'sticker-pack'
  | Proto.IMessage                // raw protobuf — anything not covered above
```

There are two ways to send: a **typed builder** (an object with a `type` discriminator — the library validates and fills protocol fields for you) or a **raw `Proto.IMessage`** (you build the protobuf yourself). The same `send()` accepts both.

## Shorthand

A plain string is sent as a text message:

```ts theme={null}
await client.message.send(jid, 'Hello!')
```

## Typed builders

Each builder is discriminated by its `type` field. Bold fields are required.

### Text & media

| `type`         | Type                       | Required fields                                                                  | Guide                                                      |
| -------------- | -------------------------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `text`         | `WaSendTextMessage`        | **`text`**                                                                       | [Sending messages](/en/guides/sending-messages#plain-text) |
| `image`        | `WaSendMediaMessage`       | **`media`** (mimetype optional with a `detectMimetype` processor)                | [Media](/en/guides/media#images)                           |
| `video`        | `WaSendMediaMessage`       | **`media`** (mimetype optional with a `detectMimetype` processor)                | [Media](/en/guides/media#video)                            |
| `ptv`          | `WaSendMediaMessage`       | **`media`** (mimetype optional with a `detectMimetype` processor)                | [Media](/en/guides/media#video)                            |
| `audio`        | `WaSendMediaMessage`       | **`media`** (mimetype optional with a `detectMimetype` processor)                | [Media](/en/guides/media#audio--voice-notes)               |
| `document`     | `WaSendMediaMessage`       | **`media`** (mimetype optional with a `detectMimetype` processor)                | [Media](/en/guides/media#documents)                        |
| `sticker`      | `WaSendMediaMessage`       | **`media`** (mimetype defaults to `image/webp`)                                  | [Media](/en/guides/media#stickers)                         |
| `sticker-pack` | `WaSendStickerPackMessage` | **`stickerPackId`**, **`name`**, **`publisher`**, **`stickers`**, **`trayIcon`** | [Media](/en/guides/media#stickers)                         |

Media builders also accept any non-managed field of the underlying protobuf message (e.g. `caption`, `gifPlayback`, `ptt`, `fileName`) via the `UserMediaFields` mapping. Protocol-managed fields (`url`, `mediaKey`, `fileSha256`, `directPath`, …) are filled by the builder.

`mimetype` is optional. The resolution order is: an explicit `mimetype` you pass wins; otherwise the builder calls `media.processor.detectMimetype` (provided by `@zapo-js/media-utils` when `file-type` is installed); otherwise it throws for `image`/`video`/`audio`/`document`/`ptv`. Stickers default to `image/webp`.

### Interactive

| `type`            | Type                         | Required fields                          | Guide                                                                    |
| ----------------- | ---------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ |
| `reaction`        | `WaSendReactionMessage`      | **`emoji`**, **`target`**                | [Reactions](/en/guides/interactive-messages#reactions)                   |
| `poll`            | `WaSendPollMessage`          | **`name`**, **`options`**                | [Polls](/en/guides/interactive-messages#polls)                           |
| `poll-vote`       | `WaSendPollVoteMessage`      | **`poll`**, **`selectedOptionNames`**    | [Voting](/en/guides/interactive-messages#voting-on-a-poll)               |
| `event`           | `WaSendEventMessage`         | **`name`**, **`startTime`**              | [Events](/en/guides/interactive-messages#events)                         |
| `event-response`  | `WaSendEventResponseMessage` | **`event`**, **`response`**              | [Event response](/en/guides/interactive-messages#responding-to-an-event) |
| `pin` / `unpin`   | `WaSendPinMessage`           | **`target`** (+ optional `durationSecs`) | [Pinning](/en/guides/interactive-messages#pinning)                       |
| `keep` / `unkeep` | `WaSendKeepMessage`          | **`target`**                             | [Keep-in-chat](/en/guides/interactive-messages#keep-in-chat)             |
| `revoke`          | `WaSendRevokeMessage`        | **`target`**                             | [Revoking](/en/guides/interactive-messages#revoking-delete-for-everyone) |

`target` is a `WaMessageTargetInput` — a [`WaMessageKey`](/en/guides/interactive-messages#targeting-a-message) (`{ remoteJid, id, fromMe, participant? }`) **or** a received `message` event passed verbatim (its `key` is used). `poll`/`event` parents additionally require `authorJid` and the 32-byte `messageSecret`.

For `revoke`, sender-vs-admin is auto-detected from `target.fromMe` (`false` triggers an admin revoke). There is no `subtype` option.

## Raw `Proto.IMessage`

For anything without a typed builder, pass a raw protobuf message. The library inspects the populated field and **automatically resolves** the stanza attributes — message `type` (\[`resolveMessageTypeAttr`]), media `type`, `polltype`, `event_type`, `view_once`, and `edit` — so you only set the content field.

```ts theme={null}
import { proto } from 'zapo-js'

await client.message.send(jid, {
  conversation: 'A raw text message'
})
```

### Text

| Field                 | Notes                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------ |
| `conversation`        | Plain text.                                                                                            |
| `extendedTextMessage` | Text with context (links, mentions, replies). A non-empty `matchedText` makes it a link/media message. |

### Media

| Field                                            | Resolved media type                   |
| ------------------------------------------------ | ------------------------------------- |
| `imageMessage`                                   | `image`                               |
| `videoMessage`                                   | `video` (or `gif` when `gifPlayback`) |
| `ptvMessage`                                     | `ptv`                                 |
| `audioMessage`                                   | `audio` (or `ptt` when `ptt`)         |
| `documentMessage` / `documentWithCaptionMessage` | `document`                            |
| `stickerMessage`                                 | `sticker`                             |
| `stickerPackMessage`                             | `sticker-pack`                        |

<Note>
  Raw media fields require pre-uploaded media (the encryption keys, `directPath`, and digests must already be set). To upload from bytes/a file, use the typed media builders instead — they perform the upload for you.
</Note>

### Location & contacts

```ts theme={null}
// Static location
await client.message.send(jid, {
  locationMessage: { degreesLatitude: -23.55, degreesLongitude: -46.63, name: 'HQ' }
})

// Live location (resolved as `live-location`)
await client.message.send(jid, {
  liveLocationMessage: { degreesLatitude: -23.55, degreesLongitude: -46.63 }
})

// Single contact (vCard)
await client.message.send(jid, {
  contactMessage: { displayName: 'Maria', vcard: 'BEGIN:VCARD\n...\nEND:VCARD' }
})

// Multiple contacts
await client.message.send(jid, {
  contactsArrayMessage: { displayName: 'Team', contacts: [/* IContactMessage[] */] }
})
```

| Field                  | Resolved media type                           |
| ---------------------- | --------------------------------------------- |
| `locationMessage`      | `location` (or `live-location` when `isLive`) |
| `liveLocationMessage`  | `live-location`                               |
| `contactMessage`       | `vcard`                                       |
| `contactsArrayMessage` | `contact_array`                               |

### Interactive & business

| Field                              | Resolved type          |
| ---------------------------------- | ---------------------- |
| `buttonsMessage`                   | `button`               |
| `buttonsResponseMessage`           | `button_response`      |
| `listMessage`                      | `list`                 |
| `listResponseMessage`              | `list_response`        |
| `interactiveMessage` (native flow) | interactive            |
| `interactiveResponseMessage`       | `native_flow_response` |
| `templateButtonReplyMessage`       | text                   |
| `orderMessage`                     | `order`                |
| `productMessage`                   | `product`              |
| `groupInviteMessage`               | `url`                  |

```ts theme={null}
await client.message.send(jid, {
  groupInviteMessage: {
    groupJid, inviteCode, inviteExpiration, groupName
  }
})
```

### Polls & events (raw)

| Field                                         | Resolved                         |
| --------------------------------------------- | -------------------------------- |
| `pollCreationMessage` / `…V2` / `…V3` / `…V5` | `poll` (`polltype: creation`)    |
| `pollUpdateMessage`                           | `poll` (`polltype: vote`)        |
| `pollResultSnapshotMessage`                   | text                             |
| `eventMessage`                                | `event` (`event_type: creation`) |
| `encEventResponseMessage`                     | `event` (`event_type: response`) |

Poll creation and event messages auto-persist their `messageSecret` so later votes/responses can be encrypted.

### Protocol, edits & system

| Field                                           | Notes                                                                                                      |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `protocolMessage`                               | Revokes (`REVOKE`), edits (`MESSAGE_EDIT`), ephemeral sync, welcome requests.                              |
| `editedMessage`                                 | Edited message wrapper (`edit` attr).                                                                      |
| `reactionMessage` / `encReactionMessage`        | Reaction (`type: reaction`); empty `text` revokes.                                                         |
| `pinInChatMessage`                              | Pin/unpin (`edit: pin_in_chat`).                                                                           |
| `keepInChatMessage`                             | Keep-in-chat.                                                                                              |
| `encCommentMessage`                             | Comment on a message.                                                                                      |
| `requestPhoneNumberMessage`                     | Request a phone number.                                                                                    |
| `newsletterAdminInviteMessage`                  | Newsletter admin invite.                                                                                   |
| `secretEncryptedMessage`                        | Carries `secretEncType`: `EVENT_EDIT`, `POLL_EDIT`, `POLL_ADD_OPTION`, `MESSAGE_EDIT`, `MESSAGE_SCHEDULE`. |
| `messageHistoryNotice` / `messageHistoryBundle` | Group history sharing.                                                                                     |

### Wrappers

These wrap an inner message; the library unwraps them when resolving attributes:

`ephemeralMessage`, `viewOnceMessage`, `viewOnceMessageV2`, `deviceSentMessage`, `groupMentionedMessage`, `botInvokeMessage`, `documentWithCaptionMessage`.

For view-once specifically, prefer the [`viewOnce` send option](/en/guides/media#view-once) over hand-wrapping.

<Tip>
  The full protobuf surface is available under the exported `proto` namespace — `proto.Message`, `proto.Message.ProtocolMessage.Type`, etc. Use it to build any field above and to reference enum values.
</Tip>

## Raw proto cookbook

Concrete `client.message.send(jid, …)` payloads for the common raw kinds. Import `proto` for enum values:

```ts theme={null}
import { proto } from 'zapo-js'
```

### Plain text

```ts theme={null}
await client.message.send(jid, { conversation: 'Hello' })
```

### Text with mentions + reply

```ts theme={null}
await client.message.send(jid, {
  extendedTextMessage: {
    text: 'Hey @5511999999999 👆',
    contextInfo: {
      mentionedJid: ['5511999999999@s.whatsapp.net'],
      // quote/reply:
      stanzaId: originalStanzaId,
      participant: originalSenderJid,
      quotedMessage: { conversation: 'the quoted text' }
    }
  }
})
```

### Text with a manual link preview

```ts theme={null}
await client.message.send(jid, {
  extendedTextMessage: {
    text: 'https://example.com',
    matchedText: 'https://example.com',
    title: 'Example',
    description: 'Example domain'
  }
})
```

### Static location

```ts theme={null}
await client.message.send(jid, {
  locationMessage: {
    degreesLatitude: -23.5505,
    degreesLongitude: -46.6333,
    name: 'São Paulo',
    address: 'SP, Brazil'
  }
})
```

### Live location

```ts theme={null}
await client.message.send(jid, {
  liveLocationMessage: {
    degreesLatitude: -23.5505,
    degreesLongitude: -46.6333,
    accuracyInMeters: 50,
    speedInMps: 0,
    caption: 'On my way',
    sequenceNumber: 1
  }
})
```

### Contact card (vCard)

```ts theme={null}
await client.message.send(jid, {
  contactMessage: {
    displayName: 'Maria Silva',
    vcard: [
      'BEGIN:VCARD',
      'VERSION:3.0',
      'FN:Maria Silva',
      'TEL;type=CELL;waid=5511999999999:+55 11 99999-9999',
      'END:VCARD'
    ].join('\n')
  }
})
```

### Multiple contacts

```ts theme={null}
await client.message.send(jid, {
  contactsArrayMessage: {
    displayName: 'My contacts',
    contacts: [
      { displayName: 'Maria', vcard: 'BEGIN:VCARD…END:VCARD' },
      { displayName: 'João', vcard: 'BEGIN:VCARD…END:VCARD' }
    ]
  }
})
```

### Group invite

```ts theme={null}
await client.message.send(jid, {
  groupInviteMessage: {
    groupJid: '123456789-987654@g.us',
    inviteCode: 'AbCdEf123',
    inviteExpiration: Math.floor(Date.now() / 1000) + 86_400,
    groupName: 'My group',
    caption: 'Join us!'
  }
})
```

### Buttons (raw)

Up to three quick-reply buttons. The header is a `oneof` — pick text, image, video, location, or document (pre-uploaded for media):

```ts theme={null}
await client.message.send(jid, {
  buttonsMessage: {
    contentText: 'Order placed — what next?',
    footerText: 'Reply within 24h',
    headerType: proto.Message.ButtonsMessage.HeaderType.TEXT,
    text: 'Order #1234',
    buttons: [
      {
        buttonId: 'track',
        buttonText: { displayText: 'Track' },
        type: proto.Message.ButtonsMessage.Button.Type.RESPONSE
      },
      {
        buttonId: 'cancel',
        buttonText: { displayText: 'Cancel' },
        type: proto.Message.ButtonsMessage.Button.Type.RESPONSE
      }
    ]
  }
})
```

### List menu (raw)

A single-select list of rows grouped into sections:

```ts theme={null}
await client.message.send(jid, {
  listMessage: {
    title: 'Menu',
    description: 'Choose an item',
    buttonText: 'View menu',
    footerText: 'Open 9–18',
    listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
    sections: [
      {
        title: 'Pizzas',
        rows: [
          { rowId: 'pizza-margherita', title: 'Margherita', description: 'Tomato, mozzarella, basil' },
          { rowId: 'pizza-pepperoni',  title: 'Pepperoni',  description: 'Tomato, cheese, pepperoni' }
        ]
      },
      {
        title: 'Drinks',
        rows: [{ rowId: 'drink-cola', title: 'Cola' }]
      }
    ]
  }
})
```

### Interactive native flow (raw)

The modern interactive surface — buttons whose params are JSON-encoded ad-hoc payloads:

```ts theme={null}
await client.message.send(jid, {
  interactiveMessage: {
    body: { text: 'Tap below to open the form' },
    footer: { text: 'Powered by your bot' },
    nativeFlowMessage: {
      buttons: [
        {
          name: 'cta_url',
          buttonParamsJson: JSON.stringify({
            display_text: 'Open form',
            url: 'https://example.com/form'
          })
        }
      ],
      messageVersion: 1
    }
  }
})
```

### Product (raw)

Send a catalog product. The inner `productImage` must already be uploaded:

```ts theme={null}
await client.message.send(jid, {
  productMessage: {
    businessOwnerJid: '5511999999999@s.whatsapp.net',
    body: 'Take a look at this',
    footer: 'In stock',
    product: {
      productId: '12345',
      title: 'Hat',
      description: 'One size, adjustable',
      currencyCode: 'BRL',
      priceAmount1000: 49_900, // 49.90 BRL — price × 1000
      retailerId: 'sku-001',
      url: 'https://example.com/p/12345',
      productImage: { /* pre-uploaded image fields */ }
    }
  }
})
```

### Order (raw)

Order confirmation / inquiry:

```ts theme={null}
await client.message.send(jid, {
  orderMessage: {
    orderId: 'ord-abc',
    orderTitle: 'Sample order',
    itemCount: 3,
    status: proto.Message.OrderMessage.OrderStatus.INQUIRY,   // or ACCEPTED / DECLINED
    surface: proto.Message.OrderMessage.OrderSurface.CATALOG,
    sellerJid: '5511888888888@s.whatsapp.net',
    totalAmount1000: 149_700, // 149.70 BRL — total × 1000
    totalCurrencyCode: 'BRL',
    message: 'Order details'
  }
})
```

### Newsletter admin invite (raw)

Invite a contact to co-admin one of your newsletters:

```ts theme={null}
await client.message.send(contactJid, {
  newsletterAdminInviteMessage: {
    newsletterJid: '120363xxxxxxxxxxxxxx@newsletter',
    newsletterName: 'My Newsletter',
    caption: 'Become a co-admin',
    inviteExpiration: Math.floor(Date.now() / 1000) + 7 * 86_400
  }
})
```

### Reaction (raw)

```ts theme={null}
await client.message.send(jid, {
  reactionMessage: {
    key: { remoteJid: jid, fromMe: false, id: targetStanzaId, participant: senderJid },
    text: '🔥', // empty string removes the reaction
    senderTimestampMs: Date.now()
  }
})
```

### Pin / unpin (raw)

```ts theme={null}
await client.message.send(jid, {
  pinInChatMessage: {
    key: { remoteJid: jid, fromMe: false, id: targetStanzaId },
    type: proto.Message.PinInChatMessage.Type.PIN_FOR_ALL, // or UNPIN_FOR_ALL
    senderTimestampMs: Date.now()
  }
})
```

### Keep / un-keep in chat (raw)

```ts theme={null}
await client.message.send(jid, {
  keepInChatMessage: {
    key: { remoteJid: jid, fromMe: false, id: targetStanzaId },
    keepType: proto.KeepType.KEEP_FOR_ALL, // or UNDO_KEEP_FOR_ALL
    timestampMs: Date.now()
  }
})
```

### Revoke / delete-for-everyone (protocolMessage)

```ts theme={null}
await client.message.send(jid, {
  protocolMessage: {
    key: { remoteJid: jid, fromMe: true, id: targetStanzaId },
    type: proto.Message.ProtocolMessage.Type.REVOKE
  }
})
```

### Toggle disappearing messages (ephemeral setting)

```ts theme={null}
await client.message.send(jid, {
  protocolMessage: {
    type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
    ephemeralExpiration: 7 * 24 * 3600 // seconds; 0 disables
  }
})
```

### Poll (raw)

```ts theme={null}
await client.message.send(jid, {
  pollCreationMessage: {
    name: 'Lunch?',
    options: [{ optionName: 'Pizza' }, { optionName: 'Sushi' }],
    selectableOptionsCount: 1
  }
})
```

### Request a phone number

```ts theme={null}
await client.message.send(jid, { requestPhoneNumberMessage: {} })
```

### Disappearing wrapper (ephemeralMessage)

Wrap any message so it inherits the chat's ephemeral timer:

```ts theme={null}
await client.message.send(jid, {
  ephemeralMessage: { message: { conversation: 'This disappears' } }
})
```

### View-once wrapper

```ts theme={null}
await client.message.send(jid, {
  viewOnceMessageV2: { message: { imageMessage: { /* pre-uploaded image fields */ } } }
})
```

<Note>
  Snippets that quote a message use a `key` of `{ remoteJid, fromMe, id, participant? }` (a `proto.IMessageKey`). For the typed equivalents (reaction, revoke, pin, keep, poll) prefer the [builders](#interactive) — they manage the message secret and key wiring for you.
</Note>
