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

# Groups

> Group management operations - create, modify, and manage WhatsApp groups

The `Groups` struct provides methods for managing WhatsApp groups, including creating groups, managing participants, and modifying group settings.

## Access

Access group operations through the client:

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

## Methods

### query\_info

Query group information with caching support.

<Tip>
  `query_info` and [`get_metadata`](#get_metadata) return different, purpose-built views of a group — pick based on what you're doing with the result:

  |         | `query_info`                                                                                               | `get_metadata`                                                                        |
  | ------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
  | Returns | `Arc<GroupInfo>` (slim)                                                                                    | `GroupMetadata` (full)                                                                |
  | Fields  | Participant JIDs, addressing mode, LID/PN mapping, CAG flag                                                | Subject, description, admin roles, ephemeral/membership settings, and everything else |
  | Cache   | Cached; a hit is free, a miss sends the persisted phash so an unchanged group costs a `not-modified` reply | Always hits the network; no phash sent, no population of the group cache              |
  | Use for | Routing and encrypting a message (the send path's own choice)                                              | Displaying or auditing a group                                                        |

  Use [`query_info_with_freshness`](#query_info_with_freshness) when you need explicit control over `query_info`'s staleness instead of the default cache-preferred behavior.
</Tip>

```rust theme={null}
pub async fn query_info(&self, jid: &Jid) -> Result<Arc<GroupInfo>, GroupError>
```

**Parameters:**

* `jid` - Group JID (must end with `@g.us`)

**Returns:**

* `Arc<GroupInfo>` - Shared, reference-counted snapshot containing the participants list and addressing mode. Repeated calls for the same group return the same `Arc` from the in-memory cache, so warm sends avoid deep-cloning group metadata.

**Example:**

```rust theme={null}
use std::sync::Arc;
use wacore::client::context::GroupInfo;

let group_jid: Jid = "123456789@g.us".parse()?;
let info: Arc<GroupInfo> = client.groups().query_info(&group_jid).await?;

println!("Participants: {}", info.participants.len());
println!("Addressing mode: {:?}", info.addressing_mode);

// Cheaply share the snapshot across tasks without copying participants.
let info_for_task = info.clone();
tokio::spawn(async move {
    println!("Participants in task: {}", info_for_task.participants.len());
});
```

### `query_info_with_freshness`

Query group information with an explicit cache [`Freshness`](#freshness) policy, instead of the always-cache-preferred behavior of `query_info`.

```rust theme={null}
pub async fn query_info_with_freshness(
    &self,
    jid: &Jid,
    freshness: Freshness,
) -> Result<Arc<GroupInfo>, GroupError>
```

**Parameters:**

* `jid` - Group JID (must end with `@g.us`)
* `freshness` - `Freshness::CachePreferred` (same as `query_info`) or `Freshness::Refresh` to force a network round-trip

**Returns:**

* `Arc<GroupInfo>` - Same shared snapshot type as `query_info`

A `Refresh` query leaves the currently cached snapshot readable to concurrent callers while the network request is in flight, then atomically replaces it once the response arrives — there is no window where the cache is empty.

**Example:**

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

// Force a refresh after a notification implies the cached snapshot is stale.
let info = client
    .groups()
    .query_info_with_freshness(&group_jid, Freshness::Refresh)
    .await?;
```

### get\_participating

Get all groups the client is participating in.

```rust theme={null}
pub async fn get_participating(&self) -> Result<HashMap<Jid, GroupMetadata>, GroupError>
```

**Returns:**

* `HashMap<Jid, GroupMetadata>` - Map of group `Jid` to metadata

<Note>
  This map is keyed by `Jid`. Call `.to_string()` on the key if you need the string form.
</Note>

`GroupMetadata` implements `PartialEq` and `Eq`, allowing direct comparison of group metadata instances.

**GroupMetadata fields:**

* `id: Jid` - Group JID
* `subject: String` - Group name
* `notify: Option<String>` - Display notification string reported by the server (from the `notify` attribute)
* `participants: Vec<GroupParticipant>` - List of participants
* `addressing_mode: AddressingMode` - Phone number or LID mode
* `creator: Option<Jid>` - Group creator JID
* `creator_pn: Option<Jid>` - Creator's phone-number JID, when `creator` is a LID
* `creator_username: Option<String>` - Creator's Meta username, when present
* `creator_country_code: Option<String>` - Creator's ISO country code, when present
* `creation_time: Option<u64>` - Group creation timestamp (Unix seconds)
* `participant_version_id: Option<String>` - Participant-list version identifier (from `p_v_id`)
* `admin_version_id: Option<String>` - Admin-list version identifier (from `a_v_id`)
* `open_thread_id: Option<String>` - Open thread identifier associated with the group
* `has_missing_participant_identification: bool` - Whether participant identity information was incomplete in this response
* `subject_time: Option<u64>` - Subject modification timestamp (Unix seconds)
* `subject_owner: Option<Jid>` - Subject owner JID
* `subject_owner_pn: Option<Jid>` - Subject owner's phone-number JID (from `s_o_pn`)
* `subject_owner_username: Option<String>` - Subject owner's Meta username (from `s_o_username`)
* `description: Option<String>` - Group description body text
* `description_id: Option<String>` - Description ID (for conflict detection)
* `description_owner: Option<Jid>` - JID of the participant who set the description
* `description_owner_pn: Option<Jid>` - Description owner's phone-number JID
* `description_owner_username: Option<String>` - Description owner's Meta username
* `description_time: Option<u64>` - Timestamp when the description was set (Unix seconds)
* `is_locked: bool` - Whether only admins can edit group info
* `is_announcement: bool` - Whether only admins can send messages
* `ephemeral: Option<GroupEphemeralSettings>` - Disappearing-message settings. `None` when the server response has no `<ephemeral>` node at all; `Some(GroupEphemeralSettings { expiration, trigger })` when the node is present — `expiration` is `None` if the node omitted the attribute, which is distinct from `Some(0)` (timer explicitly disabled)
* `membership_approval: bool` - Whether admin approval is required to join
* `member_add_mode: Option<MemberAddMode>` - Who can add members
* `member_link_mode: Option<MemberLinkMode>` - Who can use invite links
* `size: Option<u32>` - Total participant count
* `is_parent_group: bool` - Whether this group is a community parent group
* `parent_membership_approval_required: bool` - Whether joins to this parent group require approval by default
* `parent_group_jid: Option<Jid>` - JID of the parent community (for subgroups)
* `is_default_sub_group: bool` - Whether this is the default announcement subgroup of a community
* `is_general_chat: bool` - Whether this is the general chat subgroup of a community
* `allow_non_admin_sub_group_creation: bool` - Whether non-admin community members can create subgroups
* `no_frequently_forwarded: bool` - Whether frequently-forwarded messages are restricted
* `member_share_history_mode: Option<MemberShareHistoryMode>` - Who can share message history with new members
* `growth_locked: Option<GrowthLockInfo>` - Growth lock status (invite links temporarily disabled by the system)
* `is_suspended: bool` - Whether the group is suspended
* `suspension_can_auto_file: bool` - Whether a suspension appeal may be filed automatically
* `appeal_status: Option<GroupAppealStatus>` - Current suspension-appeal state
* `appeal_update_time: Option<u64>` - Last suspension-appeal update timestamp (Unix seconds)
* `is_support_group: bool` - Whether the group is marked as a support group
* `allow_admin_reports: bool` - Whether admin reports are allowed
* `is_hidden_group: bool` - Whether the group is hidden
* `is_incognito: bool` - Whether incognito mode is enabled
* `has_group_history: bool` - Whether group history is enabled
* `is_auto_add_disabled: bool` - Whether automatic participant addition is disabled
* `has_capi: bool` - Whether the group carries the CAPI capability marker
* `evolution_version: Option<u32>` - Group schema evolution version
* `has_group_safety_check: bool` - Whether the group safety-check feature is enabled
* `participant_label_enabled: bool` - Whether participant labels are enabled
* `is_limit_sharing_enabled: bool` - Whether limit sharing is enabled
* `limit_sharing_trigger: Option<u32>` - Source trigger for limit-sharing enablement

<Note>
  `ephemeral_expiration: u32` and `ephemeral_trigger: Option<u32>` were replaced by the single `ephemeral: Option<GroupEphemeralSettings>` field. Migrate reads like `metadata.ephemeral_expiration` to `metadata.ephemeral.as_ref().and_then(|e| e.expiration).unwrap_or(0)`.
</Note>

See [Community API](/api/community) for community-specific operations.

`GroupParticipant` implements `PartialEq` and `Eq`.

**GroupParticipant fields:**

* `jid: Jid` - Participant JID
* `phone_number: Option<Jid>` - Phone number JID (for LID groups)
* `lid: Option<Jid>` - Participant's LID JID, when the server includes one
* `username: Option<CompactString>` - Participant's Meta username, when present
* `participant_type: ParticipantType` - Participant role (member, admin, or super admin)
* `details: Option<Box<GroupParticipantDetails>>` - Less-common participant metadata (label, join time, display name, etc.); boxed and only populated when at least one field is present

**Example:**

```rust theme={null}
let groups = client.groups().get_participating().await?;

for (jid, metadata) in groups {
    println!("Group: {} ({})", metadata.subject, jid);
    println!("  Participants: {}", metadata.participants.len());
    
    for participant in &metadata.participants {
        println!("    {} ({:?})", participant.jid, participant.participant_type);
    }
}
```

### get\_metadata

Get metadata for a specific group.

```rust theme={null}
pub async fn get_metadata(&self, jid: &Jid) -> Result<GroupMetadata, GroupError>
```

**Parameters:**

* `jid` - Group JID

**Returns:**

* `GroupMetadata` - Owned, full user-facing metadata: subject, description, creator, per-participant admin roles, and ephemeral/membership settings (see `get_participating` for the full field list). In a LID-addressed group, participant phone numbers the server left out are backfilled from known LID/PN mappings on a best-effort basis; a participant with no known mapping keeps `phone_number: None`.

The query always hits the network (no phash is sent, so the server never answers `not-modified`) and the result does not populate the group cache. Use this for displaying or auditing a group; when you only need the participant list to send a message, prefer the cached [`query_info`](#query_info).

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let metadata = client.groups().get_metadata(&group_jid).await?;

println!("Subject: {}", metadata.subject);
println!("Mode: {:?}", metadata.addressing_mode);
```

### create\_group

Create a new group.

```rust theme={null}
pub async fn create_group(
    &self,
    options: GroupCreateOptions,
) -> Result<CreateGroupResult, GroupError>
```

**Parameters:**

* `options: GroupCreateOptions` - Group creation options
  * `subject: String` - Group name (max 100 characters)
  * `participants: Vec<GroupParticipantOptions>` - Initial participants
  * `member_link_mode: Option<MemberLinkMode>` - Who can use invite links (default: `AdminLink`)
  * `member_add_mode: Option<MemberAddMode>` - Who can add members (default: `AllMemberAdd`)
  * `membership_approval_mode: Option<MembershipApprovalMode>` - Require admin approval (default: `Off`)
  * `ephemeral_expiration: Option<u32>` - Disappearing messages timer in seconds (default: `0`)
  * `is_parent: bool` - Create as a community parent group (default: `false`)
  * `closed: bool` - Whether the community requires approval to join. Only used when `is_parent` is `true` (default: `false`)
  * `allow_non_admin_sub_group_creation: bool` - Allow non-admin members to create subgroups. Only used when `is_parent` is `true` (default: `false`)
  * `create_general_chat: bool` - Create a general chat subgroup alongside the community. Only used when `is_parent` is `true` (default: `false`)
  * `linked_parent: Option<Jid>` - Atomically link this group as a subgroup of an existing community on create. Mutually exclusive with `is_parent` — when set, the new group is always classified as a subgroup even if `is_parent: true` is also passed. (added in v0.6)
  * `description: Option<String>` - Inline group description, emitted as a `<description>` child of the `<create>` stanza so it lands in one round-trip. Matches the existing community-create behavior. (added in v0.6)

**Returns:**

* `CreateGroupResult` with a `metadata: GroupMetadata` field carrying the full server response (JID, subject, addressing mode, participants with display names, parent/community linkage, etc.).

When the `PRIVACY_TOKEN_ON_GROUP_CREATE` AB prop is enabled, the library automatically resolves and attaches privacy tokens (`tc_token`) to each participant during group creation. This is handled internally — you don't need to manage tokens yourself.

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::{GroupCreateOptions, GroupParticipantOptions};

let participant1: Jid = "15551234567@s.whatsapp.net".parse()?;
let participant2: Jid = "15559876543@s.whatsapp.net".parse()?;

let options = GroupCreateOptions::builder()
    .subject("My New Group")
    .participants(vec![
        GroupParticipantOptions::new(participant1),
        GroupParticipantOptions::new(participant2),
    ])
    .build();

let result = client.groups().create_group(options).await?;
println!("Created group: {} ({})", result.metadata.subject, result.metadata.id);
for participant in &result.metadata.participants {
    println!("- {} ({:?})", participant.jid, participant.participant_type);
}
```

<Note>
  Since v0.6, `create_group` returns the full `GroupMetadata` (matching `get_metadata`) instead of just the JID. Inspect `result.metadata` for participants, addressing mode, ephemeral timer, and parent linkage in a single round-trip. `GroupMetadata.participants` is a `Vec<GroupParticipant>` (the IQ-response shape), not the `GroupParticipantInfo` used by group notification events — but masked-number `display_name` labels are available here too, via `participant.details.as_ref().and_then(|d| d.display_name.as_deref())`, in addition to the event-side `GroupParticipantInfo.display_name` for live group-update events.
</Note>

### set\_subject

Change the group name.

```rust theme={null}
pub async fn set_subject(&self, jid: &Jid, subject: GroupSubject) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `subject` - New group name (max 100 characters)

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let new_subject = GroupSubject::new("Updated Group Name")?;

client.groups().set_subject(&group_jid, new_subject).await?;
```

### set\_description

Set or delete the group description.

```rust theme={null}
pub async fn set_description(
    &self,
    jid: impl Into<Jid>,
    description: Option<GroupDescription>,
    prev: PreviousDescription<'_>,
) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `description` - New description (max 2048 characters) or `None` to delete
* `prev` - The description this update replaces, as a [`PreviousDescription`](#previousdescription). The server accepts the update only when this token matches the group's current description id — a group that already has a description cannot be updated without it.

<Note>
  [`PreviousDescription::Resolve`](#previousdescription) (the default) reads the current id from the server first, which always works but costs one extra query. If you already hold fresh [`GroupMetadata::description_id`](#groupmetadata), pass it directly — `metadata.description_id.as_deref().into()` — to skip that query. Use `PreviousDescription::Absent` for a group you know has no description yet, such as one you just created.
</Note>

Returns [`GroupError::DescriptionConflict`](#grouperror) if the group's description changed on the server between the read and this update (for example, another device changed it first).

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::PreviousDescription;

let group_jid: Jid = "123456789@g.us".parse()?;
let desc = GroupDescription::new("This is our group chat")?;

// Resolves the current description id from the server automatically
client.groups().set_description(&group_jid, Some(desc), PreviousDescription::Resolve).await?;

// Skip the extra round trip with a token already on hand
let updated = GroupDescription::new("Updated topic")?;
let metadata = client.groups().get_metadata(&group_jid).await?;
client.groups()
    .set_description(&group_jid, Some(updated), metadata.description_id.as_deref().into())
    .await?;

// A freshly created group has no description yet
client.groups().set_description(&group_jid, Some(desc), PreviousDescription::Absent).await?;

// Delete description
client.groups().set_description(&group_jid, None, PreviousDescription::Resolve).await?;
```

### leave

Leave a group.

```rust theme={null}
pub async fn leave(&self, jid: &Jid) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID to leave

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
client.groups().leave(&group_jid).await?;
```

### add\_participants

Add participants to a group.

```rust theme={null}
pub async fn add_participants(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of participant JIDs to add

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

When the `PRIVACY_TOKEN_ON_GROUP_PARTICIPANT_ADD` AB prop is enabled, the library automatically resolves and attaches privacy tokens (`tc_token`) to each participant. This is handled internally — you don't need to manage tokens yourself.

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let new_members = vec![
    "15551234567@s.whatsapp.net".parse()?,
    "15559876543@s.whatsapp.net".parse()?,
];

let results = client.groups().add_participants(&group_jid, &new_members).await?;

for result in results {
    println!("Added: {:?}", result);
}
```

### remove\_participants

Remove participants from a group.

```rust theme={null}
pub async fn remove_participants(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of participant JIDs to remove

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let to_remove = vec!["15551234567@s.whatsapp.net".parse()?];

client.groups().remove_participants(&group_jid, &to_remove).await?;
```

### remove\_participants\_including\_linked\_groups

Remove participants from a group and cascade the removal to its linked/child groups. Used for community-linked groups, where removing someone from the community should also remove them from subgroups.

```rust theme={null}
pub async fn remove_participants_including_linked_groups(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID (typically the community parent group)
* `participants` - Array of participant JIDs to remove

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let to_remove = vec!["15551234567@s.whatsapp.net".parse()?];

let results = client.groups().remove_participants_including_linked_groups(&group_jid, &to_remove).await?;
```

### promote\_participants

Promote participants to admin.

```rust theme={null}
pub async fn promote_participants(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of participant JIDs to promote

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let to_promote = vec!["15551234567@s.whatsapp.net".parse()?];

let results = client.groups().promote_participants(&group_jid, &to_promote).await?;

for result in results {
    println!("Promoted {}: status {:?}", result.jid, result.status);
}
```

<Note>
  `promote_participants` returns `Vec<ParticipantChangeResponse>` (one entry per participant) instead of `()`.
</Note>

### demote\_participants

Demote admin participants to regular members.

```rust theme={null}
pub async fn demote_participants(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of admin JIDs to demote

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let to_demote = vec!["15551234567@s.whatsapp.net".parse()?];

let results = client.groups().demote_participants(&group_jid, &to_demote).await?;

for result in results {
    println!("Demoted {}: status {:?}", result.jid, result.status);
}
```

<Note>
  `demote_participants` returns `Vec<ParticipantChangeResponse>` (one entry per participant) instead of `()`.
</Note>

### get\_invite\_link

Get or reset the group invite link.

```rust theme={null}
pub async fn get_invite_link(&self, jid: &Jid, reset: bool) -> Result<String, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `reset` - Whether to reset and generate a new invite link

**Returns:**

* `String` - Invite link code

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Get current invite link
let link = client.groups().get_invite_link(&group_jid, false).await?;
println!("Invite link: https://chat.whatsapp.com/{}", link);

// Reset and get new link
let new_link = client.groups().get_invite_link(&group_jid, true).await?;
println!("New invite link: https://chat.whatsapp.com/{}", new_link);
```

### join\_with\_invite\_code

Join a group using an invite code or full invite URL.

```rust theme={null}
pub async fn join_with_invite_code(&self, code: &str) -> Result<JoinGroupResult, GroupError>
```

**Parameters:**

* `code` - Invite code or full URL (e.g., `"AbCdEfGh"` or `"https://chat.whatsapp.com/AbCdEfGh"`)

**Returns:**

* `JoinGroupResult` - Either `Joined(Jid)` if immediately joined, or `PendingApproval(Jid)` if the group requires admin approval

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::JoinGroupResult;

// Join using a full URL or just the code
let result = client.groups().join_with_invite_code("https://chat.whatsapp.com/AbCdEfGh").await?;

match &result {
    JoinGroupResult::Joined(jid) => println!("Joined group: {}", jid),
    JoinGroupResult::PendingApproval(jid) => println!("Pending approval for group: {}", jid),
}

// Access the group JID regardless of result
println!("Group JID: {}", result.group_jid());
```

### join\_with\_invite\_v4

Accept a V4 group invite received as a `GroupInviteMessage` (not a link). V4 invites are sent directly by a group admin as a message, rather than shared as a URL.

```rust theme={null}
pub async fn join_with_invite_v4(
    &self,
    group_jid: &Jid,
    code: &str,
    expiration: i64,
    admin_jid: &Jid,
) -> Result<JoinGroupResult, GroupError>
```

**Parameters:**

* `group_jid` - The target group JID
* `code` - Invite code from the `GroupInviteMessage`
* `expiration` - Invite expiration timestamp (Unix seconds). The method returns an error if the invite has expired.
* `admin_jid` - JID of the admin who sent the invite

**Returns:**

* `JoinGroupResult` - Either `Joined(Jid)` if immediately joined, or `PendingApproval(Jid)` if the group requires admin approval

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::JoinGroupResult;

let group_jid: Jid = "123456789@g.us".parse()?;
let admin_jid: Jid = "15551234567@s.whatsapp.net".parse()?;

let result = client.groups().join_with_invite_v4(
    &group_jid,
    "AbCdEfGh",
    1735689600,    // expiration timestamp
    &admin_jid,
).await?;

match &result {
    JoinGroupResult::Joined(jid) => println!("Joined group: {}", jid),
    JoinGroupResult::PendingApproval(jid) => println!("Pending approval for group: {}", jid),
}
```

<Note>
  V4 invites expire. The method automatically checks the expiration timestamp and returns an error if the invite has already expired. You can pass `0` as the expiration to skip the expiration check.
</Note>

### get\_invite\_info

Get group metadata from an invite code without joining the group.

```rust theme={null}
pub async fn get_invite_info(&self, code: &str) -> Result<GroupMetadata, GroupError>
```

**Parameters:**

* `code` - Invite code or full URL

**Returns:**

* `GroupMetadata` - Group metadata (see `get_participating` for fields)

**Example:**

```rust theme={null}
// Preview a group before joining
let metadata = client.groups().get_invite_info("AbCdEfGh").await?;

println!("Group: {}", metadata.subject);
println!("Participants: {}", metadata.participants.len());
println!("Approval required: {}", metadata.membership_approval);
```

### set\_locked

Lock or unlock the group so only admins can change group info.

```rust theme={null}
pub async fn set_locked(&self, jid: &Jid, locked: bool) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `locked` - `true` to lock (only admins edit info), `false` to unlock

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Lock group info
client.groups().set_locked(&group_jid, true).await?;

// Unlock group info
client.groups().set_locked(&group_jid, false).await?;
```

### set\_announce

Set announcement mode. When enabled, only admins can send messages.

```rust theme={null}
pub async fn set_announce(&self, jid: &Jid, announce: bool) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `announce` - `true` to enable (only admins send), `false` to disable

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Enable announcement mode
client.groups().set_announce(&group_jid, true).await?;

// Disable announcement mode
client.groups().set_announce(&group_jid, false).await?;
```

### set\_ephemeral

Set the disappearing messages timer on the group.

```rust theme={null}
pub async fn set_ephemeral(&self, jid: &Jid, expiration: u32) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `expiration` - Timer duration in seconds. Common values: `86400` (24 hours), `604800` (7 days), `7776000` (90 days). Pass `0` to disable.

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Enable 7-day disappearing messages
client.groups().set_ephemeral(&group_jid, 604800).await?;

// Disable disappearing messages
client.groups().set_ephemeral(&group_jid, 0).await?;
```

### set\_membership\_approval

Set membership approval mode. When enabled, new members must be approved by an admin.

```rust theme={null}
pub async fn set_membership_approval(
    &self,
    jid: &Jid,
    mode: MembershipApprovalMode,
) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `mode` - `MembershipApprovalMode::On` to require approval, `MembershipApprovalMode::Off` to disable

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::MembershipApprovalMode;

let group_jid: Jid = "123456789@g.us".parse()?;

// Require admin approval
client.groups().set_membership_approval(&group_jid, MembershipApprovalMode::On).await?;

// Remove approval requirement
client.groups().set_membership_approval(&group_jid, MembershipApprovalMode::Off).await?;
```

### get\_membership\_requests

Get pending membership approval requests for a group.

```rust theme={null}
pub async fn get_membership_requests(
    &self,
    jid: &Jid,
) -> Result<Vec<MembershipRequest>, GroupError>
```

**Parameters:**

* `jid` - Group JID

**Returns:**

* `Vec<MembershipRequest>` - List of pending requests

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let requests = client.groups().get_membership_requests(&group_jid).await?;

for request in &requests {
    println!("Pending request from: {}", request.jid);
    if let Some(time) = request.request_time {
        println!("  Requested at: {}", time);
    }
}
```

### approve\_membership\_requests

Approve pending membership requests for a group.

```rust theme={null}
pub async fn approve_membership_requests(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of JIDs to approve

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Get pending requests
let requests = client.groups().get_membership_requests(&group_jid).await?;
let jids: Vec<Jid> = requests.iter().map(|r| r.jid.clone()).collect();

// Approve all pending requests
let results = client.groups().approve_membership_requests(&group_jid, &jids).await?;

for result in results {
    println!("Approved {}: status {:?}", result.jid, result.status);
}
```

### reject\_membership\_requests

Reject pending membership requests for a group.

```rust theme={null}
pub async fn reject_membership_requests(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of JIDs to reject

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let to_reject = vec!["15551234567@s.whatsapp.net".parse()?];

let results = client.groups().reject_membership_requests(&group_jid, &to_reject).await?;
```

### set\_member\_add\_mode

Set who can add members to the group.

```rust theme={null}
pub async fn set_member_add_mode(
    &self,
    jid: &Jid,
    mode: MemberAddMode,
) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `mode` - `MemberAddMode::AdminAdd` to restrict to admins, `MemberAddMode::AllMemberAdd` to allow all members

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::MemberAddMode;

let group_jid: Jid = "123456789@g.us".parse()?;

// Only admins can add members
client.groups().set_member_add_mode(&group_jid, MemberAddMode::AdminAdd).await?;

// All members can add others
client.groups().set_member_add_mode(&group_jid, MemberAddMode::AllMemberAdd).await?;
```

### set\_no\_frequently\_forwarded

Restrict or allow frequently-forwarded messages in the group.

```rust theme={null}
pub async fn set_no_frequently_forwarded(
    &self,
    jid: &Jid,
    restrict: bool,
) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `restrict` - `true` to restrict frequently-forwarded messages, `false` to allow them

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Restrict frequently-forwarded messages
client.groups().set_no_frequently_forwarded(&group_jid, true).await?;

// Allow frequently-forwarded messages
client.groups().set_no_frequently_forwarded(&group_jid, false).await?;
```

### set\_allow\_admin\_reports

Enable or disable admin reports in the group.

```rust theme={null}
pub async fn set_allow_admin_reports(
    &self,
    jid: &Jid,
    allow: bool,
) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `allow` - `true` to enable admin reports, `false` to disable

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Enable admin reports
client.groups().set_allow_admin_reports(&group_jid, true).await?;

// Disable admin reports
client.groups().set_allow_admin_reports(&group_jid, false).await?;
```

### set\_group\_history

Enable or disable group history sharing.

```rust theme={null}
pub async fn set_group_history(&self, jid: &Jid, enabled: bool) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `enabled` - `true` to enable group history, `false` to disable

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Enable group history
client.groups().set_group_history(&group_jid, true).await?;

// Disable group history
client.groups().set_group_history(&group_jid, false).await?;
```

### set\_member\_link\_mode

Set who can share invite links. This uses the MEX (Mutation Exchange) protocol.

```rust theme={null}
pub async fn set_member_link_mode(
    &self,
    jid: &Jid,
    mode: MemberLinkMode,
) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `mode` - `MemberLinkMode::AdminLink` to restrict to admins, `MemberLinkMode::AllMemberLink` to allow all members

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::MemberLinkMode;

let group_jid: Jid = "123456789@g.us".parse()?;

// Only admins can share invite links
client.groups().set_member_link_mode(&group_jid, MemberLinkMode::AdminLink).await?;

// All members can share invite links
client.groups().set_member_link_mode(&group_jid, MemberLinkMode::AllMemberLink).await?;
```

### set\_member\_share\_history\_mode

Set who can share message history with new members. This uses the MEX protocol.

```rust theme={null}
pub async fn set_member_share_history_mode(
    &self,
    jid: &Jid,
    mode: MemberShareHistoryMode,
) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `mode` - `MemberShareHistoryMode::AdminShare` to restrict to admins, `MemberShareHistoryMode::AllMemberShare` to allow all members

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::MemberShareHistoryMode;

let group_jid: Jid = "123456789@g.us".parse()?;

// Only admins can share history with new members
client.groups().set_member_share_history_mode(
    &group_jid,
    MemberShareHistoryMode::AdminShare,
).await?;

// All members can share history
client.groups().set_member_share_history_mode(
    &group_jid,
    MemberShareHistoryMode::AllMemberShare,
).await?;
```

### set\_limit\_sharing

Enable or disable limit sharing in the group. This uses the MEX protocol.

```rust theme={null}
pub async fn set_limit_sharing(&self, jid: &Jid, enabled: bool) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID
* `enabled` - `true` to enable limit sharing, `false` to disable

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;

// Enable limit sharing
client.groups().set_limit_sharing(&group_jid, true).await?;

// Disable limit sharing
client.groups().set_limit_sharing(&group_jid, false).await?;
```

### cancel\_membership\_requests

Cancel pending membership requests from the requesting user's side.

```rust theme={null}
pub async fn cancel_membership_requests(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of JIDs whose pending requests to cancel

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let to_cancel = vec!["15551234567@s.whatsapp.net".parse()?];

let results = client.groups().cancel_membership_requests(&group_jid, &to_cancel).await?;
```

### revoke\_request\_code

Revoke invitation codes from specific participants. This is an admin operation.

```rust theme={null}
pub async fn revoke_request_code(
    &self,
    jid: &Jid,
    participants: &[Jid],
) -> Result<Vec<ParticipantChangeResponse>, GroupError>
```

**Parameters:**

* `jid` - Group JID
* `participants` - Array of participant JIDs whose invitation codes to revoke

**Returns:**

* `Vec<ParticipantChangeResponse>` - Result for each participant

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let to_revoke = vec!["15551234567@s.whatsapp.net".parse()?];

let results = client.groups().revoke_request_code(&group_jid, &to_revoke).await?;
```

### acknowledge

Acknowledge a group notification.

```rust theme={null}
pub async fn acknowledge(&self, jid: &Jid) -> Result<(), GroupError>
```

**Parameters:**

* `jid` - Group JID

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
client.groups().acknowledge(&group_jid).await?;
```

### update\_member\_label\_with\_id

Set or clear the bot's per-group member label, sent as a `ProtocolMessage` over the normal message path (not an IQ). Returns the sent stanza's message ID.

```rust theme={null}
pub async fn update_member_label_with_id(
    &self,
    group_jid: &Jid,
    label: impl Into<String>,
) -> Result<String, GroupError>
```

**Parameters:**

* `group_jid` - Group JID
* `label` - New label text, or an empty string to clear the label

**Returns:**

* `String` - Message ID of the sent stanza

**Example:**

```rust theme={null}
let group_jid: Jid = "123456789@g.us".parse()?;
let message_id = client.groups().update_member_label_with_id(&group_jid, "VIP").await?;
println!("Label update sent as message {}", message_id);
```

<Note>
  `update_member_label` is a thin wrapper around `update_member_label_with_id` that discards the message ID and returns `Result<(), GroupError>`, for callers that don't need it.
</Note>

### batch\_get\_info

Batch query group info for multiple groups at once.

```rust theme={null}
pub async fn batch_get_info(
    &self,
    jids: Vec<Jid>,
) -> Result<Vec<BatchGroupResult>, GroupError>
```

**Parameters:**

* `jids` - List of group JIDs to query (max 10,000)

**Returns:**

* `Vec<BatchGroupResult>` - Result for each group

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::BatchGroupResult;

let group_jids: Vec<Jid> = vec![
    "120363012345678@g.us".parse()?,
    "120363087654321@g.us".parse()?,
];

let results = client.groups().batch_get_info(group_jids).await?;

for result in results {
    match result {
        BatchGroupResult::Full(metadata) => {
            println!("Group: {} - {}", metadata.id, metadata.subject);
        }
        BatchGroupResult::Truncated { id, size } => {
            println!("Truncated: {} (size: {:?})", id, size);
        }
        BatchGroupResult::Forbidden(id) => {
            println!("Forbidden: {}", id);
        }
        BatchGroupResult::NotFound(id) => {
            println!("Not found: {}", id);
        }
    }
}
```

### set\_profile\_picture

Set a group's profile picture. Admin operation.

```rust theme={null}
pub async fn set_profile_picture(
    &self,
    group_jid: &Jid,
    image_data: Vec<u8>,
) -> Result<SetProfilePictureResponse, GroupError>
```

**Parameters:**

* `group_jid` - Group JID (must end with `@g.us`)
* `image_data` - JPEG image bytes. The caller is responsible for sizing/cropping the image (WhatsApp uses 640x640).

**Returns:**

* `SetProfilePictureResponse` - Contains the new picture ID

Passing empty `image_data` routes to removal, mirroring the own-picture API. Prefer [`remove_profile_picture`](#remove_profile_picture) when removal is the intent.

**Example:**

```rust theme={null}
use std::fs;

let group_jid: Jid = "120363012345678@g.us".parse()?;
let image_bytes = fs::read("group_avatar.jpg")?;

let response = client.groups().set_profile_picture(&group_jid, image_bytes).await?;
if let Some(id) = response.id {
    println!("Group picture updated: {}", id);
}
```

<Warning>
  The image must be a valid JPEG. Other formats are not supported. The caller must be a group admin.
</Warning>

### remove\_profile\_picture

Remove a group's profile picture. Admin operation.

```rust theme={null}
pub async fn remove_profile_picture(
    &self,
    group_jid: &Jid,
) -> Result<SetProfilePictureResponse, GroupError>
```

**Parameters:**

* `group_jid` - Group JID

**Returns:**

* `SetProfilePictureResponse` - Confirmation of removal

**Example:**

```rust theme={null}
let group_jid: Jid = "120363012345678@g.us".parse()?;
client.groups().remove_profile_picture(&group_jid).await?;
```

See [`SetProfilePictureResponse`](/api/profile#setprofilepictureresponse) for the response shape.

### get\_profile\_pictures

Batch fetch group profile pictures.

```rust theme={null}
pub async fn get_profile_pictures(
    &self,
    group_jids: Vec<Jid>,
    picture_type: PictureType,
) -> Result<Vec<GroupProfilePicture>, GroupError>
```

**Parameters:**

* `group_jids` - List of group JIDs (max 1,000)
* `picture_type` - `PictureType::Preview` for thumbnails or `PictureType::Image` for full-size

**Returns:**

* `Vec<GroupProfilePicture>` - Profile picture data for each group

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::{PictureType, GroupProfilePicture};

let group_jids: Vec<Jid> = vec![
    "120363012345678@g.us".parse()?,
    "120363087654321@g.us".parse()?,
];

let pictures = client.groups().get_profile_pictures(group_jids, PictureType::Preview).await?;

for pic in pictures {
    if let Some(url) = &pic.url {
        println!("Group {}: {}", pic.group_jid, url);
    }
}
```

## Types

### `Freshness`

Selects whether an operation may return an existing cached snapshot or must consult its source before returning. Shared across the SDK wherever a cache-backed lookup accepts an explicit staleness policy — `Groups::query_info_with_freshness` here, and `StatusSendOptions::device_freshness` (see [Status](/api/status#statussendoptions)).

```rust theme={null}
#[non_exhaustive]
pub enum Freshness {
    /// Return a cached snapshot when available and consult the source on a miss (default).
    CachePreferred,
    /// Consult the source and publish the resulting snapshot without clearing the
    /// previous one first.
    Refresh,
}
```

`Freshness` is re-exported from the crate root, so you can import it directly: `use whatsapp_rust::Freshness;`. When you pass `Refresh`, you never see a caller-visible gap — the previous snapshot stays servable until the new one is published.

### GroupInfo

Cached group information returned by `query_info`. Contains participant list, addressing mode, and LID-to-phone mappings for privacy-addressed groups.

`query_info` returns `Arc<GroupInfo>` so that repeated lookups and warm sends share the same snapshot without deep-cloning the participants list.

```rust theme={null}
pub struct GroupInfo {
    pub participants: Vec<Jid>,
    pub addressing_mode: AddressingMode,
}
```

**Methods:**

| Method                                                                | Description                                    |
| --------------------------------------------------------------------- | ---------------------------------------------- |
| `phone_jid_for_lid_user(lid_user: &str) -> Option<&Jid>`              | Look up the phone JID for a LID user           |
| `lid_user_for_phone_user(phone_user: &str) -> Option<&CompactString>` | Look up the LID user string for a phone number |
| `phone_device_jid_to_lid(phone_device_jid: &Jid) -> Jid`              | Convert a phone-based device JID to LID format |

**Example:**

```rust theme={null}
let info = client.groups().query_info(&group_jid).await?; // Arc<GroupInfo>

// Access participants and addressing mode
for participant in &info.participants {
    println!("Participant: {}", participant);
}

// For LID-addressed groups, resolve phone numbers
if info.addressing_mode == AddressingMode::Lid {
    if let Some(phone_jid) = info.phone_jid_for_lid_user("lid_user_id") {
        println!("Phone: {}", phone_jid);
    }
}
```

### GroupSubject

Validated group name with 100 character limit.

```rust theme={null}
impl GroupSubject {
    pub fn new(subject: impl Into<String>) -> Result<Self, anyhow::Error>
    pub fn into_string(self) -> String
}
```

### GroupDescription

Validated group description with 2048 character limit.

```rust theme={null}
impl GroupDescription {
    pub fn new(description: impl Into<String>) -> Result<Self, anyhow::Error>
    pub fn into_string(self) -> String
}
```

### PreviousDescription

The description a [`set_description`](#set_description) call expects to replace — the server's optimistic-concurrency token for the group's description.

```rust theme={null}
pub enum PreviousDescription<'a> {
    Resolve,      // default
    Absent,
    Id(&'a str),
}

impl<'a> From<Option<&'a str>> for PreviousDescription<'a>
```

* `Resolve` (default) — read the group's current description id from the server before sending. The only variant that's correct without knowing anything about the group, and the only one that costs a round trip.
* `Absent` — the group carries no description yet (e.g. one you just created), so no token is sent.
* `Id(&str)` — a description id you already hold, typically [`GroupMetadata::description_id`](#groupmetadata) from a recent [`get_metadata`](#get_metadata) call.

`From<Option<&str>>` lets you turn a held `GroupMetadata::description_id` into a token directly: `None` maps to `Absent` (that metadata says the group has no description), `Some(id)` maps to `Id(id)`. Use `Resolve` instead when the metadata's age is unknown, since stale metadata that missed a description added since would otherwise surface as `GroupError::DescriptionConflict`.

### GroupEphemeralSettings

Disappearing-message settings carried by a group's `<ephemeral>` node.

```rust theme={null}
pub struct GroupEphemeralSettings {
    pub expiration: Option<u32>,
    pub trigger: Option<u32>,
}
```

### MemberAddMode

```rust theme={null}
pub enum MemberAddMode {
    AdminAdd,      // Only admins can add members
    AllMemberAdd,  // All members can add
}
```

### ParticipantType

Participant role within a group.

```rust theme={null}
pub enum ParticipantType {
    Member,     // Regular member
    Admin,      // Group admin
    SuperAdmin, // Group creator / super admin
}
```

**Methods:**

* `is_admin(&self) -> bool` - Returns `true` if admin or super admin

### GroupParticipantDetails

Less-common participant metadata. Boxed on `GroupParticipant`/`GroupParticipantResponse` and only populated when at least one field is present.

```rust theme={null}
#[non_exhaustive]
pub struct GroupParticipantDetails {
    pub participant_label: Option<CompactString>,
    pub participant_label_mtime: Option<u64>,
    pub join_time: Option<u64>,
    pub group_history_sent: Option<bool>,
    pub display_name: Option<CompactString>,
    pub is_addressable: bool,
}
```

<Note>
  `GroupParticipantDetails` is `#[non_exhaustive]`. Field reads are unaffected; only exhaustive struct destructuring from outside the crate requires adding `..`.
</Note>

### MemberLinkMode

Controls who can use invite links to join the group.

```rust theme={null}
pub enum MemberLinkMode {
    AdminLink,      // Only admins can share invite links
    AllMemberLink,  // All members can share invite links
}
```

### MemberShareHistoryMode

Controls who can share message history with new members.

```rust theme={null}
pub enum MemberShareHistoryMode {
    AdminShare,      // Only admins can share history
    AllMemberShare,  // All members can share history
}
```

### MembershipApprovalMode

```rust theme={null}
pub enum MembershipApprovalMode {
    Off,  // No approval required
    On,   // Admin approval required
}
```

### GroupAppealStatus

Review state for an appeal on a suspended group.

```rust theme={null}
pub enum GroupAppealStatus {
    Approved,
    InReview,
    NoAppeal,
    Rejected,
}
```

### GroupParticipantOptions

Options for specifying a participant when creating or modifying a group.

```rust theme={null}
pub struct GroupParticipantOptions {
    pub jid: Jid,
    pub phone_number: Option<Jid>,
    pub privacy: Option<Vec<u8>>,
}
```

**Constructors:**

* `GroupParticipantOptions::new(jid)` - Create from a JID
* `GroupParticipantOptions::from_phone(phone_number)` - Create from a phone number JID
* `GroupParticipantOptions::from_lid_and_phone(lid, phone_number)` - Create from a LID and phone number

**Builder methods:**

* `.with_phone_number(jid)` - Attach a phone number JID (for LID participants)
* `.with_privacy(token)` - Attach a privacy token (`tc_token` bytes)

<Note>
  You typically don't need to set the `privacy` field manually. When AB props are enabled, the library automatically resolves and attaches privacy tokens during `create_group` and `add_participants` operations.
</Note>

**Example:**

```rust theme={null}
use whatsapp_rust::features::groups::GroupParticipantOptions;

let participant = GroupParticipantOptions::new("15551234567@s.whatsapp.net".parse()?);
```

### JoinGroupResult

Result of joining a group via invite code.

```rust theme={null}
pub enum JoinGroupResult {
    Joined(Jid),          // Successfully joined
    PendingApproval(Jid), // Membership approval required
}
```

**Methods:**

* `group_jid(&self) -> &Jid` - Returns the group JID regardless of result variant

### MembershipRequest

A pending membership approval request.

```rust theme={null}
pub struct MembershipRequest {
    pub jid: Jid,
    pub request_time: Option<u64>,
}
```

### ParticipantChangeResponse

Result of a participant change operation (add, remove, approve, reject).

```rust theme={null}
#[non_exhaustive]
pub struct ParticipantChangeResponse {
    pub jid: Jid,
    pub status: Option<String>,
    pub error: Option<String>,
    pub add_request: Option<AddRequestInfo>,
}
```

<Note>
  `ParticipantChangeResponse` is `#[non_exhaustive]`. Field reads are unaffected; only exhaustive struct destructuring from outside the crate requires adding `..`.
</Note>

`add_request` is populated when the server responds with HTTP 403 carrying an `<add_request>` child — that happens when a participant has privacy blocked direct adds and the inviter must send them a v4 invite link out-of-band. Since v0.6 the value is preserved instead of being dropped, so consumers can drive the v4 invite flow without parsing the raw IQ.

```rust theme={null}
pub struct AddRequestInfo {
    pub code: String,        // v4 invite token
    pub expiration: i64,     // unix seconds; 0 means the server omitted it
}
```

### AddressingMode

```rust theme={null}
pub enum AddressingMode {
    Pn,   // Phone number addressing
    Lid,  // LID (privacy) addressing
}
```

### BatchGroupResult

Result for a single group in a batch query.

```rust theme={null}
pub enum BatchGroupResult {
    Full(Box<GroupMetadata>),           // Full group metadata
    Truncated { id: Jid, size: Option<u32> },  // Only ID and size returned
    Forbidden(Jid),                     // Access denied
    NotFound(Jid),                      // Group does not exist
}
```

### GrowthLockInfo

Growth lock information (system-managed, read-only). When present, invite links are temporarily disabled by the system.

```rust theme={null}
pub struct GrowthLockInfo {
    pub lock_type: String,
    pub expiration: u64,
}
```

### PictureType

Profile picture query type for batch fetching.

```rust theme={null}
pub enum PictureType {
    Preview,  // Thumbnail / preview size
    Image,    // Full-size image
}
```

### GroupProfilePicture

A single group profile picture result from a batch query.

```rust theme={null}
pub struct GroupProfilePicture {
    pub group_jid: Jid,
    pub url: Option<String>,
    pub direct_path: Option<String>,
    pub photo_id: Option<String>,
}
```

### CreateGroupResult

Result of creating a group.

```rust theme={null}
#[non_exhaustive]
pub struct CreateGroupResult {
    pub metadata: GroupMetadata,
}
```

<Note>
  `CreateGroupResult` is `#[non_exhaustive]`. Field reads are unaffected; only exhaustive struct destructuring from outside the crate requires adding `..`.
</Note>

The `metadata` field carries the full group state returned by the server (same shape as [`get_metadata`](#get_metadata)). `metadata.participants` is `Vec<GroupParticipant>` — masked-number `display_name` labels are reachable via `participant.details.as_ref().and_then(|d| d.display_name.as_deref())`, or from the event-side `GroupParticipantInfo` (which carries `<participant>` children of *notification* events) when handling live group-update events.

<Note>
  Prior to v0.6 this struct only exposed a `gid: Jid`. Replace `result.gid` with `result.metadata.id` when upgrading (`GroupMetadata.id`, not `jid`). The same migration applies to `CreateCommunityResult`.
</Note>

### GroupJoinError

Error codes returned when joining a group via invite fails.

```rust theme={null}
pub enum GroupJoinError {
    AlreadyMember,       // 304 - Already a member of the group
    BadRequest,          // 400 - Invalid request
    Forbidden,           // 403 - Not allowed to join
    NotFound,            // 404 - Group not found
    NotAllowed,          // 405 - Join method not allowed
    Conflict,            // 409 - Conflicting state
    Gone,                // 410 - Group no longer exists
    CommunityFull,       // 412 - Community has reached capacity
    GroupFull,           // 419 - Group has reached capacity
    Locked,              // 423 - Group is locked
    Unknown(u16),        // Other error code
}
```

**Methods:**

* `from_code(code: u16) -> Self` - Create from a numeric status code
* `code(&self) -> u16` - Get the numeric status code

### InviteInfoError

Error codes returned when fetching group info from an invite code.

```rust theme={null}
pub enum InviteInfoError {
    BadRequest,            // 400 - Invalid request
    NotAuthorized,         // 401 - Not authorized
    NotFound,              // 404 - Invite code not found
    NotAcceptable,         // 406 - Not acceptable
    Gone,                  // 410 - Invite code expired
    ParentGroupSuspended,  // 416 - Parent community is suspended
    Locked,                // 423 - Group is locked
    GrowthLocked,          // 436 - Group growth is temporarily locked
    Unknown(u16),          // Other error code
}
```

**Methods:**

* `from_code(code: u16) -> Self` - Create from a numeric status code
* `code(&self) -> u16` - Get the numeric status code

## Error types

### `GroupError`

All group methods return `Result<T, GroupError>`:

```rust theme={null}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum GroupError {
    #[error("{0}")]
    Iq(#[from] IqError),
    #[error("{0}")]
    Mex(#[from] MexError),
    #[error("invalid group request: {0}")]
    InvalidRequest(String),
    #[error("the group description changed since it was read")]
    DescriptionConflict,
    #[error("{0}")]
    Internal(#[from] anyhow::Error),
}
```

**Variants:**

* `Iq` — IQ request failed (timeout, server rejection, etc.)
* `Mex` — MEX protocol error (for methods using the MEX transport)
* `InvalidRequest` — malformed request (expired invite, missing fields, etc.)
* `DescriptionConflict` — [`set_description`](#set_description)'s `prev` token no longer matches the group's current description (the server answered `409 conflict`); another device changed it first. Kept distinct from a permission refusal so a caller can re-read the description and retry instead of giving up.
* `Internal` — catch-all for other errors

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

match client.groups().leave(&group_jid).await {
    Ok(_) => println!("Left group"),
    Err(GroupError::Iq(e)) => eprintln!("Server rejected: {}", e),
    Err(GroupError::InvalidRequest(msg)) => eprintln!("Bad request: {}", msg),
    Err(e) => eprintln!("Error: {}", e),
}
```
