Pular para o conteúdo
Start free

WhatsApp Groups

Este conteúdo não está disponível em sua língua ainda.

Manage Meta WhatsApp groups through wabery.groups, REST, CLI, or MCP. A group_id is opaque. It is not a phone number; never pass it as to.

Meta currently makes Groups API available only when all of these are true:

  • The business has an Official Business Account (OBA).
  • The WhatsApp number is a Cloud API number, not a WhatsApp Business app number or a number onboarding to Multi-Solution Conversations.
  • A group has at most eight participants, excluding the business account.
  • A business number can manage at most 10,000 groups.
  • Only one Cloud API business can be connected to a group.

Groups support text, media attachments (image, document, video, audio, and sticker), approved templates whose components are text or media, and pin/unpin messages. Meta does not support location, reaction, contacts, commerce, interactive, calls, ephemeral, view-once, authentication, edit, or delete message operations in groups. See the official Groups, Group management, and Group messaging references.

The TypeScript SDK maps the public REST endpoints to a group namespace:

const groups = await wabery.groups.list({ channelId: "channel_..." });
const creation = await wabery.groups.create({
channelId: "channel_...",
idempotencyKey: "create-support-group-2026-08-22",
subject: "Customer support",
description: "Support community",
joinApprovalMode: "approval_required",
});
// Meta completes create/update/delete asynchronously. Read group_id from the
// group.lifecycle webhook, then use it for management calls.
const groupId = "group_opaque_7f3a92";
await wabery.groups.updateSettings("channel_...", groupId, {
subject: "Priority support",
});
await wabery.groups.updateProfilePicture("channel_...", groupId, {
profilePicture: new Blob([jpegBytes], { type: "image/jpeg" }),
filename: "support-group.jpg",
});
const current = await wabery.groups.get("channel_...", groupId);
const invite = await wabery.groups.getInviteLink("channel_...", groupId);
const replacement = await wabery.groups.resetInviteLink("channel_...", groupId);

The REST equivalents are:

Operation Method Endpoint
List GET /v1/channels/{channel_id}/whatsapp-groups
Create POST /v1/channels/{channel_id}/whatsapp-groups
Get GET /v1/channels/{channel_id}/whatsapp-groups/{group_id}
Delete DELETE /v1/channels/{channel_id}/whatsapp-groups/{group_id}
Update settings POST /v1/channels/{channel_id}/whatsapp-groups/{group_id}
Read invite link GET /v1/channels/{channel_id}/whatsapp-groups/{group_id}/invite-link
Reset invite link POST /v1/channels/{channel_id}/whatsapp-groups/{group_id}/invite-link
List join requests GET /v1/channels/{channel_id}/whatsapp-groups/{group_id}/join-requests
Approve requests POST /v1/channels/{channel_id}/whatsapp-groups/{group_id}/join-requests
Reject requests DELETE /v1/channels/{channel_id}/whatsapp-groups/{group_id}/join-requests
Remove participants DELETE /v1/channels/{channel_id}/whatsapp-groups/{group_id}/participants

At the provider level, Wabery uses the WhatsApp Graph API group collection under the connected phone-number scope for listing and creation, the opaque group-id scope for group settings, invite links, join requests, and participants, and the phone-number messages scope for group sends. The public REST paths above are the stable Wabery contract; callers do not need to call Graph directly.

The groups list and join-request list use cursor pagination. limit defaults to 25 and accepts 1–1024. Use starting_after for the next forward page and ending_before for a previous page. after and before are accepted aliases for those parameters. In the SDK, use startingAfter and endingBefore; all cursor values are opaque strings. Never construct a cursor from a group ID, join-request ID, or the last item in a page. Follow next_cursor first, or paging.cursors.after when that is the available provider cursor.

const first = await wabery.groups.list({ channelId: "channel_...", limit: 25 });
const next = await wabery.groups.list({
channelId: "channel_...",
limit: 25,
startingAfter: first.next_cursor ?? undefined,
});

REST query example:

GET /v1/channels/{channel_id}/whatsapp-groups?limit=25&starting_after=<opaque-cursor>
GET /v1/channels/{channel_id}/whatsapp-groups/{group_id}/join-requests?ending_before=<opaque-cursor>

Create a group with subject (maximum 128 characters), optional description (maximum 2048), and either auto_approve or approval_required:

Every create request requires a caller-generated Idempotency-Key (SDK: idempotencyKey). Generate it once for the logical creation and reuse it for all retries. Wabery persists the accepted operation in its API ledger, so a lost response cannot cause a second Meta group.

{
"subject": "Customer support",
"description": "Support community",
"join_approval_mode": "approval_required"
}

REST callers send the key as a header:

Terminal window
curl -X POST "https://api.wabery.com/v1/channels/channel_.../whatsapp-groups" \
-H "Authorization: Bearer wab_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-support-group-2026-08-22" \
-d '{"subject":"Customer support","join_approval_mode":"approval_required"}'

Create, update, and delete return HTTP 202 with object: "whatsapp_group_operation" and status: "pending". These are acceptances, not completed mutations: Meta performs the operation later and Wabery does not turn the response into a synchronous group snapshot. Handle group.lifecycle for create/delete and group.settings for subject, description, or profile-picture changes. A successful create event contains the authoritative new group_id and invite link; use that ID only after receiving the event. A failed event contains the provider error data. Do not assume an accepted operation has already completed or that a follow-up read is authoritative before the corresponding webhook arrives.

Create responses that are still processing

Section titled “Create responses that are still processing”

Creation can also return a WhatsAppGroupCreateProcessingResponse while the same idempotent request is being reconciled:

{
"object": "api_operation",
"status": "processing",
"idempotency_key": "create-support-group-2026-08-22",
"reconciliation": "provider"
}

reconciliation: "provider" means Wabery has already claimed or started the Meta create call and is waiting for provider evidence. local_persistence means the durable Wabery acceptance is still being completed before provider work can safely be retried. Neither response contains a group_id, and neither means that a group snapshot is ready. Retry the identical request with the same Idempotency-Key (or SDK idempotencyKey) and branch on object and status; never create a second group with a new key. Acceptance-unknown is not a processing response. If a later retry returns HTTP 409 with error: "whatsapp_group_create_acceptance_unknown", the SDK raises WaberyConflictError with that code. Verify whether Meta created the group in WhatsApp Manager before retrying with a new logical key, exactly as required by the API contract.

Profile-picture updates use multipart/form-data with the public profile_picture_file field. It must be JPEG only, no larger than 5 MiB, and square with minimum dimensions 192 × 192. Wabery maps profile_picture_file to Meta’s file upload field. Meta validates the aspect ratio and dimensions; Wabery preserves its 131209 and 131210 errors.

Group responses include suspended and total_participant_count. The latter does not include the business account.

Wabery returns provider failures as structured { error, message, meta_code, fbtrace_id } responses. Common behavior is HTTP 400 for invalid cursors, unsupported group message types, invalid participants, or invalid join-request IDs; 403 for a suspended group; 409 when the channel is not ready; 429 for provider rate limits; and 502 when Meta cannot be reached or returns an unusable response. Bulk management can return HTTP 206 with per-item failures. Treat HTTP 202 operation responses as pending and wait for the corresponding webhook before declaring a lifecycle or settings mutation successful.

Each normalized join request preserves Meta’s official join_request_id, wa_id, and Unix-seconds creation_timestamp. REST also returns stable id and ISO created_at; the SDK exposes id, joinRequestId, creationTimestamp, and createdAt while retaining the raw snake-case aliases. Join request approval and rejection accept bulk string IDs. Participant removal uses Meta’s required { user } objects:

const pending = await wabery.groups.listJoinRequests(
"channel_...",
"group_...",
{ limit: 25, startingAfter: "<opaque-cursor>" },
);
const ids = pending.join_requests.map((request) => request.id);
await wabery.groups.approveJoinRequests("channel_...", "group_...", {
joinRequestIds: ids,
});
await wabery.groups.rejectJoinRequests("channel_...", "group_...", {
joinRequestIds: ["join_request_..."],
});
await wabery.groups.removeParticipants("channel_...", "group_...", {
participants: [{ user: "15551234567" }],
});

The provider can partially fail a bulk operation. Inspect failed_join_requests or failed_participants instead of assuming every item succeeded. Reads and successful management mutations return HTTP 200; a partial provider result is surfaced as HTTP 206 with per-item failures. An invalid or expired cursor is a provider error, not a signal to substitute the last item ID.

Use groupId, never to or conversationId:

The exact supported content choices are text, media attachments (image, document, video, audio, or sticker), approved text/media templates, and pin or unpin. The SDK uses expirationDays; REST and the Meta wire payload use pin.expiration_days. The currently enforced pin duration bounds are 1–30 days.

await wabery.messages.send({
channelId: "channel_...",
groupId: "group_opaque_7f3a92",
idempotencyKey: "support-status-2026-08-22",
text: "The support team is online.",
});
await wabery.messages.send({
channelId: "channel_...",
groupId: "group_opaque_7f3a92",
idempotencyKey: "pin-support-status-2026-08-22",
pin: { type: "pin", messageId: "wamid....", expirationDays: 7 },
});
await wabery.messages.send({
channelId: "channel_...",
groupId: "group_opaque_7f3a92",
idempotencyKey: "unpin-support-status-2026-08-22",
pin: { type: "unpin", messageId: "wamid...." },
});
await wabery.messages.send({
channelId: "channel_...",
groupId: "group_opaque_7f3a92",
idempotencyKey: "sticker-support-status-2026-08-22",
sticker: { link: "https://example.com/sticker.webp" },
});

The REST pin request is:

{
"channel_id": "channel_...",
"group_id": "group_opaque_7f3a92",
"idempotency_key": "support-status-2026-08-22",
"pin": {
"type": "pin",
"message_id": "wamid....",
"expiration_days": 7
}
}

Wabery forwards the pin.expiration_days property to Meta unchanged.

Terminal window
wabery messages send \
--channel-id channel_... \
--group-id 'group_opaque_7f3a92' \
--text 'The support team is online.' \
--idempotency-key 'support-status-2026-08-22'
{
"channel_id": "channel_...",
"group_id": "group_opaque_7f3a92",
"idempotency_key": "support-status-2026-08-22",
"text": "The support team is online."
}

The REST Idempotency-Key header is also accepted for group sends. Keep the same key for every retry of the same logical send; do not generate a new key after a processing response.

group_id, conversation_id, and to are mutually exclusive. Do not send location, reaction, contacts, commerce, interactive, calls, ephemeral, view-once, authentication, edit, or delete content/operations to a group.

Wabery preserves the group identity and sender participant on message.received:

{
"event": "message.received",
"payload": {
"channel_id": "channel_...",
"conversation_id": "conversation_group_...",
"group_id": "group_opaque_7f3a92",
"sender_participant": "15551234567",
"type": "text",
"messages": [
{
"id": "wamid....",
"type": "text",
"group_id": "group_opaque_7f3a92",
"sender_participant": "15551234567",
"text": "Can you help?"
}
]
}
}

Do not route a group event by from alone. Use group_id for the thread and sender_participant for the author.

Existing conversation APIs stay individual-only by default. Opt into the group shape explicitly; this keeps older integrations’ response types and pagination stable:

const conversations = await wabery.conversations.listGroups();
const conversation = await wabery.conversations.getGroup("conversation_...");
const messages = await wabery.conversations.listGroupMessages("conversation_...");

The REST equivalent is recipient_type=group on GET /conversations, GET /conversations/{id}, and GET /conversations/{id}/messages. The list also accepts recipient_type=all when a mixed feed is intentionally desired.

Commerce and order messages are not available in groups. For individual WhatsApp inbound messages, Wabery preserves the official order, referral, ctwa_clid, referred_product, and identity_key_hash fields rather than discarding them. Do not infer that preservation makes those message types available for group sends.

Group delivery statuses identify the group with recipient_id and recipient_type: "group". Meta sends participant delivery updates as separate entries in the top-level statuses array. Depending on the event, an entry uses either recipient_participant_id or participant_recipient_id; treat those fields as alternatives. Wabery preserves the exact field received and also accepts nested participant_statuses defensively for forward compatibility:

{
"event": "message.status",
"payload": {
"message_id": "message_...",
"recipient_id": "group_opaque_7f3a92",
"recipient_type": "group",
"status": "delivered",
"recipient_participant_id": "15551234567"
}
}

Another entry may contain participant_recipient_id instead of recipient_participant_id; it does not contain both merely to describe the same participant.

Subscribe to Meta’s group_lifecycle_update, group_participants_update, group_settings_update, and group_status_update fields in accordance with the official Groups webhook reference. Wabery forwards these as the typed SDK events group.lifecycle, group.participants, group.settings, and group.status; the original Meta field payload remains available at payload.data.

Every management delivery is signed like other Wabery webhooks and has this stable outer shape. payload.data is the original Meta groups[] item, so keep it when new provider fields appear:

{
"event": "group.lifecycle",
"api_version": "2026-06-18",
"payload": {
"object": "whatsapp_group_event",
"channel_id": "channel_...",
"group_id": "group_opaque_7f3a92",
"field": "group_lifecycle_update",
"type": "group_create",
"request_id": "meta-request-...",
"occurred_at": "2026-08-22T12:00:00.000Z",
"data": {
"group_id": "group_opaque_7f3a92",
"type": "group_create",
"timestamp": "1785320000",
"request_id": "meta-request-...",
"subject": "Customer support",
"invite_link": "https://chat.whatsapp.com/example"
}
},
"sentAt": "2026-08-22T12:00:01.000Z"
}

The data.timestamp value is a Unix timestamp from Meta; the normalized occurred_at is ISO-8601 when Meta supplies a timestamp and is null when Meta omits it; consumers must tolerate both values. For participant changes, the useful data variant is:

{
"group_id": "group_opaque_7f3a92",
"type": "group_participants_add",
"timestamp": "1785320000",
"added_participants": [{ "wa_id": "15551234567" }]
}

For settings changes, apply only fields whose update_successful is true:

{
"group_id": "group_opaque_7f3a92",
"type": "group_settings_update",
"group_subject": { "text": "Priority support", "update_successful": true },
"group_description": { "text": "Escalations", "update_successful": true },
"profile_picture": { "mime_type": "image/jpeg", "update_successful": true }
}

For status changes, type identifies the transition:

{
"group_id": "group_opaque_7f3a92",
"type": "group_suspend",
"timestamp": "1785320000"
}

For group_join_request_created and group_join_request_revoked, the data item contains join_request_id and wa_id. Bulk operations can contain both successful entries and errors. A top-level data.errors array is aggregate diagnostic information: it does not invalidate successful participant entries or successful settings fields. Apply only participant entries that do not have their own errors, and only settings fields whose update_successful is true. Correlate operations with group_id and request_id, deduplicate retries, and wait for a successful group_create event before storing the authoritative group ID or invite link. For settings, participants, and status, update local state from the webhook rather than treating the 202 operation response as final.

Wabery’s external project webhooks also require an explicit opt-in. This avoids sending unknown event names to an existing customer endpoint after an upgrade:

await wabery.projects.update("project_...", {
groupWebhookEventsEnabled: true,
});
const event = wabery.webhooks.constructEventWithGroups(
rawBody,
signature,
webhookSecret,
);

With REST, PATCH /v1/projects/{id} using { "group_webhook_events_enabled": true }. Group message.received and message.status deliveries remain part of messaging; this switch controls the four new group management event names.

Wabery configures the required provider event delivery when you connect an eligible channel. You do not need to add group webhook fields in Meta. The project setting above only controls whether Wabery forwards the four group management event names to your endpoint.

Group conversations appear in the inbox and external-routing projects receive their messages through message.received. Wabery intentionally does not run private-contact AI automations, Flows, recovery retries, or handoff operations against a group conversation because those paths require an individual contact. Reply manually in the inbox or from an external webhook using groupId.

The CLI exposes groups list, get, create, delete, settings (including --profile-picture), invite-link, join-requests, and remove-participants. MCP exposes matching read tools plus confirmed mutation tools, wabery_update_group_profile_picture, and wabery_send_group_message. Any MCP client advertising elicitation uses client-controlled confirmation, including local clients. A local client without elicitation falls back to the confirmation_token policy in explicit write mode.