Messaging

Create and manage WhatsApp Business groups

WhatsApp Business groups are groups a business creates on its own Cloud API number through Meta's Groups API. They are not personal WhatsApp groups: Tyxter does not connect to, read, join, import or mirror groups created in the WhatsApp or WhatsApp Business app.

Read WhatsApp Business groups are not personal WhatsApp groups first if you were asked to connect existing groups, add people by phone number or put more than eight people in one group: that page explains why none of those works with WhatsApp Business groups.

Beta. This guide walks one group through its lifecycle, sandbox first and production second. It explains how each step works, not whether it is available through Tyxter today: the WhatsApp Business groups API reference is the one place where availability is kept current, and it has every field, status and error.

Facts marked (heard) come from Meta's Groups API documentation and have not yet been observed on a live group through Tyxter. Facts marked (assumed) are Tyxter's reading where Meta's documentation is silent.

Before you start

  • An API key with the scopes the steps use: groups:read for the eligibility read and the group reads (steps 1, 3, 5, 6 and 7); groups:write for the eligibility check, the create, the sandbox participant simulation, the invite-link reset and the delete (steps 1, 2, 5, 6 and 7); messages:send for the invite message with POST /v1/messages (step 4); and, to receive the group events (next item), webhooks:write to update an existing endpoint's subscriptions or webhooks:read for the listen route. Finding the phone number id (next item) takes phone_numbers:read for GET /v1/phone-numbers, or sandbox:write for GET /v1/sandbox/quickstart. A tx_sandbox_ key works against sandbox and a tx_live_ key against production; the calls are the same, except the sandbox-only participant simulation in step 5.
  • A Tyxter phone number. Every groups call that takes phone_number_id expects the Tyxter phone number id (the id on GET /v1/phone-numbers), never Meta's; the calls on one group (retrieve, delete, reset and the sandbox simulation) identify it by group_id alone. In sandbox, GET /v1/sandbox/quickstart returns the phone number id as sender.default_sender_id.
  • A way to receive the eight group events: a webhook endpoint subscribed to them, or the listen route GET /v1/webhook-events/listen. Existing endpoints are not subscribed to new event types, and PATCH /v1/webhook-endpoints/{webhook_endpoint_id} replaces the whole subscribed_events list: send the types the endpoint already has plus the eight group.* types. The Webhooks guide covers signatures, retries and listening.

1. Check eligibility

Read the stored answer with GET /v1/groups/eligibility?phone_number_id={phone_number_id}. A sandbox number answers eligible. A production number answers not_checked until a check completes: request one with POST /v1/groups/eligibility-checks, a Tyxter worker then reads the number's facts from Meta, and the read shows the new answer once checked_at is later than check_requested_at.

Of the eligibility answers, only a stored not_eligible refuses a create, with 422 group_phone_number_not_eligible and the reasons Meta reported. unknown, not_checked and read_failed do not refuse it.

2. Create the group

POST /v1/groups with phone_number_id and subject (the group name), plus an Idempotency-Key. The answer is 202 with the group's Tyxter id and status pending. Keep the id: every later call uses it, never Meta's group id. A retry with the same key and body returns that first response again, still pending, and creates no second group; read the group to see where it is now.

3. Wait for group.created

A Tyxter worker picks the create up.

  • Sandbox: the group becomes active by itself, with a sandbox provider_group_id and a sandbox invite_link that cannot be used to join a real group (assumed). It becomes failed instead if its number can no longer host a group when the worker runs.
  • Production: Tyxter sends the create to Meta, and Meta reports the outcome later by webhook (heard). Tyxter applies that report: the group becomes active with Meta's provider_group_id and invite_link, or failed. Under "Available today", the API reference says when that report reaches Tyxter and which cases leave a group pending.

When the group leaves pending, Tyxter produces group.created, or group.create_failed with failure.code saying why, for the webhook endpoints subscribed to them; the errors page lists every group failure.code. Instead of waiting for the event you can poll GET /v1/groups/{group_id} until status leaves pending, or list the environment's groups with GET /v1/groups, filtered by phone_number_id or status. Tyxter promises no completion time, and a pending group cannot be deleted yet (409 group_not_active).

The invite link is the only way anyone joins a WhatsApp Business group: the business cannot add people (heard). Send invite_link to each person as an ordinary 1:1 WhatsApp message with POST /v1/messages, under the same service-window and template rules as any other send (Messages API). The invite is a message to that person, not a message into the group.

Treat invite_link like a password: anyone who has it can join the group. The same goes for the payloads of the six group lifecycle events, which carry it.

5. Follow participants

  • Sandbox: simulate a person joining or leaving an active group with POST /v1/sandbox/groups/{group_id}/participants and { "wa_id": "...", "action": "join" } (or "remove"). The sandbox refuses a ninth participant with 409 group_participant_limit_reached, the cap Meta documents (heard).
  • Production: a person joins by opening the link, and Meta reports each join and removal by webhook (heard), under the same condition as the create outcome.

A join Tyxter records produces a group.participant_joined event for the endpoints subscribed to it, and a removal a group.participant_removed event, whose initiated_by says whether the participant left (participant) or the business removed them (business). Both carry the participant's WhatsApp ID as participant.wa_id. When a contact erasure (DELETE /v1/contacts/{id}) matches the participant, Tyxter erases their recorded joins and removals: one whose event was not yet produced then produces none, and the listen route no longer returns their events; the API reference's Participants section says which WhatsApp IDs an erasure matches. Delivery follows the Webhooks guide's rules. GET /v1/groups/{group_id} lists the current participants. Events can arrive late, out of order or twice; the API reference's Participants section explains how Tyxter orders them.

Only an active group can be reset. POST /v1/groups/{group_id}/invite-link/reset answers 202 with the group still active and still showing the old link. A Tyxter worker then resets it. On success invite_link holds the new link (in production the one Meta returns; earlier links stop working, heard) and Tyxter produces group.invite_link_reset. When Tyxter records a failed reset, it produces group.invite_link_reset_failed with failure.code. Where that failure lands depends on the group's status when it is recorded: on an active group the group's failure shows it too; on a deleting group only the event carries it and the group keeps the delete's failure (a refused delete returns the group to active, so a reset failure recorded after that lands on the group). A reset replaced by a newer accepted reset, or not yet answered when the group becomes deleted, produces no event. The API reference says what each failure means for the stored link. Read the group to see the new link, and share it again.

7. Delete the group

DELETE /v1/groups/{group_id} answers 202. An active group becomes deleting, then deleted once the delete finishes, with group.deleted; if Meta refuses the delete, the group is active again with failure set, with group.delete_failed. In production a delete can also stay deleting, and while it does it produces neither event: when the call to Meta fails without a definite answer (for example a timeout), the group's failure.code is provider_error and no group.deleted or group.delete_failed follows unless Meta later reports the outcome. A delete Tyxter has not been able to send, or one still waiting for Meta's outcome, also stays deleting, with failure null. Read the group rather than waiting for an event; the API reference lists when a delete stays deleting. A failed group becomes deleted at once, with no call to Meta. A deleted group stays readable with status deleted. The delete keeps working when WhatsApp Business groups are disabled in feature controls, so you can always delete the groups you created.

Sandbox first, production second

The routes, request bodies, responses, error codes and events are the same in both. What differs:

  • In sandbox the create, the delete and the invite-link reset finish by themselves, and participants come only from the simulation route. A sandbox key never receives group.delete_failed or group.invite_link_reset_failed, and receives group.create_failed only when the sandbox number is released, disconnected or removed before the worker runs.
  • In production the outcome of each create, delete and reset Tyxter sends comes from Meta. Meta's own limits apply: 8 participants per group and 10,000 groups per business phone number (heard). Tyxter enforces neither in production; when Meta refuses an operation, the group's failure.code is group_provider_rejected with Meta's reason.

Example

import { randomUUID } from 'node:crypto';
import type { GroupParticipantWebhookEnvelope, GroupWebhookEnvelope } from '@tyxter/sdk-js';

// 1. Eligibility: a sandbox number answers 'eligible'.
const eligibility = await client.groups.retrieveEligibility({ phone_number_id: phoneNumberId });

// 2. Create: 202 with status 'pending'.
const group = await client.groups.create(
  { phone_number_id: phoneNumberId, subject: 'Support team' },
  { idempotencyKey: randomUUID() },
);

// 3. React to the outcome in your webhook handler, or poll client.groups.retrieve(group.id)
// until status leaves 'pending'. Steps 4 to 7 need the group to be 'active'.
async function onGroupEvent(event: GroupWebhookEnvelope) {
  if (event.type === 'group.created') await onActiveGroup(event.data.group_id);
  if (event.type === 'group.create_failed') console.log(event.data.failure?.code);
}

async function onActiveGroup(groupId: string) {
  // Read the group first: an event can arrive late, after the group changed again.
  const active = await client.groups.retrieve(groupId);
  if (active.status !== 'active' || !active.invite_link) return;

  // 4. Share the invite link with an ordinary 1:1 message.
  await client.whatsapp.sendText(
    { from: phoneNumberId, to: recipient, body: `Join our group: ${active.invite_link}` },
    { idempotencyKey: randomUUID() },
  );

  // 5. Sandbox only (a production key gets 400 sandbox_group_participant_sandbox_only):
  // simulate a person joining.
  await client.sandbox.groups.simulateParticipant(
    groupId,
    { wa_id: '5511999990001', action: 'join' },
    { idempotencyKey: randomUUID() },
  );
}

function onParticipantEvent(event: GroupParticipantWebhookEnvelope) {
  console.log(event.type, event.data.participant.wa_id, event.data.participant_count);
}

// 6. Later, on an active group: reset the invite link; read the group again for the new one.
async function resetInvite(groupId: string) {
  await client.groups.resetInviteLink(groupId, { idempotencyKey: randomUUID() });
}

// 7. When you are done: delete the group once it is 'active' (or 'failed').
async function deleteGroup(groupId: string) {
  await client.groups.delete(groupId, { idempotencyKey: randomUUID() });
}

The same create and read with curl:

curl -X POST "https://api.tyxter.com/v1/groups" \
  -H "authorization: Bearer $TYXTER_API_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: $(uuidgen)" \
  -d '{"phone_number_id":"'"$PHONE_NUMBER_ID"'","subject":"Support team"}'

curl "https://api.tyxter.com/v1/groups/{group_id}" \
  -H "authorization: Bearer $TYXTER_API_KEY"

Where to go next