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:Methods
create
Create a new poll in a chat.Recipient JID. Can be a direct message or group chat.
Poll question or title.
List of poll options. Must have between 2 and 12 entries, with no duplicates.
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).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 for details.create_quiz
Create a quiz poll — a single-select poll with exactly one correct answer. Quizzes are inherently single-select (WhatsApp Web forcesselectableOptionsCount = 1); the chosen option is sent as the poll’s correctAnswer and poll_type is set to QUIZ.
Recipient JID. Can be a direct message or group chat.
Quiz question or title.
List of answer options (2–12 entries, no duplicates).
0-based index into
options of the correct answer. Out-of-range indices return an error.Same shape as
create: a SendResult plus the 32-byte secret needed to decrypt votes. Votes are cast, decrypted, and aggregated exactly like a regular single-select poll.vote
Cast a vote on an existing poll.Chat JID where the poll was sent.
Message ID of the original poll creation message.
JID of the user who created the poll.
The 32-byte secret returned by
create. Required for vote encryption.Names of the selected options. Pass an empty slice to clear your vote.
Result containing the message ID and recipient JID. See SendResult.
decrypt_vote
Decrypt an encrypted poll vote.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?.Encrypted vote payload (from the
PollUpdateMessage).12-byte initialization vector from the encrypted vote.
The 32-byte secret from poll creation.
Message ID of the original poll.
JID of the poll creator (AD suffix is stripped automatically).
JID of the voter (AD suffix is stripped automatically).
List of 32-byte SHA-256 hashes of the selected option names.
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.Also async +
&self since v0.6 for the same LID↔PN reasons. Migrate Polls::aggregate_votes(...) → client.polls().aggregate_votes(...).await?.The original poll option names (in order).
Slice of
(voter_jid, enc_payload, enc_iv) tuples, ordered oldest-first.The 32-byte secret from poll creation.
Message ID of the original poll.
JID of the poll creator.
One entry per poll option, each containing the option name and a list of voter JID strings.
Votes that fail to decrypt are logged as warnings and skipped. An empty selection from a voter clears their previous vote.
Types
PollOptionResult
Aggregated result for a single poll option.| Field | Type | Description |
|---|---|---|
name | String | The option name |
voters | Vec<String> | JID strings of voters who selected this option |
Low-level utilities
Thewacore::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 singlesecret_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.
The client now decrypts these inline on receive. Since v0.6 the receive path automatically resolves the parent secret from the
MsgSecretStore, decrypts the envelope, and dispatches the result as a normal Event::Message 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.| 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() -> Option<Jid> | Returns the JID the parent message was addressed to — pass it as the fallback to recover edits whose sender migrated namespace between the original message and the edit. |
SecretEncrypted::original_sender_for_dispatch(is_from_me, envelope_sender, my_jid) -> Result<Jid> | 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. |
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. |
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.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.Error handling
All five poll methods (create, create_quiz, vote, decrypt_vote, and aggregate_votes) return Result<T, PollError>:
See also
- Polls guide - Step-by-step usage guide
- Sending messages - Send other message types
- Send API - Low-level send operations