Groups
A group is a conversation with more than two people in it. It is listed, created and changed here, and messaged with the ordinary send endpoint.
List groups
This is the conversation list filtered to groups, so the short-page rule applies to it: the filter runs after the page comes back, so a page can be short or empty while cursor is still non-null. Keep paging until the cursor is null. A Group is a Chat with kind: "group" plus one field, pictureUrl.
A single group read carries members. Asking for a one-to-one conversation on a groups route is 409 CONFLICT ("that conversation is not a group"), not a 404 - the conversation exists, it is just not a group.
Create a group
curl -X POST https://api.sendveo.com/v1/groups \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "subject": "Deliveries north",
"members": ["+212670111222", "+212671333444"],
"text": "Welcome to the deliveries group." }'{
"group": {
"id": "cht_m8k3x7q2a1b4",
"chatId": "120363001122334455@g.us",
"channelId": "chn_sales01",
"kind": "group",
"name": "Deliveries north",
"pictureUrl": "https://api.sendveo.com/v1/groups/120363001122334455%40g.us/picture",
"members": [
{ "id": "cmb_1", "phone": "+212661234567", "name": null, "role": "super_admin", "self": true },
{ "id": "cmb_2", "phone": "+212670111222", "name": "Khadija Alaoui", "role": "member", "self": false }
]
}
}Every member is checked before the group is created. WhatsApp does not refuse an unreachable member, it silently leaves them out - so a group would come back one person short with no explanation. Sendveo looks each one up first and refuses the whole create with 400 VALIDATION_FAILED, details[0].path = "members.N", naming the number. Nothing is created. The same check runs when adding a member, with the path phone.
A group is created by starting a conversation in it, so text becomes its first message. Omit it and the group is created with nothing said. At most 50 members per call, canonicalised and de-duplicated; a malformed one is 400 naming its index.
Members
A member carries role: member, admin or super_admin. It is member unless WhatsApp discloses an admin marker, and its published attendee shape carries none - so treat a non-member role as reliable and member as "we were not told otherwise". Do not build a permission check on it: WhatsApp refuses an action the number may not perform regardless, and a check built on the role would hide the action from people who can do it.
leftAt is set rather than the row deleted. A message from somebody who left the group last week still has to resolve to a name.
Removing your OWN number is refused with 409 CONFLICT: "to remove your own number from a group, leave the group instead". A script walking a member list would otherwise leave the group by accident.
Leaving
Leaving uses the number verified at connect time, never one from the request. A channel whose number was never read back cannot leave, and answers 409 CONFLICT.
{ "left": true, "chatId": "cht_m8k3x7q2a1b4" }Sending to a group
Sending to a group is POST /v1/messages with the group chatId. It is an ordinary chat id and needs no new endpoint and no new scope - messages:send, as always. Reactions, forwards, replies, attachments and the read and typing actions all work in a group exactly as they do in a one-to-one conversation.
curl -X POST https://api.sendveo.com/v1/messages \
-H "X-API-KEY: sv_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "channelId": "chn_sales01",
"chatId": "120363001122334455@g.us",
"text": "The gate code is unchanged." }'Group messages
Two fields are added to every Message, and to the message.received webhook. Both are additive and both are null on a one-to-one message, which is what a handler written before groups existed keeps seeing.
{
"id": "msg_8Rm5",
"channelId": "chn_sales01",
"chatId": "120363001122334455@g.us",
"direction": "INBOUND",
"from": "+212671333444",
"senderName": "Omar Idrissi",
"text": "The Rabat round leaves at 8am tomorrow.",
"type": "text",
"status": "RECEIVED",
"group": { "chatId": "120363001122334455@g.us", "id": "cht_m8k3x7q2a1b4", "name": "Atlas team - deliveries" },
"author": { "phone": "+212671333444", "name": "Omar Idrissi" }
}group identifies the conversation: group.id is the Sendveo chat id when Sendveo holds the row and null otherwise, while group.chatId and group.name are always meaningful. author is WHO said it inside the group. It is null on a one-to-one message, because there the sender IS the conversation and from already says so, and null on an outbound group message, which was said by the connected number. from keeps its V1 meaning and carries the author on an inbound group message, so a handler that only knows from still reads a sender.
Group pictures
A group pictureUrl is always present, and a group with no picture answers 404 there. That is one request rather than an extra upstream call on every group in a list. It is the same proxy the contact picture uses, with the same rules: chosen content type, nothing stored, 2 MB cap.
Group events
Three events are relayed to your webhook. They are about a conversation, so they carry chat where the message events carry message, plus by. Being removed and leaving are one event with member.removed telling them apart; a group being created and a group being renamed are one event too.
| Event | Fired when | Extra fields |
|---|---|---|
| group.member_joined | Somebody was added to a group. | member, by |
| group.member_left | Somebody left, or was removed. | member.removed, by |
| group.updated | A group was created, or its subject changed. | subject, by |
Scopes
Reading groups and changing them are separate authorities. *:read grants groups:read like every other read scope. A key issued before these scopes existed does not carry them and answers 403; issue a new key.
| Scope | Grants |
|---|---|
| groups:read | List and read groups, and group pictures. |
| groups:manage | Create a group, add and remove members, leave. |