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

# Polls

> Poll creation, voting, and vote decryption API reference

The `Polls` struct provides methods for creating polls, casting votes, and decrypting poll results. Votes are end-to-end encrypted using AES-256-GCM with HKDF-SHA256 key derivation.

## Access

Access poll operations through the client:

```rust theme={null}
let polls = client.polls();
```

## Methods

### create

Create a new poll in a chat.

```rust theme={null}
pub async fn create(
    &self,
    to: &Jid,
    name: &str,
    options: &[String],
    selectable_count: u32,
) -> Result<(SendResult, Vec<u8>), PollError>
```

<ParamField path="to" type="&Jid" required>
  Recipient JID. Can be a direct message or group chat.
</ParamField>

<ParamField path="name" type="&str" required>
  Poll question or title.
</ParamField>

<ParamField path="options" type="&[String]" required>
  List of poll options. Must have between 2 and 12 entries, with no duplicates.
</ParamField>

<ParamField path="selectable_count" type="u32" required>
  Maximum number of options a voter can select. Must be between 1 and the number of options. When set to 1, creates a single-select poll (uses `poll_creation_message_v3`). When greater than 1, creates a multi-select poll (uses `poll_creation_message`).
</ParamField>

<ResponseField name="(send_result, message_secret)" type="(SendResult, Vec<u8>)">
  A tuple containing a `SendResult` (with `message_id`, `to`, and `message_key()`) and a 32-byte random secret. The `message_secret` is required to decrypt votes — store it securely. See [SendResult](/api/send#sendresult) for details.
</ResponseField>

**Example:**

```rust theme={null}
let chat_jid: Jid = "15551234567@s.whatsapp.net".parse()?;

let options = vec![
    "Yes".to_string(),
    "No".to_string(),
    "Maybe".to_string(),
];

let (result, secret) = client
    .polls()
    .create(&chat_jid, "Do you agree?", &options, 1)
    .await?;

println!("Poll sent with ID: {}", result.message_id);
```

### create\_quiz

Create a quiz poll — a single-select poll with exactly one correct answer. Quizzes are inherently single-select (WhatsApp Web forces `selectableOptionsCount = 1`); the chosen option is sent as the poll's `correctAnswer` and `poll_type` is set to `QUIZ`.

```rust theme={null}
pub async fn create_quiz(
    &self,
    to: &Jid,
    name: &str,
    options: &[String],
    correct_index: usize,
) -> Result<(SendResult, Vec<u8>), PollError>
```

<ParamField path="to" type="&Jid" required>
  Recipient JID. Can be a direct message or group chat.
</ParamField>

<ParamField path="name" type="&str" required>
  Quiz question or title.
</ParamField>

<ParamField path="options" type="&[String]" required>
  List of answer options (2–12 entries, no duplicates).
</ParamField>

<ParamField path="correct_index" type="usize" required>
  0-based index into `options` of the correct answer. Out-of-range indices return an error.
</ParamField>

<ResponseField name="(send_result, message_secret)" type="(SendResult, Vec<u8>)">
  Same shape as [`create`](#create): a [`SendResult`](/api/send#sendresult) plus the 32-byte secret needed to decrypt votes. Votes are cast, decrypted, and aggregated exactly like a regular single-select poll.
</ResponseField>

**Example:**

```rust theme={null}
let chat_jid: Jid = "15551234567@s.whatsapp.net".parse()?;

let options = vec![
    "Paris".to_string(),
    "London".to_string(),
    "Berlin".to_string(),
];

// The correct answer is "Paris" (index 0).
let (result, secret) = client
    .polls()
    .create_quiz(&chat_jid, "Capital of France?", &options, 0)
    .await?;
```

### vote

Cast a vote on an existing poll.

```rust theme={null}
pub async fn vote(
    &self,
    chat_jid: &Jid,
    poll_msg_id: &str,
    poll_creator_jid: &Jid,
    message_secret: &[u8],
    option_names: &[String],
) -> Result<SendResult, PollError>
```

<ParamField path="chat_jid" type="&Jid" required>
  Chat JID where the poll was sent.
</ParamField>

<ParamField path="poll_msg_id" type="&str" required>
  Message ID of the original poll creation message.
</ParamField>

<ParamField path="poll_creator_jid" type="&Jid" required>
  JID of the user who created the poll.
</ParamField>

<ParamField path="message_secret" type="&[u8]" required>
  The 32-byte secret returned by `create`. Required for vote encryption.
</ParamField>

<ParamField path="option_names" type="&[String]" required>
  Names of the selected options. Pass an empty slice to clear your vote.
</ParamField>

<ResponseField name="SendResult" type="SendResult">
  Result containing the message ID and recipient JID. See [SendResult](/api/send#sendresult).
</ResponseField>

**Example:**

```rust theme={null}
let selected = vec!["Yes".to_string()];

let vote_result = client
    .polls()
    .vote(
        &chat_jid,
        &poll_msg_id,
        &poll_creator_jid,
        &message_secret,
        &selected,
    )
    .await?;

println!("Vote sent with ID: {}", vote_result.message_id);
```

### decrypt\_vote

Decrypt an encrypted poll vote.

```rust theme={null}
pub async fn decrypt_vote(
    &self,
    enc_payload: &[u8],
    enc_iv: &[u8],
    message_secret: &[u8],
    poll_msg_id: &str,
    poll_creator_jid: &Jid,
    voter_jid: &Jid,
) -> Result<Vec<Vec<u8>>, PollError>
```

<Note>
  Since v0.6 this is an `async` instance method (`&self`) rather than a static helper. The client uses its LID↔PN cache to resolve the voter against the poll creator's namespace and falls back to the opposite namespace if the first attempt fails — both encrypt and decrypt now address the vote with the same JID family as the poll itself, so votes authored across LID/PN migrations still decrypt. Update old call sites: `Polls::decrypt_vote(...)` → `client.polls().decrypt_vote(...).await?`.
</Note>

<ParamField path="enc_payload" type="&[u8]" required>
  Encrypted vote payload (from the `PollUpdateMessage`).
</ParamField>

<ParamField path="enc_iv" type="&[u8]" required>
  12-byte initialization vector from the encrypted vote.
</ParamField>

<ParamField path="message_secret" type="&[u8]" required>
  The 32-byte secret from poll creation.
</ParamField>

<ParamField path="poll_msg_id" type="&str" required>
  Message ID of the original poll.
</ParamField>

<ParamField path="poll_creator_jid" type="&Jid" required>
  JID of the poll creator (AD suffix is stripped automatically).
</ParamField>

<ParamField path="voter_jid" type="&Jid" required>
  JID of the voter (AD suffix is stripped automatically).
</ParamField>

<ResponseField name="selected_hashes" type="Vec<Vec<u8>>">
  List of 32-byte SHA-256 hashes of the selected option names.
</ResponseField>

**Example:**

```rust theme={null}
use whatsapp_rust::Polls;

let selected_hashes = client
    .polls()
    .decrypt_vote(
        &enc_payload,
        &enc_iv,
        &message_secret,
        "3EB0ABC123",
        &poll_creator_jid,
        &voter_jid,
    )
    .await?;
```

### aggregate\_votes

Decrypt multiple votes and tally results per option. Later votes from the same voter replace earlier ones (last-vote-wins). Voters are deduped by their canonical (LID-preferred) identity, so a voter who re-votes under the opposite namespace counts once instead of producing a duplicate row.

```rust theme={null}
pub async fn aggregate_votes(
    &self,
    poll_options: &[String],
    votes: &[(&Jid, &[u8], &[u8])],
    message_secret: &[u8],
    poll_msg_id: &str,
    poll_creator_jid: &Jid,
) -> Result<Vec<PollOptionResult>, PollError>
```

<Note>
  Also async + `&self` since v0.6 for the same LID↔PN reasons. Migrate `Polls::aggregate_votes(...)` → `client.polls().aggregate_votes(...).await?`.
</Note>

<ParamField path="poll_options" type="&[String]" required>
  The original poll option names (in order).
</ParamField>

<ParamField path="votes" type="&[(&Jid, &[u8], &[u8])]" required>
  Slice of `(voter_jid, enc_payload, enc_iv)` tuples, ordered oldest-first.
</ParamField>

<ParamField path="message_secret" type="&[u8]" required>
  The 32-byte secret from poll creation.
</ParamField>

<ParamField path="poll_msg_id" type="&str" required>
  Message ID of the original poll.
</ParamField>

<ParamField path="poll_creator_jid" type="&Jid" required>
  JID of the poll creator.
</ParamField>

<ResponseField name="results" type="Vec<PollOptionResult>">
  One entry per poll option, each containing the option name and a list of voter JID strings.
</ResponseField>

**Example:**

```rust theme={null}
use whatsapp_rust::{Polls, PollOptionResult};

let options = vec!["Yes".to_string(), "No".to_string()];

let results = client
    .polls()
    .aggregate_votes(
        &options,
        &votes,
        &message_secret,
        &poll_msg_id,
        &poll_creator_jid,
    )
    .await?;

for result in &results {
    println!("{}: {} votes", result.name, result.voters.len());
}
```

<Note>
  Votes that fail to decrypt are logged as warnings and skipped. An empty selection from a voter clears their previous vote.
</Note>

## Types

### PollOptionResult

Aggregated result for a single poll option.

```rust theme={null}
pub struct PollOptionResult {
    pub name: String,
    pub voters: Vec<String>,
}
```

| Field    | Type          | Description                                    |
| -------- | ------------- | ---------------------------------------------- |
| `name`   | `String`      | The option name                                |
| `voters` | `Vec<String>` | JID strings of voters who selected this option |

<Note>
  `Polls`, `PollOptionResult`, and `PollVoteCiphertext` are re-exported from the crate root. `whatsapp_rust::Polls` and `whatsapp_rust::PollOptionResult` (shown in the examples above) are now the preferred paths; the legacy `whatsapp_rust::features::` import path continues to work as well.
</Note>

## Low-level utilities

The `wacore::poll` module exposes the cryptographic primitives used internally:

| Function                                                                                                                  | Description                                                                                                                           |
| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `compute_option_hash(name: &str) -> [u8; 32]`                                                                             | SHA-256 hash of an option name                                                                                                        |
| `derive_vote_encryption_key(secret, stanza_id, creator, voter) -> [u8; 32]`                                               | HKDF-SHA256 key derivation                                                                                                            |
| `encrypt_poll_vote(hashes, key, stanza_id, voter) -> (Vec<u8>, [u8; 12])`                                                 | AES-256-GCM encryption                                                                                                                |
| `decrypt_poll_vote(payload, iv, key, stanza_id, voter) -> Vec<Vec<u8>>`                                                   | AES-256-GCM decryption                                                                                                                |
| `decrypt_poll_vote_with_secret(payload, iv, secret, stanza_id, creator, voter) -> Vec<Vec<u8>>`                           | Single-namespace convenience: derives the vote key from the poll secret and decrypts                                                  |
| `decrypt_poll_vote_with_fallback(payload, iv, secret, stanza_id, creator, voter, alt_creator, alt_voter) -> Vec<Vec<u8>>` | Tries the canonical (creator, voter) namespace first and falls back to `(alt_creator, alt_voter)` so LID/PN-mixed votes still decrypt |

## Decrypting secret-encrypted envelopes

WhatsApp wraps message edits, poll edits, poll add-option, and event edits in a single `secret_encrypted_message` envelope keyed by a per-use-case secret derived from the parent message's `message_secret`. v0.6 added support for decrypting all four kinds.

<Note>
  **The client now decrypts these inline on receive.** Since v0.6 the receive path automatically resolves the parent secret from the [`MsgSecretStore`](/api/store#msgsecretstore), decrypts the envelope, and dispatches the result as a normal [`Event::Messages`](/concepts/events#messages) carrying the decrypted payload — so most apps never call the helpers below. The client captures `MessageContextInfo.message_secret` from inbound messages and seeds secrets from history-sync, so edits decrypt as long as the parent secret is known. Edits whose secret can't be found are skipped silently (no undecryptable event); the raw envelope stays on the message. The manual helpers remain for custom pipelines or when you store secrets yourself.
</Note>

```rust theme={null}
use whatsapp_rust::features::message_edit::{
    self, SecretEncKind, SecretEncrypted,
};

if let Some(envelope) = message_edit::extract_secret_encrypted(&message)? {
    let SecretEncrypted { kind, target_message_id, sender, .. } = &envelope;
    let parent_secret: [u8; 32] = load_message_secret(target_message_id)?;
    let plaintext: wa::Message = message_edit::decrypt_secret_encrypted_with_fallback(
        &envelope,
        &parent_secret,
        envelope.original_sender_jid(&my_jid).ok().as_ref(),
    )?;

    match kind {
        SecretEncKind::MessageEdit   => handle_message_edit(plaintext),
        SecretEncKind::PollEdit      => handle_poll_edit(plaintext),
        SecretEncKind::PollAddOption => handle_poll_add_option(plaintext),
        SecretEncKind::EventEdit     => handle_event_edit(plaintext),
    }
}
```

| Function                                                                                                              | Description                                                                                                                                                                                                                                                                                                                                                                                      |
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `extract_secret_encrypted(msg) -> Option<SecretEncrypted<'_>>`                                                        | Pulls envelope metadata (`kind`, target id, sender, ciphertext, IV) out of any `secret_encrypted_message` wrapper. Replaces the v0.5 `extract_envelope` helper (which now delegates to this).                                                                                                                                                                                                    |
| `decrypt_secret_encrypted(envelope, parent_secret) -> wa::Message`                                                    | Empty-AAD AES-GCM decrypt against the canonical addressing of the envelope.                                                                                                                                                                                                                                                                                                                      |
| `decrypt_secret_encrypted_with_fallback(envelope, parent_secret, alt_sender) -> wa::Message`                          | Same as above but retries with `alt_sender` (typically the LID↔PN twin) so edits authored across the migration still decrypt.                                                                                                                                                                                                                                                                    |
| `SecretEncrypted::original_sender_jid(&self, my_jid: &Jid) -> Result<Jid, MessageEditError>`                          | Resolves the original sender from the target message key: `participant` if present, `my_jid` if `from_me == Some(true)`, otherwise `remote_jid`. Pass the result (as `Option`) as the fallback to recover edits whose sender migrated namespace between the original message and the edit. As of PR #1090 returns [`MessageEditError`](/api/errors#messageediterror) instead of `anyhow::Error`. |
| `SecretEncrypted::original_sender_for_dispatch(is_from_me, envelope_sender, my_jid) -> Result<Jid, MessageEditError>` | Dispatch-time resolver. For `MessageEdit` it returns the **envelope sender** (an author edits their own message, so the secret target is written in the editor's frame); for poll/event kinds it falls back to the target-key resolution. Use this — not `original_sender_jid` — when decrypting incoming peer edits. Also returns `MessageEditError` as of PR #1090.                            |
| `rewrap_as_legacy_edit(plaintext)`                                                                                    | Re-emits a decrypted `MessageEdit` envelope as the legacy `protocol_message { MESSAGE_EDIT }` shape for consumers that already handled the old form.                                                                                                                                                                                                                                             |

<Note>
  For incoming **peer** message edits (edits authored on a contact's other device, or self-synced from your own linked device), the parent-author resolution must use the envelope sender rather than the target key — otherwise the parent author resolves to the receiver and the GCM tag fails. The client handles this internally via `original_sender_for_dispatch`; replicate it if you decrypt edits yourself. `EncryptedEdit` exposes the same method for the `MessageEdit`-only façade.
</Note>

```rust theme={null}
pub enum SecretEncKind {
    MessageEdit,    // edited text of a previously sent message
    PollEdit,       // poll question / metadata change
    PollAddOption,  // new option appended to a multi-select poll
    EventEdit,      // event title / time change
}
```

<Note>
  The `EncryptedEdit` type from v0.5 still exists as a `MessageEdit`-specific façade for callers that only care about text edits; `extract_envelope` now returns `SecretEncrypted` and the older shape is reachable via the same struct. Both `EncryptedEdit::original_sender_jid` and `SecretEncrypted::original_sender_jid` share the same [`MessageEditError`](/api/errors#messageediterror) error type.
</Note>

## Error handling

All five poll methods (`create`, `create_quiz`, `vote`, `decrypt_vote`, and `aggregate_votes`) return `Result<T, PollError>`:

```rust theme={null}
#[non_exhaustive]
pub enum PollError {
    #[error("{0}")]
    Send(#[from] SendError),
    #[error("invalid poll: {0}")]
    InvalidPoll(String),
    #[error("client is not logged in")]
    NotLoggedIn,
    #[error("poll vote crypto failed: {0}")]
    Crypto(#[source] anyhow::Error),
}
```

```rust theme={null}
use whatsapp_rust::PollError;

match client.polls().create(jid, "Question?", &options, 1).await {
    Ok((result, secret)) => println!("Poll created: {}", result.message_id),
    Err(PollError::InvalidPoll(msg)) => eprintln!("Invalid poll: {}", msg),
    Err(PollError::Send(e)) => eprintln!("Send failed: {}", e),
    Err(e) => eprintln!("Error: {}", e),
}
```

## See also

* [Polls guide](/guides/polls) - Step-by-step usage guide
* [Sending messages](/guides/sending-messages) - Send other message types
* [Send API](/api/send) - Low-level send operations
