# Managing WhatsApp groups

Everything the [**Groups**](https://bird.com/dashboard/w/whatsapp/groups) page does, twelve operations under `/v1/whatsapp/groups` do too, so a group's whole life fits in code: create it, wait for WhatsApp to confirm it, hand out its invite link, decide who gets in, keep its details and its pins current, and delete it when the conversation is over.

Two things shape every call on this page. **Nobody can be added to a group**, so the invite link is the only way in, and inviting people happens outside the API. **Most changes settle at WhatsApp rather than in the response**, so the call that accepts a change is not the call that tells you it worked; [How a change settles](#how-a-change-settles) covers that once for every operation it applies to.

[WhatsApp groups](/docs/guides/whatsapp/groups) covers what a group is for and the same lifecycle in the dashboard. [Sending to a WhatsApp group](/docs/guides/whatsapp/groups/sending) covers messaging one.

## Prerequisites

You need an API key with `whatsapp_management` access: read level for the reads on this page, write level for everything else. [Users, teams, and roles](/docs/guides/users-teams-roles) covers granting it.

You also need a business number that holds Official Business Account status, which WhatsApp requires of a group's administrator and [Before you create a group](/docs/guides/whatsapp/groups#before-you-create-a-group) covers earning. `GET /v1/whatsapp/numbers` reports it per number as `is_official_business_account`, so read it there rather than discovering it from a `412`. The flag is as of the number's `meta_synced_at`, so a number connected minutes ago may not have been read back yet.

Initialize the client for your language using the [TypeScript](/docs/sdks/typescript), [Python](/docs/sdks/python), [Go](/docs/sdks/go), or [PHP](/docs/sdks/php) SDK guide. For CLI examples, [install and authenticate the CLI](/docs/cli#authenticate). The cURL examples name the US host; if your key starts with `bk_eu1_`, call `https://eu1.platform.bird.com` instead. The SDKs read the region from your key, so they set no host.

## 1. Create the group

A create names the administering number and a subject, and nobody else. Two choices are fixed for the group's whole life and cannot be changed afterwards: the number, and `join_approval_mode`, which decides whether opening the invite link admits someone outright (`auto_approve`, the default) or raises a request you decide (`approval_required`).

**TypeScript**

```typescript
const group = await bird.whatsapp.groups.create({
  whatsapp_number_id: "wan_01krdgeqcxet5s7t44vh8rt9mg",
  subject: "Norwood Fleet — Tuesday route",
  join_approval_mode: "approval_required",
});
console.log(group.id, group.status); // pending; read it back for the invite link
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

The API returns `202` with the group at `status` `pending` and no `invite_link`: WhatsApp issues the link when it confirms the group. The subject runs to 128 characters and an optional `description` to 2,048, and both are shown to anyone who opens the link, so name the group for its purpose rather than for an internal reference.

## 2. Wait for WhatsApp to confirm it

Read the group back until its `status` leaves `pending`. The read answers `200` whatever the status is, so branch on `status` rather than on the call succeeding.

**TypeScript**

```typescript
const group = await bird.whatsapp.groups.get("wag_01krdgeqcxet5s7t44vh8rt9mg");
console.log(group.status, group.invite_link);
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

Five statuses can come back, and only one of them is a group you can work with:

| Status      | What it means                                                                             |
| ----------- | ----------------------------------------------------------------------------------------- |
| `pending`   | WhatsApp has not confirmed the group. `invite_link` is null and nothing here is shareable |
| `active`    | The group exists, its invite link works, and it takes messages and changes                |
| `suspended` | WhatsApp stopped activity in the group. Only WhatsApp lifts this                          |
| `deleted`   | The group is gone, and stays readable so a stored reference to it still resolves          |
| `failed`    | WhatsApp refused the create, which is terminal                                            |

`failed` is the other way out of `pending`, and `last_operation.last_error.description` carries WhatsApp's own reason. Never retry a failed create against that group: create a new one instead, changing what the refusal objected to.

## 3. Let people into the group

Send the group's `invite_link` to the people you want in it, over whatever channel already reaches them. No operation adds a participant, so the link is the only way in and joining is their decision. It admits whoever holds it, so handle it as the credential it is: [WhatsApp groups](/docs/guides/whatsapp/groups#invite-people-to-the-group) covers how far to spread one.

On an `auto_approve` group, anyone holding the link is in and there is nothing to decide. On an `approval_required` group, opening the link raises a join request. List the ones waiting, then decide them in batches of up to 50:

**TypeScript**

```typescript
const waiting = [];
for await (const request of bird.whatsapp.groups.joinRequests.list(
  "wag_01krdgeqcxet5s7t44vh8rt9mg",
)) {
  waiting.push(request.id);
}
const result = await bird.whatsapp.groups.joinRequests.approve("wag_01krdgeqcxet5s7t44vh8rt9mg", {
  join_request_ids: waiting.slice(0, 50),
});
console.log(result.decided.length, result.failed.length);
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

WhatsApp decides each request on its own, so a batch is part-appliable: the response splits `decided` from `failed`, and each failure carries the reason WhatsApp gave in `error.description`. Read the response rather than the status code. A refusal is often a person who has not accepted WhatsApp's current terms, which no retry fixes.

Approving more people than the group has room for does not refuse the call: the requests that fit are decided and the rest come back in `failed`, each saying the approval would take the group past [its participant limit](/docs/guides/whatsapp/groups#what-whatsapp-limits). Rejecting works the same way, through `POST /v1/whatsapp/groups/{group_id}/join-requests/batch-reject`. A request also leaves the list when the person cancels it themselves, so an ID that was valid a moment ago can return a `422` [`E15051`](/docs/api/errors/E15051) by the time you use it.

The 50 is Bird's own bound on one request, not a WhatsApp one, so a rejection sweep is not held to how many people the group can hold.

### Rotating the invite link

Rotating issues a new link and stops every link the group had before, so anyone still holding one cannot join. Rotate when a link has spread further than you intended, or after removing someone you do not want back. The new link comes back on the call itself, which answers `200`:

**TypeScript**

```typescript
const link = await bird.whatsapp.groups.inviteLink.rotate("wag_01krdgeqcxet5s7t44vh8rt9mg");
console.log(link.invite_link); // every earlier link has stopped working
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

Rotating is not how you read the current link. The group carries its `invite_link`, so reading the group is the read.

The CLI never prompts, so its destructive commands take `--yes` instead: rotating a link, removing a participant, deleting a group, unpinning a message, and rejecting join requests all refuse with `confirmation_required` without it.

## How a change settles

WhatsApp confirms some changes in its reply and others on a webhook, and the status code says which kind you are looking at:

| Operation                       | Answers | Where the outcome lands                                   |
| ------------------------------- | ------- | --------------------------------------------------------- |
| Create, update, delete          | `202`   | The group's `last_operation`, once WhatsApp reports back  |
| Remove a participant            | `202`   | That participant's own `last_operation` in `participants` |
| Approve or reject join requests | `202`   | `decided` and `failed` on the response itself             |
| Rotate, pin, unpin              | `200`   | The response, which is the change already applied         |

For the first two rows, the `202` is the request accepted, not applied. `last_operation.status` is `pending` until WhatsApp reports back, usually within seconds, then settles to `success` or `failed`, with `last_error.description` carrying the reason on a failure. Re-read the group to see what took effect.

While an operation is `pending`, another change to the same thing returns a `409` [`E15054`](/docs/api/errors/E15054). Group-level changes share the group's `last_operation`, so an outstanding rename blocks a delete. Each participant carries their own, so removing several people at once gives each their own state and their own failure, and only that person's pending removal blocks another attempt at them.

Two `409`s guard the writes and `status` is checked first, so a group that is not `active` returns [`E15047`](/docs/api/errors/E15047) whatever its `last_operation` says. That matters for a group still being created, which is pending on both counts at once.

Every write also takes an `Idempotency-Key` header. [Idempotency](/docs/guides/idempotency) covers retrying safely.

## Change the group's subject, description, or picture

An update changes what participants see at the top of the group. Fields you omit are left alone; send `null` to clear the description or the picture, since an empty string is a `422` rather than a second way to clear.

**TypeScript**

```typescript
const group = await bird.whatsapp.groups.update("wag_01krdgeqcxet5s7t44vh8rt9mg", {
  subject: "Norwood Fleet — Wednesday route",
});
console.log(group.last_operation?.status); // pending until WhatsApp reports back
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

WhatsApp applies each field separately, so an update can be part-applied: one field refused while the others take effect. When the operation settles, `last_operation.results` carries one entry per field the update sent, which is how you put a refusal next to the input it came from.

`profile_picture_url` names a file in your workspace's media library rather than any address on the internet, and WhatsApp takes a square JPEG of at least 192 by 192 pixels and up to 5 MB. Clearing it clears the picture Bird stores, but WhatsApp has no operation for taking a group's photo down, so participants keep seeing the current one until another picture replaces it. The administering number and `join_approval_mode` are fixed at create time and cannot be changed here.

## Remove a participant

Removal cannot be undone. WhatsApp blocks a removed person from joining the group by invite link, and the block follows the person rather than the link, so rotating the link does not let them back in. With no add operation to pair with this one, nothing returns someone to this group once they are out, and reaching them again means creating another group and inviting them to it.

Name the person by either identifier the group's `participants` list them under: their `bsuid`, which everyone has, or their `phone_number` in E.164 format, which is there only when WhatsApp shares it. In a URL, percent-encode the leading `+` as `%2B` unless your client does that for you.

**TypeScript**

```typescript
const group = await bird.whatsapp.groups.participants.remove(
  "wag_01krdgeqcxet5s7t44vh8rt9mg",
  "+15550002222",
);
console.log(group.participants?.length);
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

A participant's removal is `pending` on their own entry until WhatsApp confirms it, and never reaches `success`: the entry disappearing from `participants` is what says it worked. A value naming someone who is not in the group returns a `422` [`E15050`](/docs/api/errors/E15050).

## Pin a message

Pinning holds one of the group's own messages at the top of its chat, so someone joining later does not have to scroll for the thing the conversation keeps coming back to. A pin lasts `duration_days`, from 1 to 30, and defaults to 7.

**TypeScript**

```typescript
const pin = await bird.whatsapp.groups.pins.create("wag_01krdgeqcxet5s7t44vh8rt9mg", {
  message_id: "wam_01kya19eknftrs2s6p82asmvnh",
  duration_days: 14,
});
console.log(pin.pinned_until);
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

The message has to be one this group carries: a message in another group, or a one-to-one message, returns a `422` [`E15053`](/docs/api/errors/E15053). A group holds [only so many pins at once](/docs/guides/whatsapp/groups#what-whatsapp-limits), and pinning past that unpins the oldest rather than failing; pinning an already-pinned message replaces its expiry. What the group currently pins is on the group itself, as `pinned_messages`.

WhatsApp unpins a message on its own once its days have passed, so unpin only to take one down early. `DELETE /v1/whatsapp/groups/{group_id}/pinned-messages/{message_id}` answers `200`, and repeating it is safe: a message this group carries that is not pinned changes nothing and still answers `200`.

## Delete the group

Deleting removes the group at WhatsApp. Every participant loses access, your business included, and the invite link stops working. This cannot be undone, and running the conversation again means creating a new group and sending its link.

**TypeScript**

```typescript
const group = await bird.whatsapp.groups.delete("wag_01krdgeqcxet5s7t44vh8rt9mg");
console.log(group.last_operation?.status); // pending until WhatsApp confirms it
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

The group stays readable afterwards at `status` `deleted`, so a stored reference to it still resolves rather than turning into a `404`. A `failed` group can be deleted too, and that one reaches no external system: the create never produced a group at WhatsApp, so clearing the row is Bird's own bookkeeping. Every other status refuses with a `409` [`E15047`](/docs/api/errors/E15047).

## List your groups

The list returns the groups your workspace created, newest first, as a cursor page. Filter by the administering number (`number`, as an E.164 number or a `wan_` ID), by the WhatsApp Business Account it sends under (`waba`), or by `status`, repeating the parameter to match any of several. `q` searches the subject and the description for a substring, case-insensitively.

**TypeScript**

```typescript
for await (const group of bird.whatsapp.groups.list({ status: ["active"] })) {
  console.log(group.id, group.subject, group.participant_count);
}
```

Examples: [TypeScript](/docs/guides/whatsapp/groups/management.ts.md) · [Python](/docs/guides/whatsapp/groups/management.py.md) · [Go](/docs/guides/whatsapp/groups/management.go.md) · [PHP](/docs/guides/whatsapp/groups/management.php.md) · [CLI](/docs/guides/whatsapp/groups/management.cli.md) · [cURL](/docs/guides/whatsapp/groups/management.curl.md)

A deleted group stays in the list with `status` `deleted`, so filter by status to see only the groups you can still message.

## Troubleshooting

- **`412` ([E15045](/docs/api/errors/E15045))**: The number does not hold Official Business Account status, so WhatsApp will not let it create a group. The decision is WhatsApp's: request the status in WhatsApp Manager on the number's profile, or create the group on a number that already holds it.
- **`404` ([E15046](/docs/api/errors/E15046))**: The ID names no group this workspace holds. A group belongs to the workspace that created it, so an ID from another workspace is not found here.
- **`409` ([E15047](/docs/api/errors/E15047))**: The group is pending, suspended, deleted, or failed. Only an active group takes a change. Read the group to see where it stands.
- **`409` ([E15049](/docs/api/errors/E15049))**: The number already administers the 10,000 groups WhatsApp allows. Delete groups whose conversation is over, or create this one on another number.
- **`409` ([E15054](/docs/api/errors/E15054))**: A change to the same group or the same participant is still outstanding. Read `last_operation` and wait for it to settle.
- **`422` ([E15050](/docs/api/errors/E15050))**: The path names nobody in the group. Read the group's `participants` and use a `bsuid` or `phone_number` as it reports them.
- **`422` ([E15051](/docs/api/errors/E15051))**: One of the join request IDs is no longer waiting for a decision. List the requests again and use the IDs that come back.
- **`422` ([E15053](/docs/api/errors/E15053))**: The message belongs to another group or to a one-to-one conversation. A message's `to.group_id` says which group can pin it.
- **A `pending` group that never becomes active**: Bird gives up waiting rather than leaving it pending, and moves the group to `failed` with the reason on `last_operation.last_error`. Create a replacement rather than retrying.

## Reference

The full request and response shapes are in the API reference: [create](/docs/api/reference/create-whatsapp-group), [list](/docs/api/reference/list-whatsapp-groups), [get](/docs/api/reference/get-whatsapp-group), [update](/docs/api/reference/update-whatsapp-group), [delete](/docs/api/reference/delete-whatsapp-group), [rotate the invite link](/docs/api/reference/rotate-whatsapp-group-invite-link), [remove a participant](/docs/api/reference/delete-whatsapp-group-participant), [list](/docs/api/reference/list-whatsapp-group-join-requests), [approve](/docs/api/reference/approve-whatsapp-group-join-requests) and [reject](/docs/api/reference/reject-whatsapp-group-join-requests) join requests, and [pin](/docs/api/reference/create-whatsapp-group-pinned-message) and [unpin](/docs/api/reference/delete-whatsapp-group-pinned-message) a message.

## Next steps

- [WhatsApp groups](/docs/guides/whatsapp/groups): what a group is for, and the same lifecycle in the dashboard
- [Sending to a WhatsApp group](/docs/guides/whatsapp/groups/sending): addressing the group, what it takes, and its per-participant receipts
- [Receiving WhatsApp group messages](/docs/guides/whatsapp/groups/receiving): which participant wrote one, and replying to the group
- [Business-scoped user IDs](/docs/guides/whatsapp/business-scoped-user-ids): the identifier a participant is named by when you have no phone number
- [Idempotency](/docs/guides/idempotency): retrying a write that may already have been accepted

## Related resources

- [Connecting WhatsApp to Bird: from buying a number to a live channel](/learn/whatsapp/connecting-whatsapp-to-bird) (video)
- [What is the 24-hour customer service window on WhatsApp?](/explained/whatsapp/what-is-the-24-hour-customer-service-window) (answer)
- [WhatsApp message builder](/tools/whatsapp-message-builder) (tool)
- [WhatsApp](/products/whatsapp) (product)

[Get an implementation brief](/learn/workspace?topic=whatsapp)
