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

# Contacts

> Contact lookup, profile picture, and user info operations

The `Contacts` struct provides methods for checking WhatsApp registration status and retrieving profile pictures and user information.

## Access

Access contact operations through the client:

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

## Methods

### is\_on\_whatsapp

Check if JIDs are registered on WhatsApp. Accepts both PN JIDs and LID JIDs.

```rust theme={null}
pub async fn is_on_whatsapp(
    &self,
    jids: &[Jid],
) -> Result<Vec<IsOnWhatsAppResult>, ContactError>
```

**Parameters:**

* `jids` - Array of JIDs to check. Supports `Jid::pn("phone_number")` for phone number lookups and `Jid::lid("lid_value")` for LID lookups. If you pass a non-PN/non-LID JID (groups, newsletters, etc.), the method returns an error immediately.

**Returns:**

* `Vec<IsOnWhatsAppResult>` - Registration status for each JID

**IsOnWhatsAppResult fields:**

* `jid: Jid` - WhatsApp JID for the user
* `is_registered: bool` - Whether the JID is on WhatsApp
* `lid: Option<Jid>` - LID (Linked Identity) if available
* `pn_jid: Option<Jid>` - Phone number JID, present when the server returns LID as the primary JID
* `is_business: bool` - Whether this is a WhatsApp Business account
* `verified_name: Option<VerifiedName>` - Decoded verified business name certificate, when the account is a verified business
* `contact_error: Option<UsyncSubprotocolError>` - Server error for the `contact` subprotocol (e.g. privacy-blocked lookup); `is_registered` will be `false`
* `lid_error: Option<UsyncSubprotocolError>` - Server error for the `lid` subprotocol; `lid` will be `None`
* `business_error: Option<UsyncSubprotocolError>` - Server error for the `business` subprotocol; `is_business` will be `false`

**VerifiedName fields:**

* `name: Option<String>` - Display name shown to other users (decoded from the certificate when the server omits the attribute)
* `serial: Option<String>` - Certificate serial number
* `issuer: Option<String>` - Certificate issuer
* `certificate: Option<Vec<u8>>` - Raw `VerifiedNameCertificate` protobuf bytes, for callers that need to verify the signature themselves

<Note>
  `IsOnWhatsAppResult` is marked `#[non_exhaustive]`, so new fields may be added in future versions without a breaking change.
</Note>

**Example — phone number lookup:**

```rust theme={null}
let results = client.contacts().is_on_whatsapp(&[
    Jid::pn("15551234567"),
    Jid::pn("15559876543"),
]).await?;

for result in results {
    if result.is_registered {
        println!("{} is on WhatsApp", result.jid);
        if let Some(lid) = &result.lid {
            println!("  LID: {}", lid);
        }
        if result.is_business {
            println!("  Business account");
            if let Some(vn) = &result.verified_name
                && let Some(name) = &vn.name
            {
                println!("  Verified name: {}", name);
            }
        }
    } else {
        println!("{} is NOT on WhatsApp", result.jid);
    }
}
```

**Example — LID lookup:**

```rust theme={null}
let results = client.contacts().is_on_whatsapp(&[
    Jid::lid("100000001"),
]).await?;

for result in results {
    if let Some(pn) = &result.pn_jid {
        println!("LID {} maps to phone {}", result.jid, pn);
    }
}
```

PN and LID queries use different wire protocols (matching WhatsApp Web's ExistsJob), so mixed inputs are automatically split into separate requests. LID-PN mappings discovered from results are persisted to the local cache.

### get\_profile\_picture

Get the profile picture URL for a JID.

```rust theme={null}
pub async fn get_profile_picture(
    &self,
    jid: &Jid,
    preview: bool,
) -> Result<Option<ProfilePicture>, ContactError>
```

**Parameters:**

* `jid` - Target JID (user, group, or newsletter)
* `preview` - `true` for preview thumbnail, `false` for full-size image

**Returns:**

* `Option<ProfilePicture>` - Picture info or `None` if not available

**ProfilePicture fields:**

* `id: String` - Picture ID
* `url: String` - Download URL
* `direct_path: Option<String>` - Direct path for media download
* `hash: Option<String>` - SHA-256 hash for integrity and cache validation

**Example:**

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

// Get preview thumbnail
if let Some(preview) = client.contacts().get_profile_picture(&jid, true).await? {
    println!("Preview URL: {}", preview.url);
    println!("Picture ID: {}", preview.id);
}

// Get full-size picture
if let Some(full) = client.contacts().get_profile_picture(&jid, false).await? {
    println!("Full URL: {}", full.url);
    if let Some(path) = full.direct_path {
        println!("Direct path: {}", path);
    }
}

// Handle user with no profile picture
let no_pic_jid: Jid = "15559999999@s.whatsapp.net".parse()?;
if client.contacts().get_profile_picture(&no_pic_jid, true).await?.is_none() {
    println!("User has no profile picture");
}
```

**For groups:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
if let Some(pic) = client.contacts().get_profile_picture(&group_jid, true).await? {
    println!("Group picture: {}", pic.url);
}
```

<Note>
  To override the default request timeout for a single fetch, use `get_profile_picture_with_timeout(jid, preview, timeout)`, which takes an extra `timeout: Option<Duration>` argument. Internally the request is built via `ProfilePictureSpec`'s `with_timeout(...)` builder method; pass `None` to fall back to the default timeout.
</Note>

<Note>
  The system/announcements JID (`0@s.whatsapp.net`, and its legacy form `0@c.us`) never answers this IQ, so it's short-circuited client-side: both `get_profile_picture` and `get_profile_picture_with_timeout` return `Ok(None)` immediately for it instead of waiting out the full request timeout. This mirrors WhatsApp Web, which never sends the request for this JID in the first place.
</Note>

### get\_user\_info

Get user information by JID.

```rust theme={null}
pub async fn get_user_info(
    &self,
    jids: &[Jid],
) -> Result<HashMap<Jid, UserInfo>, ContactError>
```

**Parameters:**

* `jids` - Array of JIDs to query

<Note>
  The system/announcements JID (`0@s.whatsapp.net`, and its legacy form `0@c.us`) is filtered out of the batch before it's sent — it's not usync-eligible and the server never answers for it. If it's the only JID passed in, `get_user_info` returns an empty map without making a request; if it's mixed with other JIDs, the remaining JIDs are still queried normally.
</Note>

**Returns:**

* `HashMap<Jid, UserInfo>` - Map of JID to user info

**UserInfo fields:**

* `jid: Jid` - WhatsApp JID
* `lid: Option<Jid>` - LID if available
* `lid_error: Option<UsyncSubprotocolError>` - Server error for the `lid` subprotocol; `lid` will be `None`
* `status: Option<String>` - Status message
* `status_error: Option<UsyncSubprotocolError>` - Server error for the `status` subprotocol (e.g. privacy-hidden status); `status` will be `None`
* `picture_id: Option<String>` - Profile picture ID
* `picture_error: Option<UsyncSubprotocolError>` - Server error for the `picture` subprotocol; `picture_id` will be `None`
* `is_business: bool` - Whether business account
* `business_error: Option<UsyncSubprotocolError>` - Server error for the `business` subprotocol; `is_business` will be `false`
* `verified_name: Option<VerifiedName>` - Decoded verified business name certificate, for verified business accounts (see [`is_on_whatsapp`](#is_on_whatsapp) for field details)
* `devices: Vec<u16>` - Device IDs from the `<devices version="2">` sublist the same usync query returns (device `0` is the primary). Empty when the server omits the sublist — no extra request is needed.
* `devices_error: Option<UsyncSubprotocolError>` - Server error for the `devices` subprotocol; `devices` will be empty

<Note>
  `UserInfo` is `#[non_exhaustive]`, so new fields may be added in future versions without a breaking change.
</Note>

**UsyncSubprotocolError fields:**

* `code: Option<u16>` - Numeric error code from the server (e.g. `403`, `404`)
* `text: Option<String>` - Human-readable error description
* `backoff: Option<u32>` - Server-suggested retry delay in seconds

The library preserves per-subprotocol errors on the result struct rather than failing the whole request. A privacy error from one user's status does not block retrieval of devices for other users in the same batch. Check the `*_error` fields when a corresponding `Option` field is `None` and you need to distinguish "not set" from "server error".

```rust theme={null}
let user_info = client.contacts().get_user_info(&[jid]).await?;
if let Some(info) = user_info.get(&jid) {
    if info.status.is_none() {
        if let Some(err) = &info.status_error {
            println!("Status hidden: code={:?} text={:?}", err.code, err.text);
        }
    }
}
```

**Example:**

```rust theme={null}
let jids = vec![
    "15551234567@s.whatsapp.net".parse()?,
    "15559876543@s.whatsapp.net".parse()?,
];

let user_info = client.contacts().get_user_info(&jids).await?;

for (jid, info) in user_info {
    println!("User: {}", jid);
    println!("  Business: {}", info.is_business);
    println!("  Devices: {:?}", info.devices); // e.g. [0, 1, 2]

    if let Some(vn) = &info.verified_name
        && let Some(name) = &vn.name
    {
        println!("  Verified name: {}", name);
    }

    if let Some(status) = info.status {
        println!("  Status: {}", status);
    }

    if let Some(pic_id) = info.picture_id {
        println!("  Picture ID: {}", pic_id);
    }
}
```

## Privacy & TC tokens

For user JIDs (not groups/newsletters), the library automatically includes TC tokens when fetching profile pictures. TC tokens are used for privacy-gated operations.

The implementation automatically:

* Looks up TC tokens for user JIDs
* Includes tokens in profile picture requests
* Skips tokens for groups and newsletters

`get_user_info` does the same for status/about: when the `profile_scraping_privacy_token_in_about_usync` AB prop is on, each queried JID's TC token is attached to its `<user>` node in the usync IQ, matching WhatsApp Web's `USyncStatusProtocol`. This is what lets `status`/`status_error` and `about` resolve correctly for a privacy-restricted contact instead of coming back hidden. See [TC Token](/api/tctoken#automatic-usage) for details.

## Async compatibility

`is_on_whatsapp` and `get_user_info` work correctly when called from `#[async_trait]` implementations or any context that boxes the returned future (`Box<dyn Future + Send>`). Earlier versions produced a compile error (`"implementation of FnOnce is not general enough"`) that could not be worked around in user code. Fixed in [#826](https://github.com/oxidezap/whatsapp-rust/pull/826) with no API changes.

## Error handling

All methods return `Result<T, ContactError>`:

```rust theme={null}
#[non_exhaustive]
pub enum ContactError {
    #[error("{0}")]
    Iq(#[from] IqError),
    #[error("unsupported contact JID: {0}")]
    InvalidJid(String),
}
```

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

match client.contacts().is_on_whatsapp(&[jid]).await {
    Ok(results) => { /* ... */ }
    Err(ContactError::Iq(e)) => eprintln!("Server error: {}", e),
    Err(e) => eprintln!("Error: {}", e),
}
```

## Batch operations

All lookup methods support batch operations for efficiency:

```rust theme={null}
// Check multiple JIDs at once
let results = client.contacts().is_on_whatsapp(&[
    Jid::pn("15551111111"),
    Jid::pn("15552222222"),
    Jid::pn("15553333333"),
]).await?;

// Get info for multiple JIDs
let jids = vec![
    "15551111111@s.whatsapp.net".parse()?,
    "15552222222@s.whatsapp.net".parse()?,
];
let user_info = client.contacts().get_user_info(&jids).await?;
```

## Contact notification events

The server sends `contacts` notifications when contact data changes. These are emitted as events you can subscribe to:

* **`ContactUpdated`** — a contact's profile changed (invalidate cached presence/profile picture)
* **`ContactNumberChanged`** — a contact changed their phone number (includes old/new JID and optional LID mappings)
* **`ContactSyncRequested`** — the server requests a full contact re-sync

```rust theme={null}
Event::ContactUpdated(update) => {
    // Refresh cached profile data for update.jid
}
Event::ContactNumberChanged(change) => {
    // Migrate chat data from change.old_jid to change.new_jid
}
Event::ContactSyncRequested(sync) => {
    // Re-sync contacts (optionally filtered by sync.after timestamp)
}
```

See the [events reference](/concepts/events#contact-notification-events) for full struct definitions and wire format details.

## Empty input handling

Passing empty arrays returns empty results without making network requests:

```rust theme={null}
let empty: Vec<Jid> = vec![];
let results = client.contacts().is_on_whatsapp(&empty).await?;
assert!(results.is_empty());
```

## Migration from previous versions

The `is_on_whatsapp` method previously accepted `&[&str]` (phone number strings). It now takes `&[Jid]`:

```rust theme={null}
// Before
let results = client.contacts().is_on_whatsapp(&["1234567890"]).await?;

// After
let results = client.contacts().is_on_whatsapp(&[Jid::pn("1234567890")]).await?;
```

The `get_info` method and `ContactInfo` type have been removed. Use `is_on_whatsapp` for registration checks (now includes `lid`, `pn_jid`, and `is_business` fields) or `get_user_info` for detailed profile data (status, picture ID).
