# Group Chat

The group chat class allows you to easily work with a [Status Group Chat](https://status.app/help/messaging/create-a-group-chat). Group chats aren't the same as communities - they are meant for smaller groups of people. **A group chat can have 20 members at most**
A `GroupChat` is always bound to a logged-in [`Account`](./account.md). It can either wrap an **existing** chat (by passing a `chat_id`) or create a brand new one. **A chat be created only with [mutual contacts](./account.md#contacts)** - accounts where the `mutual` key is `True`.
## Administrator
The account that creates a group chat becomes its **administrator**. Only the administrator can [remove](./group-chat.md#removepublic_keys) members from the chat. Every member (admin or not) can [add](./group-chat.md#addpublic_keys) members, [send_message](./group-chat.md#send_messagemessage-reply_to_message_idnone), [get messages](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone) and [leave](./group-chat.md#leave).
## Group chat name
The **group chat name** is the human-readable name of the chat. It is set when [creating](./group-chat.md#createpublic_keys-name) the chat and can be updated through the [`name`](./group-chat.md#name) property.
Group chat names must follow the validation rules enforced by the library and expected by the Status application. A valid group chat name must satisfy all of the following conditions:
- It may contain **letters (`A–Z`, `a–z`)**
- It may contain **numbers (`0–9`)**
- It may contain **underscores (`_`)**
- It may contain **periods (`.`)**
- It may contain **hyphens (`-`)**
- It may contain **whitespaces (` `)**
- It must be **at least 1 character long**
- It **cannot be more than 30 characters long**
Characters such as punctuation, emojis, or other symbols are **not allowed**.
### Valid examples
```
Status Bots
status-bot.01
SNT_PUMP
dev.team-42
a
9000
```
### Invalid examples
| Example | Reason |
|-------|--------|
| | Too short (minimum length is 1) |
| `a-very-long-group-chat-name-42` + more | Too long (maximum length is 30) |
| `bot!123` | Contains invalid character `!` |
| `status 🚀` | Contains an emoji |
If a group chat name does not follow these rules, a custom exception will be raised.
**Note**: Unlike the [display name](./account.md#display-name), a group chat name **can** start or end with a whitespace.
## `GroupChat(account, chat_id=None)`
Create a new `GroupChat` instance. The constructor binds the group chat to a **logged-in** [`Account`](./account.md). If `chat_id` is not provided, an empty `GroupChat` is created and you must call [`create`](./group-chat.md#createpublic_keys-name) before the chat can be used.
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `account` | `Account` | Yes | A **logged-in** [`Account`](./account.md). If the account is not logged in, a custom exception is raised. |
| `chat_id` | `str` | No | The identifier of an existing group chat. Group chat IDs can be obtained from the [`chats`](./account.md#chats) property, where `type` is `group_chat`. If the chat does not exist, a `GroupChatNotFoundError` is raised. |
Prepare an empty `GroupChat` to create a new chat:
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
group_chat = GroupChat(account)
```
Wrap an existing group chat:
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
# This is under the assumption you are already in a group chat
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
print(group_chat.name)
```
## Methods
### `create(public_keys, name)`
Create a **new group chat** with the given members. The logged-in account becomes the [administrator](./group-chat.md#administrator) of the chat. **[Group chats can have up to 20 members.](https://status.app/help/messaging/create-a-group-chat)**
Each member can be identified in three different ways, so you can pass whichever value you have at hand - the public key, the chat key as shown in Status App, or the profile link a user shares with you:
| Format | Example | Where to find it |
|-------|--------|-----------------|
| **Public key** | `0x04ebcad...` | `public_key` in [`contacts`](./account.md#contacts) |
| **Chat key** (compressed key) | `zQ3shYSHp7...` | `compressed_key` in [`contacts`](./account.md#contacts), or the **chat key** in Status App |
| **Account URL** | `https://status.app/u/...` | `url` in [`contacts`](./account.md#contacts), or **Share profile** in Status App |
Every value is normalised into the public key with [`get_public_key`](./account.md#get_public_keyvalue), so the formats can be **mixed within the same list**.
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `public_keys` | `list[str]`
`str` | Yes | The **public keys** (`0x...`), **chat keys** (`zQ...`) or **account URLs** (`https://...`) of the members to create the chat with. A single value can be passed as a `str`. The members must be [mutual contacts](./account.md#contacts). |
| `name` | `str` | Yes | The name of the group chat. Must follow the [group chat name](./group-chat.md#group-chat-name) rules. |
Returns the current `GroupChat` instance, allowing method chaining.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
public_keys = [contact["public_key"] for contact in account.contacts.values() if contact["mutual"]]
group_chat = GroupChat(account).create(public_keys, "Status Bots")
print(group_chat.id)
```

Because the method returns the instance, calls can be chained:
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
public_keys = [contact["public_key"] for contact in account.contacts.values() if contact["mutual"]]
GroupChat(account).create(public_keys, "Status Bots").send_message("Hello!")
```
The formats can also be mixed:
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
members = [
"0x04ebcad...",
"zQ3shYSHp7...",
"https://status.app/u/..."
]
group_chat = GroupChat(account).create(members, "Status Bots")
```
**Note**: The account's **own public key** is automatically filtered out of `public_keys`, since the creator is always a member of the chat. This happens after the values are normalised, so it also works when your own account is passed as a chat key or account URL.
### `send_message(message, reply_to_message_id=None)`
Send a text message to the group chat. This method currently supports **text messages only**. A message can also be sent as a **reply** to an existing message in the chat, which renders in Status App with the original message quoted above it - the same as replying to a message in the app.
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `message` | `str` | Yes | The text message to send. |
| `reply_to_message_id` | `str` | No | The `id` of the message being replied to. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone). When omitted (default), the message is sent as a standalone message. |
Returns `str` - the `id` of the message that was just sent, delegated from [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone) on `Account`. It is the same identifier that appears under the `id` key in [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), so it can be passed straight into [`delete_message`](./group-chat.md#delete_messageid) or used as the `reply_to_message_id` of a follow-up message, without having to fetch the chat's messages first.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
first_id = group_chat.send_message("Hello from my Status bot #1!")
# Reply to the message that was just sent, without fetching the chat's messages
second_id = group_chat.send_message("Hello from my Status bot #2!", first_id)
```
Reply to a message:
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
# Messages are returned newest first, so this is the latest message in the chat
messages = group_chat.get_messages()
latest = messages[0]
group_chat.send_message("Thanks for the update!", latest["id"])
```
### `send_image(file_path, message=None, reply_to_message_id=None)`
Send an image to the group chat, with an optional text **caption**. The image renders inline in Status App, the same as attaching an image in the app. Like [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone), it can be sent as a **reply** to an existing message in the chat.
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `file_path` | `str` | Yes | Local full path to the image file. |
| `message` | `str` | No | Caption sent together with the image. |
| `reply_to_message_id` | `str` | No | The `id` of the message being replied to. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone). When omitted (default), the image is sent as a standalone message. |
Returns `str` - the `id` of the message that was just sent, delegated from [`send_image`](./account.md#send_imagechat_id-file_path-messagenone-reply_to_message_idnone) on `Account`. It is the same identifier that appears under the `id` key in [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), so it can be passed straight into [`delete_message`](./group-chat.md#delete_messageid) or used as the `reply_to_message_id` of a follow-up message.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
message_id = group_chat.send_image("./meme-67.png", "Daily random meme")
print(f"Sent image: {message_id}")
```
### `send_emoji_reaction(message_id, emoji_shortname)`
React to a message in the group chat with an emoji, the same as reacting to a message in Status App. The reaction is a **toggle** - calling the method again with the same emoji on the same message removes it, so the same call both sets and unsets the reaction.
Emojis are identified by their **shortname**, exactly as Status App names them (`:thumbsup:`, `:heart_eyes:`). The surrounding colons are optional - `thumbsup` and `:thumbsup:` are the same emoji - and the full list of supported shortnames is documented under [Emojis](./utils.md#emojis).
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `message_id` | `str` | Yes | The `id` of the message to react to. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), or directly from the return value of [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone) / [`send_image`](./group-chat.md#send_imagefile_path-messagenone-reply_to_message_idnone). |
| `emoji_shortname` | `str` | Yes | The emoji shortname as in Status App, with or without the surrounding colons. See [Emojis](./utils.md#emojis) for all supported values. |
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
# Messages are returned newest first, so this is the latest message in the chat
messages = group_chat.get_messages()
latest = messages[0]
group_chat.send_emoji_reaction(latest["id"], ":thumbsup:")
# Reacting with the same emoji again removes the reaction
group_chat.send_emoji_reaction(latest["id"], ":thumbsup:")
```
### `delete_message(id)`
Delete one of your **own** messages from the group chat. The deletion is propagated to the other members, so the message disappears for everybody. You can only delete messages that the logged-in account has sent.
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `id` | `str` | Yes | The `id` of the message to delete. Message IDs can be obtained from the `id` key of [`get_messages`](./group-chat.md#get_messagesstart_timestampnone-end_timestampnone), or directly from the return value of [`send_message`](./group-chat.md#send_messagemessage-reply_to_message_idnone). |
Returns `bool`.
| Value | Meaning |
|------|--------|
| `True` | The message was deleted. |
| `False` | The message was not deleted, because the account does not have permission to delete it. |
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
message_id = group_chat.send_message("Oops, this was a mistake!")
deleted = group_chat.delete_message(message_id)
print(f"Deleted: {deleted}")
```
### `get_messages(start_timestamp=None, end_timestamp=None)`
Retrieve messages from the group chat within an optional time range. Messages are returned in **descending order** (newest to oldest).
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `start_timestamp` | `str`
`datetime.date`
`datetime.datetime`
`pandas.Timestamp` | No | The earliest timestamp to include. Messages older than this value will stop the fetch process. |
| `end_timestamp` | `str`
`datetime.date`
`datetime.datetime`
`pandas.Timestamp` | No | The latest timestamp to include. Messages newer than this value will be skipped. |
Both timestamps accept the same values as [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone) on `Account`, so a range can be written as a plain `str` - for example `2026-08-11 22:57:51.134000`, `2026-08-11 22:57` or `2026-08-11`.
Returns `list[dict]` containing message objects. Timestamp fields returned by the backend are automatically converted into `datetime.datetime` objects.
```python
from status_sdk import Account, GroupChat
import datetime
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
messages = group_chat.get_messages(start_timestamp=datetime.datetime(2024, 1, 1))
for message in messages:
print(f"{message['timestamp']}\t{message['text']}")
```
**Note**: This is the group chat equivalent of [`delete_message`](./account.md#delete_messagemessage_id) on `Account`. The only difference is that it first verifies the group chat exists - a custom exception is raised if the chat has not been created or joined.
### `add(public_keys)`
Add members to the group chat.
Just like [`create`](./group-chat.md#createpublic_keys-name), each member can be identified by their **public key**, **chat key** or **account URL**, and the formats can be mixed within the same list.
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `public_keys` | `list[str]`
`str` | Yes | The **public keys** (`0x...`), **chat keys** (`zQ...`) or **account URLs** (`https://...`) of the members to add. A single value can be passed as a `str`. The members must be [mutual contacts](./account.md#contacts). |
Returns the current `GroupChat` instance, allowing method chaining.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
public_keys = [contact["public_key"] for contact in account.contacts.values() if contact["mutual"]]
group_chat.add(public_keys)
print(group_chat.members.keys())
```

---

### `remove(public_keys)`
Remove members from the group chat. **Only the [administrator](./group-chat.md#administrator) of the chat can remove members.**
Just like [`create`](./group-chat.md#createpublic_keys-name), each member can be identified by their **public key**, **chat key** or **account URL**, and the formats can be mixed within the same list. All three values are exposed in the [`members`](./group-chat.md#members) property as `public_key`, `compressed_key` and `url`.
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `public_keys` | `list[str]`
`str` | Yes | The **public keys** (`0x...`), **chat keys** (`zQ...`) or **account URLs** (`https://...`) of the members to remove. A single value can be passed as a `str`. The values must belong to current members of the chat, which can be obtained from the [`members`](./group-chat.md#members) property. |
Returns the current `GroupChat` instance, allowing method chaining.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
member = list(group_chat.members.values())[0]
group_chat.remove(member["public_key"])
```

**Alternative remove:**

---

### `leave()`
Leave the group chat.
Returns the current `GroupChat` instance, allowing method chaining.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
group_chat.leave()
```

**Note**: The `GroupChat` instance cannot be reused - accessing those properties raises a custom exception. To use it again, either [`create`](./group-chat.md#createpublic_keys-name) a new chat or ask somebody to add you in the chat.
## Properties
### `id`
The unique identifier of the group chat. This is the same value found in the [`chats`](./account.md#chats) property where `type` is `group_chat`, and it can be used directly with [`send_message`](./account.md#send_messagechat_id-message-reply_to_message_idnone) and [`get_messages`](./account.md#get_messageschat_id-start_timestampnone-end_timestampnone) on `Account`.
Returns `str`. Raises a custom exception if the chat has not been created or joined.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
print(group_chat.id)
```
### `name`
Get or update the **name** of the group chat. The name must follow the [group chat name](./group-chat.md#group-chat-name) rules.
Returns `str` when reading the property. Raises a custom exception if the chat has not been created or joined.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
# Get the current group chat name
print(group_chat.name)
```

You can update the name by assigning a new value:
```python
# Change the group chat name
group_chat.name = "Status Bots Electric Boogaloo"
print(group_chat.name)
```

---

### `members`
Get the current members of the group chat.
Returns `dict[str, dict]` where the key is the member's **public key**. This makes internal searching for member specific information faster. Raises a custom exception if the chat has not been created or joined.
| Key | Type | Description |
|----|----|-------------|
| `public_key` | `str` | Public key that uniquely identifies the member. |
| `url` | `str` | The URL that can be shared with other users. |
| `display_name` | `str` | The current display name of the member. |
| `compressed_key` | `str` | The member's compressed chat key as shown in Status App. |
| `admin` | `bool` | Whether the member is the [administrator](./group-chat.md#administrator) of the group chat. |
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
for member in group_chat.members.values():
print(member["display_name"], member["admin"])
```
### `available_slots`
The number of members that can still be [added](./group-chat.md#addpublic_keys) to the group chat.
Returns `int`. Raises a custom exception if the chat has not been created or joined.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
print(f"{group_chat.available_slots} slots left")
```
This is useful to check before adding members, since the group chat is full when there are no slots left:
```python
public_keys = [contact["public_key"] for contact in account.contacts.values() if contact["mutual"]]
if group_chat.available_slots >= len(public_keys):
group_chat.add(public_keys)
```
### `is_admin`
Whether the logged-in [`Account`](./account.md) is the [administrator](./group-chat.md#administrator) of the group chat.
Returns `bool`.
```python
from status_sdk import Account, GroupChat
account = Account()
params = {
"name": "status-app-bot",
"password": "SNTPUMP"
}
account.login(**params)
chat = [chat for chat in account.chats if chat["type"] == "group_chat"][0]
group_chat = GroupChat(account, chat["id"])
if group_chat.is_admin:
print("Account is admin!")
```