Groups
Read the WhatsApp groups your numbers are in, and post a message to one.
Groups
A group is a WhatsApp group one of your connected numbers belongs to. You can read the list, refresh it from the phone, and post a message into a group.
Everything else about a group happens on the phone. Groups are joined, renamed, left and administered there, so this API has no endpoint that creates, edits or deletes one — it would be describing something it cannot do.
Members are counted, never stored
This is the most important thing to know before you build against it.
A group carries the phone numbers of everyone in it, and almost none of those
people ever contacted your workspace or agreed to be on file with us. So when
the group list is read, the members are counted and the identities are thrown
away. participant_count is a number, and there is no endpoint that returns the
people behind it, because there is no table that holds them.
Two consequences worth designing around:
- Group members never become contacts. Messaging a group does not add 241
people to your audience, and they will not appear in
GET /contacts. - Groups are excluded from campaigns. A campaign audience is built from contacts with recorded consent. A group has neither.
List your groups
GET /api/v1/groups
Returns the groups your connected numbers belong to, largest first, in pages.
| Query | Default | Notes |
|---|---|---|
search |
— | Match on group name, case-insensitive |
page |
1 |
Which page to return |
per_page |
25 |
Page size, 1–100 |
{
"data": [
{
"id": "5926b085-8cb7-4e18-9c69-c94c783049b6",
"name": "Wholesale Traders PK",
"topic": "Bulk orders and stock updates",
"participant_count": 241,
"is_admin": true,
"is_announce": false,
"can_send": true,
"connection": {
"id": "9f2b1c7e-53a8-4a2e-b8f1-2d6c0a4e77b3",
"display_name": "Sales line"
},
"created_at": "2026-03-04T10:00:00+00:00",
"synced_at": "2026-07-23T09:44:48+00:00"
}
],
"links": { "first": "…", "last": "…", "prev": null, "next": null },
"meta": { "current_page": 1, "per_page": 25, "total": 9 }
}
| Field | Notes |
|---|---|
id |
The group's UUID here. Use it as {id} when you post a message |
name |
As it is on the phone. null for an unnamed group |
participant_count |
How many people are in it. A count only — see above |
is_admin |
Whether your number is an admin of the group |
is_announce |
Whether the group only lets admins post |
can_send |
Whether you may post — read this rather than deriving it |
connection |
Which of your numbers this group belongs to |
created_at |
When the group was created, per the phone. null if unknown |
synced_at |
When this row was last refreshed from the phone |
Read can_send, do not derive it
can_send is false when the group only accepts posts from admins and your
number is not one. Work it out yourself from is_announce and is_admin and
your code will need changing the day another reason to refuse is added.
Refresh from the phone
POST /api/v1/groups/sync
The stored list is a mirror. Someone adds your number to a group, or removes it, and nothing tells us — call this to re-read it.
It runs across every connected number, because this API deliberately exposes no connection ids. Each number is attempted on its own, so one being offline does not cost you the others.
{
"data": {
"numbers": [
{ "number": "Sales line", "refreshed": true, "groups": 9, "removed": 1, "error": null },
{ "number": "Orders line", "refreshed": false, "groups": 0, "removed": 0, "error": "connection_not_ready" }
],
"groups": 9
}
}
| Field | Notes |
|---|---|
numbers[].refreshed |
Whether that number's list was re-read |
numbers[].groups |
How many groups it reported |
numbers[].removed |
Groups it is no longer in, now deleted here |
numbers[].error |
connection_not_ready when the number is offline, else null |
groups |
Total groups in the workspace after the refresh |
The response is 200 even when every number failed. The per-number results say
what happened, which is more use than one opaque error — and a number being
offline is not a fault in your request.
A group you have left is removed, along with the messages you sent to it. It is not kept as a row that has quietly stopped being true.
Running this twice is the same as running it once, so it needs no
Idempotency-Key.
Message a group
POST /api/v1/groups/{id}/messages
{
"body": "New stock has arrived. Orders open until 6pm."
}
| Field | Required | Notes |
|---|---|---|
body |
yes | Up to 4096 characters. Text only — attachments are not offered |
Use the id from GET /groups. Every member of the group receives it.
Response
HTTP/1.1 202 Accepted
{
"data": {
"id": "4275ce52-0b1d-4a90-8d3f-19a7c0b2e5f1",
"group_id": "5926b085-8cb7-4e18-9c69-c94c783049b6",
"status": "queued",
"failure_code": null,
"sent_at": null,
"failed_at": null,
"created_at": "2026-07-23T09:48:30+00:00"
}
}
The status ends at sent
queued → sending → sent, or failed. That is the whole lifecycle, and it
stops there on purpose.
WhatsApp acknowledges a group message once, but then returns a delivery and a
read receipt for every member who gets it. There is no single "the group
received it" or "the group read it" — one person opening the message would
otherwise mark it read for five hundred. So no delivered or read state is
reported for a group, and none should be inferred.
A 202 means the message was queued and handed to your number. As with a direct
send, it is not proof that WhatsApp took it; status is.
One message, whatever the group size
A group send costs one message from the same plan allowance as any other send, whether the group has 6 members or 600.
When it is refused
Both of these are refused before anything is queued, because WhatsApp would otherwise accept the message and drop it without telling anyone:
HTTP/1.1 422 Unprocessable Content
{ "message": "Only admins can post to this group." }
HTTP/1.1 422 Unprocessable Content
{ "message": "The number this group belongs to is not connected." }
An unknown or foreign group id gives:
HTTP/1.1 404 Not Found
{ "message": "The group was not found. Use the id from GET /groups." }
And an exhausted plan allowance gives 403 with the same wording as any other
capacity refusal. See Errors and idempotency.
Retrying safely
Send an Idempotency-Key and a retry after a timeout replays the first response
instead of posting to the group a second time. Nothing about a group send can be
un-sent, which makes the key worth more here than almost anywhere else in this
API — a duplicated message reaches every member again.
Scopes
GET /groups and POST /groups/sync need groups:read.
POST /groups/{id}/messages needs groups:send.
groups:send is separate from messages:write deliberately. One call to a group
reaches every member at once, none of whom opted in to anything with you — a key
issued to send order confirmations should not also be able to post to a group of
five hundred. Scopes are fixed when the key is issued, so a key created before
these existed will keep returning 403 until you create a new
one.
What this surface does not include
No creating, renaming, leaving or deleting groups — those are phone operations. No participant list, for the reason at the top of this page. No attachments to a group. And no inbound group messages: what other people post in a group is not received by MessageVia, so it does not reach your webhooks and does not appear in your inbox. Only what you send is recorded.