wats.sh
Reference

Groups API

The WhatsApp Groups API surface: endpoint contracts, limits, webhook outcomes, and service opt-in routes.

implemented credential-free; live validation pending

Capture the group request shapes in the playground.

WATS Groups is an official WhatsApp Cloud API v25 surface, exposed as an opt-in, composable family: import the Groups subpath, construct a scoped client, or pass enableGroupRoutes: true to the service. Non-group users get no new default service routes.

Groups hang off the business phone-number id, not the WABA id. Public WATS names stay camelCase; Meta snake_case is emitted only at the Graph wire boundary.

Live validation status

listGroups is live-validated against a real WhatsApp Business number. Group mutations — create, update, delete, participant removal, join-request approve/reject — are shape-only. A live createGroup attempt was rejected with an entitlement error on the test number; mutations stay shape-only until a Groups-entitled phone number exists. See the parity matrix Groups row.

Endpoint contracts

OperationWATS helperWire method/pathNotes
Create groupcreateGroup, PhoneNumberClient.createGroup, WhatsApp.createGroupPOST /{phoneNumberId}/groupsAsync mutation; response is a request_id, not the new group id.
List groupslistGroups, PhoneNumberClient.listGroupsGET /{phoneNumberId}/groupsSupports limit, after, and before.
Get groupgetGroup, GroupClient.getInfoGET /{groupId}Optional fields query.
Update groupupdateGroup, GroupClient.updatePOST /{groupId}Subject, description, or join-approval settings.
Delete groupdeleteGroup, GroupClient.deleteDELETE /{groupId}Async mutation.
Get invite linkgetGroupInviteLink, GroupClient.getInviteLinkGET /{groupId}/invite_linkReturns current invite URL.
Reset invite linkresetGroupInviteLink, GroupClient.resetInviteLinkPOST /{groupId}/invite_linkReset invalidates the previous invite link. It is not a DELETE.
Remove participantsremoveGroupParticipants, GroupClient.removeParticipantsDELETE /{groupId}/participantsBody carries up to 8 participants. It is not a POST.
List join requestslistGroupJoinRequests, GroupClient.getJoinRequestsGET /{groupId}/join_requestsPending invite-link join requests.
Approve join requestsapproveGroupJoinRequests, GroupClient.approveJoinRequestsPOST /{groupId}/join_requestsBulk approve.
Reject join requestsrejectGroupJoinRequests, GroupClient.rejectJoinRequestsDELETE /{groupId}/join_requestsReject is DELETE, not POST.
Send to groupPhoneNumberClient.sendText, WhatsApp.sendGroupMessage, message buildersPOST /{phoneNumberId}/messagesEmits recipient_type: "group" and to: groupId.
Pin/unpinbuildSendPinPayloadPOST /{phoneNumberId}/messagesGroups pin/unpin body; Meta allows up to 3 pinned messages.

Limits and gotchas

  • The business is the sole admin. No direct participant-add endpoint exists and there is no admin promote/demote endpoint.
  • Joining is invite-link only. With approval required, approve or reject join requests from the join-request queue.
  • Max 8 participants, excluding the business. Max 10,000 groups per business phone number.
  • subject <=128; description <=2048.
  • Create/update/delete/remove/approve/reject are asynchronous. The HTTP response carries request_id; terminal success or failure arrives later via a group webhook.
  • The group id and first invite link for a newly created group arrive in group_lifecycle_update, not in the create HTTP response.
  • A suspended group is reported through group_status_update (group_suspend / group_suspend_cleared). Treat suspended groups as unusable until Meta clears the suspension.

SDK usage

import { GraphClient, PhoneNumberClient } from "@wats/graph";
import { createFetchTransport } from "@wats/graph";

const graphClient = new GraphClient({
  accessToken: process.env.WATS_ACCESS_TOKEN!,
  apiVersion: "v25.0",
  transport: createFetchTransport()
});

const phone = new PhoneNumberClient({
  graphClient,
  phoneNumberId: process.env.WATS_PHONE_NUMBER_ID!
});

const create = await phone.createGroup({
  subject: "Operators",
  description: "Launch coordination",
  joinApprovalMode: "approval_required"
});
console.log(create.request_id);

// Wait for group_lifecycle_update to learn the group id.
const group = phone.group("GROUP_ID_FROM_WEBHOOK");
const invite = await group.getInviteLink();
console.log(invite.invite_link);
await group.approveJoinRequests({ joinRequestIds: ["JOIN_REQUEST_ID"] });
await phone.sendText({ to: "GROUP_ID_FROM_WEBHOOK", recipientType: "group", text: "Welcome" });

Direct callables remain available from @wats/graph/endpoints/groups when you do not want a scoped client.

Webhooks

WATS normalizes the four group webhook fields through normalizeWebhookEnvelope. Meta sends each lifecycle/settings/status/participant entry under value.groups[]; WATS emits one typed update per group item:

  • group_lifecycle_update -> kind: "groupLifecycle"
  • group_participants_update -> kind: "groupParticipants"
  • group_settings_update -> kind: "groupSettings"
  • group_status_update -> kind: "groupStatus"

Inbound group messages carry message.groupId. Group message statuses keep recipientType: "group" and recipientParticipantId when Meta sends participant aggregation details. filtersTyped.group matches group messages, group status aggregation, and the four group update kinds. WhatsApp.listen({ groupId }) narrows message/status listeners to one group.

Service routes

@wats/service exposes Groups routes only when enableGroupRoutes: true is passed to createWatsServiceApp / OpenAPI generation:

  • GET|POST /groups
  • GET|POST|DELETE /groups/{groupId}
  • GET|POST /groups/{groupId}/invite-link
  • DELETE /groups/{groupId}/participants
  • GET|POST|DELETE /groups/{groupId}/join-requests

These routes use the configured business phone-number id. They require service bearer auth, forward only the Graph access token to Graph, and use the same sanitized graph_request_failed envelope as message routes for Meta errors.

Error semantics

WATS validation failures reject before transport with GraphRequestValidationError: empty ids, unsafe path segments, over-limit subjects/descriptions, too many participants, bad join-request arrays, unsupported group message types, and phone-number-shaped ids used as group recipients. Meta errors remain GraphApiError subclasses in SDK code and graph_request_failed service envelopes at the service boundary.

The public API is camelCase for WATS-authored inputs and normalized updates: phoneNumberId, groupId, joinApprovalMode, joinRequestIds, addedParticipants, recipientType. Raw Graph response envelopes still expose Meta fields such as request_id and invite_link; use normalizeWebhookEnvelope(...) for camelCase webhook evidence. Wire snapshots and raw webhook fields preserve Meta snake_case only inside raw/rawChange evidence.

On this page