| title | USync |
|---|---|
| description | Typed USync query engine for user sync, device lists, profiles, and bot metadata |
USync ("user sync") is the WhatsApp protocol used to batch-query per-user data: registration status, device lists, profile picture/status, business verification, bot profiles, and more. Higher-level helpers like is_on_whatsapp, get_user_info, and get_user_devices already build on USync internally.
Client::query_usync exposes the same typed query engine directly, for protocol combinations the specialized helpers don't cover — for example fetching a bot's profile, resolving a username, or reading disappearing_mode/text_status in the same request as a device-list lookup.
query_usync is a direct method on Client (not behind a sub-accessor):
let response = client.query_usync(query).await?;pub fn new(
mode: UsyncMode,
context: UsyncContext,
protocols: Vec<UsyncProtocol>,
users: Vec<UsyncUser>,
) -> Result<Self, UsyncValidationError>UsyncQuery::new validates the whole query before it reaches the network. It requires at least one protocol and one user, and rejects duplicate protocol kinds. It also enforces per-user field consistency — for example, a tc_token requires the Status protocol to be selected, and device_sync requires DevicesV2. Deserializing a UsyncQuery from an external source runs this same validation, so a serialized input can't bypass it.
UsyncMode:
Query(default) — contact lookupsFull— user info with more detailDelta— incremental contact synchronization
UsyncContext:
Interactive(default) — user-initiated operationsBackground— background syncMessage— message-related operationsVoip— call setup refreshing device lists
Construct a query target from a JID, phone number, or username, then attach protocol-specific inputs with builder methods:
pub fn from_jid(jid: Jid) -> Self
pub fn from_phone(phone: impl Into<CompactString>) -> Self
pub fn from_username(username: impl Into<CompactString>) -> Self
pub fn from_pn_jid(pn_jid: Jid) -> Self
pub fn with_id(mut self, jid: Jid) -> Self
pub fn with_pn_jid(mut self, jid: Jid) -> Self
pub fn with_phone(mut self, phone: impl Into<CompactString>) -> Self
pub fn with_known_lid(mut self, lid: Jid) -> Self
pub fn with_device_sync(mut self, hint: UsyncDeviceSyncHint) -> Self
pub fn with_persona_id(mut self, persona_id: impl Into<CompactString>) -> Self
pub fn with_username(mut self, username: impl Into<CompactString>) -> Self
pub fn with_username_pin(mut self, pin: impl Into<CompactString>) -> Self
pub fn with_contact_type(mut self, contact_type: impl Into<CompactString>) -> Self
pub fn with_tc_token(mut self, token: impl Into<Vec<u8>>) -> SelfUsyncDeviceSyncHint carries cache hints for the DevicesV2 subprotocol so the server can skip returning an unchanged device list:
pub const fn new() -> Self
pub fn with_device_hash(mut self, device_hash: impl Into<CompactString>) -> Self
pub const fn with_timestamp(mut self, timestamp: i64) -> Self
pub const fn with_expected_timestamp(mut self, expected_timestamp: i64) -> Selfpub enum UsyncProtocol {
Contact { addressing_mode: UsyncAddressingMode },
DevicesV2,
Status,
TextStatus,
DisappearingMode,
BusinessVerifiedName,
Picture,
Lid,
Username,
BotProfileV1,
Features(Vec<UsyncFeature>),
}UsyncAddressingMode is Pn (default) or Lid. UsyncFeature lists the feature flags the Features subprotocol can query (Document, Encrypt, EncryptBlocklist, EncryptContact, EncryptGroupGen2, EncryptImage, EncryptLocation, EncryptUrl, EncryptV2, Voip, MultiAgent).
pub struct UsyncResponse {
pub protocol_states: Vec<UsyncProtocolState>,
pub users: Vec<UsyncUserResult>,
}
impl UsyncResponse {
pub fn protocol_state(&self, protocol: UsyncProtocolKind) -> Option<&UsyncProtocolState>
}
pub struct UsyncUserResult {
pub id: Option<Jid>, // absent for contact-only results without a JID
pub pn_jid: Option<Jid>,
pub protocols: Vec<UsyncProtocolResult>,
}
impl UsyncUserResult {
pub fn protocol(&self, kind: UsyncProtocolKind) -> Option<&UsyncProtocolResult>
}Each per-user protocol result is wrapped in UsyncOutcome<T>, which is either the decoded value or the server's per-subprotocol error — matching the same "errors don't fail the whole batch" behavior as is_on_whatsapp/get_user_info:
pub enum UsyncOutcome<T> {
Value(T),
Error(Box<UsyncSubprotocolError>),
}
impl<T> UsyncOutcome<T> {
pub fn value(&self) -> Option<&T>
pub fn error(&self) -> Option<&UsyncSubprotocolError>
}UsyncProtocolResult carries the typed payload per protocol:
pub enum UsyncProtocolResult {
Contact(UsyncOutcome<UsyncContactResult>),
Devices(UsyncOutcome<UsyncDevicesResult>),
Status(UsyncOutcome<UsyncStatusResult>),
TextStatus(UsyncOutcome<UsyncTextStatusResult>),
DisappearingMode(UsyncOutcome<UsyncDisappearingModeResult>),
Business(UsyncOutcome<Box<UsyncBusinessResult>>),
Picture(UsyncOutcome<u64>),
Lid(UsyncOutcome<Option<Jid>>),
Username(UsyncOutcome<Option<CompactString>>),
Bot(UsyncOutcome<Box<UsyncBotProfileResult>>),
Features(UsyncOutcome<Vec<UsyncFeatureResult>>),
}Payload structs, all #[non_exhaustive]:
UsyncContactResult—contact_type: CompactString,username: Option<CompactString>,content: Option<CompactString>UsyncDevicesResult—device_list: Option<UsyncDeviceListResult>,key_index: Option<UsyncKeyIndexResult>UsyncDeviceListResult—hash: Option<CompactString>,devices: Vec<UsyncDeviceResult>UsyncDeviceResult—id: u16,key_index: Option<u32>,is_hosted: boolUsyncKeyIndexResult—timestamp: i64,signed_key_index_bytes: Option<Vec<u8>>,expected_timestamp: Option<i64>
UsyncStatusResult—status: Option<CompactString>,timestamp: Option<i64>(WhatsApp Web itself only consumesstatus; the wire timestamp is kept for callers that need it)UsyncTextStatusResult—text,emoji,ephemeral_duration_seconds,last_update_time(allOption)UsyncDisappearingModeResult—duration_seconds: u32,setting_timestamp: i64,ephemerality_disabled: boolUsyncBusinessResult—verified_name: Option<VerifiedName>(seeis_on_whatsappforVerifiedNamefields)UsyncFeatureResult—feature: UsyncFeature,value: CompactStringUsyncBotProfileResult—name,attributes,description,category,is_default,prompts: Vec<UsyncBotPrompt>,persona_id,commands: Vec<UsyncBotCommand>,commands_description,is_meta_created: Option<bool>,creator_name: Option<CompactString>,creator_profile_url: Option<CompactString>,posing_as_professional: Option<UsyncBotProfessionalType>UsyncBotPrompt—emoji: CompactString,text: CompactStringUsyncBotCommand—name: CompactString,description: CompactStringUsyncBotProfessionalType—Unknown,Yes,No, or anOther(String)catch-all for unrecognized wire values
use whatsapp_rust::usync::{
UsyncContext, UsyncMode, UsyncProtocol, UsyncProtocolKind, UsyncProtocolResult,
UsyncQuery, UsyncUser,
};
let query = UsyncQuery::new(
UsyncMode::Query,
UsyncContext::Interactive,
vec![UsyncProtocol::BotProfileV1, UsyncProtocol::Username],
vec![UsyncUser::from_jid(jid)],
)?;
let response = client.query_usync(query).await?;
for user in &response.users {
if let Some(bot) = user.protocol(UsyncProtocolKind::Bot)
&& let UsyncProtocolResult::Bot(outcome) = bot
&& let Some(profile) = outcome.value()
{
println!("Bot: {} ({})", profile.name, profile.category);
}
}#[non_exhaustive]
pub enum UsyncValidationError {
EmptyProtocols,
EmptyUsers,
DuplicateProtocol(UsyncProtocolKind),
EmptyFeatureSet,
MissingUserIdentity { index: usize },
InvalidUserJid { index: usize, jid: String },
InvalidPnJid { index: usize },
EmptyPhone { index: usize },
InvalidPhone { index: usize },
EmptyUsername { index: usize },
UsernamePinWithoutUsername { index: usize },
InvalidKnownLid { index: usize },
EmptyDeviceHash { index: usize },
ConflictingContactInputs { index: usize },
ContactInputWithoutProtocol { index: usize },
DeviceSyncWithoutProtocol { index: usize },
TcTokenWithoutProtocol { index: usize },
PersonaIdWithoutProtocol { index: usize },
KnownLidWithoutProtocol { index: usize },
EmptySid,
}Client::query_usync surfaces a validation failure as IqError::EncodeError (the query never reaches the network).
UsyncDeviceResult.is_hosted (and the corresponding is_hosted field on the persisted DeviceInfo/UsyncDevice types — see Store: DeviceListRecord) marks a device as belonging to WhatsApp's hosted PN/LID address space rather than the regular one. Use Jid::with_device_hosting(device_id, is_hosted) to build a correctly-addressed device JID from a device-list entry:
let device_jid = user_jid.with_device_hosting(device.id, device.is_hosted);DeviceInfo(wacore::store::traits::DeviceInfo) andUsyncDevice(wacore::usync::UsyncDevice) both gained anis_hosted: boolfield. This breaks both construction and exhaustive pattern matching. Struct-literal construction (DeviceInfo { device_id, key_index }) no longer compiles — use the new constructors instead:An exhaustive destructuring pattern (DeviceInfo::new(device_id, key_index).with_hosting(is_hosted) UsyncDevice::new(device, key_index).with_hosting(is_hosted)
let DeviceInfo { device_id, key_index } = info;) also no longer compiles — add a..to the pattern (let DeviceInfo { device_id, key_index, .. } = info;) or match onis_hostedas well. PersistedDeviceInfoJSON withoutis_hostedstill deserializes correctly (is_hosteddefaults tofalse) — this only affects Rust construction and pattern-matching call sites, not on-disk data.UsyncMode::DeltaandUsyncContext::Voipare new enum variants (see the warning above).