WhatsApp Operators DailyThe Blueticks DispatchSunday, August 2, 2026
Productivity

WhatsApp Group API on Your Own Number: Send, Create & Manage Groups from Code (Python & Node, 2026)

Sending to a group is one path segment. Creating one, adding twelve people in a single call, promoting an admin and knowing in advance which groups will reject you is the rest of the job.

DRBy Daniel Roth · August 2, 2026 · 11 min read
WhatsApp Group API on Your Own Number: Send, Create & Manage Groups from Code (Python & Node, 2026)

Sending a message to a group from code is the easy part. It is one path segment. What eats an afternoon is everything around it: finding the group's id, creating the group in the first place, adding twelve people without making twelve calls, and working out why one particular group swallows every send. This is the whole group surface of the Blueticks /v1 API, read off production source on 2026-08-02, with runnable Python and Node.

Can you send a WhatsApp message to a group via API?

Yes. A group id is a path recipient exactly like a phone number: POST /v1/scheduled-messages/{chatId} with a @g.us id in the path. It runs on your own WhatsApp number through a connected engine, not the Meta Cloud API.

What you need before the first call, in one block:

  • API key - a bt_live_ token sent as Authorization: Bearer <key>
  • A connected engine - GET /v1/engines returns an empty array when nothing is paired, not an error
  • groups:read - required for list and get
  • groups:write - required for every mutation
  • Base URL - https://api.blueticks.co

Keys are minted in the dashboard. One thing worth knowing before you go hunting for checkboxes: the backend forces scopes: ['*'] and environment: 'live' on every key created through the app, so a dashboard-minted key already carries the wildcard, and the wildcard satisfies both groups:read and groups:write. The scope names still matter, because that is the vocabulary you see when something is denied. A key without the right scope returns 403 with code: "permission_denied" and the message Missing required scope: groups:write.

Three per-key rate buckets sit in front of these routes, each with its own counter:

BucketLimitWindowApplies to
tool:read12060slist groups, get group, common groups
tool:write6060screate, patch, all member routes
tool:media3060sthe group picture route only

These are fixed windows, not rolling ones. The counter is incremented in Redis and the 60-second expiry is set on the first request of a window, so a burst at second 59 and another at second 61 both pass. Exceeding a bucket returns 429 with Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining headers. Separately, accounts without an active subscription share a plan-level ceiling of 5 API requests per fixed six-hour UTC slot, so slots begin at 00:00, 06:00, 12:00 and 18:00 UTC.

Auth setup, key rotation and what "your own number" actually means are covered in the WhatsApp REST API on your own number guide. This piece assumes you already have a key.

How do you find a group's @g.us id from code?

Two read routes return group ids. GET /v1/groups lists groups with limit, skip, searchToken and includeArchive. GET /v1/groups/{id} returns the richer single group. The list row is deliberately slimmer than the single-group object, and that difference bites people.

Start with the search:

import requests

BASE = "https://api.blueticks.co"
H = {"Authorization": f"Bearer {API_KEY}"}

r = requests.get(
    f"{BASE}/v1/groups",
    headers=H,
    params={"searchToken": "warehouse", "limit": 25},
    timeout=60,
)
r.raise_for_status()
body = r.json()
for g in body["data"]:
    print(g["id"], g.get("name"), g.get("participantCount"))
print(body["total"], "matches")

searchToken is a case-insensitive substring match on the group name, 1 to 128 characters. limit is 1 to 200 and defaults to 50, skip defaults to 0, and archived groups are excluded unless you pass includeArchive=true. The query object is strict, so a typo like search=warehouse is rejected rather than ignored.

Now the trap. A list row is a GroupListItem and carries exactly eight fields: id, name, description, owner, createdAt, lastMessageAt, participantCount, restrict. There is no announce and no participants on a list row. Do not write code that reads either one off the list.

The /v1 envelope also deep-strips null and undefined properties from every 2xx body before it goes on the wire, so a group whose metadata has not synced yet comes back with those keys absent, not present-and-null. In Python use .get(). In JavaScript use ??. A response looks like this:

{
  "success": true,
  "data": [
    {
      "id": "120363042318711234@g.us",
      "name": "Warehouse Ops",
      "participantCount": 34,
      "restrict": false,
      "lastMessageAt": "2026-08-01T18:42:07.000Z"
    }
  ],
  "limit": 25,
  "skip": 0,
  "total": 1
}

For the full object, including announce and the member list, fetch the group directly and ask for participants:

r = requests.get(
    f"{BASE}/v1/groups/120363042318711234@g.us",
    headers=H, params={"include": "participants"}, timeout=60,
)
g = r.json()["data"]
admins = [p["chatId"] for p in g.get("participants", []) if p["isAdmin"]]

include accepts exactly one value, participants. There is no include=picture, no include=admins, and no useful comma-separated form. Omit it and the participants key is simply not in the response.

Two alternatives are worth knowing. GET /v1/chats?kinds=group returns group chats through the general chat list, which is handy when you are already paging chats. And GET /v1/contacts/{id}/common_groups returns the groups your number shares with a given contact, in the same slim list-item shape, under groups:read. The Node.js WhatsApp bot guide covers the general chat-discovery path in more detail.

About the id format itself. The groupId path parameter is validated against ^\d+(?:-\d+)?@g\.us$. That accepts both the modern form, 120363042318711234@g.us, and the legacy form built from the creator's number and a timestamp, 15551234567-1633024800@g.us. It does not accept @c.us, and it does not accept @lid. The member-reference validator is wider and does accept @lid, so the whatsapp group id g.us rule is narrower than the member rule sitting right next to it in the same file.

warehouse team huddle

How do you send a message to that group in Python and Node?

The recipient lives in the URL. POST /v1/scheduled-messages/{chatId} takes the group's @g.us id in the path exactly where a phone number would go, and returns 201. That is the entire difference between a group send and a one-to-one send.

r = requests.post(
    f"{BASE}/v1/scheduled-messages/120363042318711234@g.us",
    headers={**H, "Content-Type": "application/json"},
    json={"type": "text", "text": "Dock 3 inbound at 14:00."},
    timeout=60,
)
m = r.json()["data"]
print(m["status"], m.get("waMessageKey"))
const res = await fetch(
  `${BASE}/v1/scheduled-messages/120363042318711234@g.us`,
  {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ type: "text", text: "Dock 3 inbound at 14:00." }),
  },
);
const { data } = await res.json();
console.log(data.status, data.waMessageKey ?? null);

type is required and is one of text, media, poll. Add an ISO 8601 sendAt with an offset to defer it. A 201 is acceptance, not delivery, and waMessageKey is absent until the engine has actually dispatched. Posting to the bare collection path with no recipient returns 410 Gone. The request body, the response envelope, the status lifecycle and the delivery-polling loop are all documented in the send endpoint reference, so I will not restate them here.

Skip the Meta Business verification queue. No Official Business Account, no template approval, no per-message platform fee on the send. Your existing WhatsApp number, one POST, groups included. Start free at blueticks.co/signup.

How do you create a WhatsApp group from an API call?

POST /v1/groups with a name and a participants array creates the group and returns the created group with its members already inlined. The name is 1 to 100 characters, the array is 1 to 256 entries, and every member is either a JID or a +E.164 phone number.

r = requests.post(
    f"{BASE}/v1/groups",
    headers={**H, "Content-Type": "application/json"},
    json={
        "name": "Q4 Planning",
        "participants": ["+15555550100", "15555550101@c.us"],
    },
    timeout=60,
)
if r.status_code >= 400:
    err = r.json()["error"]
    print(err["code"], err["message"], err.get("details"))
    raise SystemExit(1)
g = r.json()["data"]
print(g["id"], len(g["participants"]))
const res = await fetch(`${BASE}/v1/groups`, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Q4 Planning", participants: ["+15555550100"] }),
});
const body = await res.json();
if (!body.success) {
  console.error(body.error.code, body.error.message, body.error.details ?? []);
  process.exit(1);
}
console.log(body.data.id);

Three things account for most first-call failures on the create whatsapp group api path.

The body is strict. Unknown keys are rejected outright, not ignored. {"name": "...", "members": [...]} fails because the field is participants. {"name": "...", "participants": [...], "description": "..."} fails because description is not accepted at create time; set it afterwards with a PATCH. You get 400 with code: "invalid_request" and a details array of {path, code, message}. For an unknown key the path is empty and the offending key name is in message; for a bad value, such as a malformed phone, path names the field, for example participants.0.

Phone format is exact. The member validator accepts a JID matching ^\d+(?:-\d+)?@(?:c\.us|g\.us|lid)$, or a phone matching ^\+\d{6,15}$. A leading + is mandatory on the phone form, and six to fifteen digits after it. "15555550100" with no plus is a 400.

Create normalizes, the member routes do not. The create handler runs every entry through a phone-to-JID normalizer before dispatching to the engine, stripping the + and appending @c.us. The member sub-resource routes covered in the next section hand your value to the engine as-is. Send +15555550100 to create if you like, but send 15555550100@c.us to the member routes.

The response is the full group object, and participants is always inlined on a create even though you did not ask for it.

Every error body across the whole surface has the same shape, which makes the handling above reusable:

{
  "success": false,
  "error": {
    "code": "invalid_request",
    "message": "Unrecognized key(s) in object: 'members'",
    "requestId": "req_...",
    "request_id": "req_..."
  }
}

code is drawn from a fixed map: invalid_request for 400 and 422, authentication_required for 401, permission_denied for 403, not_found for 404, rate_limited for 429, and internal_error for anything 5xx.

How do you add, remove, promote and demote members?

Member management is five raw routes mounted outside the Feathers service, all requiring groups:write and all charged to the tool:write bucket. Every one of them returns HTTP 200.

Verb + pathEffect
POST /v1/groups/{id}/membersAdd one or many participants
DELETE /v1/groups/{id}/members/{chatId}Remove a participant
POST /v1/groups/{id}/members/{chatId}/adminPromote to admin
DELETE /v1/groups/{id}/members/{chatId}/adminDemote to member
DELETE /v1/groups/{id}/members/meLeave the group yourself

The add member to whatsapp group api route takes either a single chatId or a participants array of 1 to 256 entries, and at least one of the two must be present. So a batch add is one HTTP call, not N:

const res = await fetch(`${BASE}/v1/groups/${groupId}/members`, {
  method: "POST",
  headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    participants: ["15555550100@c.us", "15555550101@c.us", "15555550102@c.us"],
  }),
});
const body = await res.json();
if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);

It is one HTTP call, but not one atomic operation. Internally the service loops and dispatches one engine call per participant, and surfaces the first failure. If participant seven is unreachable you get an error, and participants one through six are already in the group. Treat a failed batch add as "partially applied" and re-read the group rather than blindly retrying the whole array.

Response shapes are small and are not the group object. Add, remove, promote and demote all return {"ok": true} inside the success envelope. Leaving returns a slightly larger object:

{ "success": true, "data": { "ok": true, "groupId": "120363042318711234@g.us", "left": true } }

Now the part the API cannot help you with. Every one of these operations is subject to WhatsApp's own permission model, and the API has no way to override it. Promoting, demoting, removing and renaming generally require your number to already be an admin of that group. Adding somebody can fail purely because of their privacy settings, which no amount of correct request formatting fixes. When WhatsApp refuses, the engine's rejection surfaces as 422 carrying the engine's own message, so read error.message rather than assuming your payload was wrong.

One line, no hedging: adding people to groups without their consent is how accounts get reported and banned. Meta's WhatsApp Business Platform policy and spam enforcement page sets out an escalation ladder for accounts that repeatedly violate the Business Messaging Policy - feature limits, then blocks, then permanent disablement - and an unofficial engine does not exempt you from any of it. If you want the group scheduling workflow without writing any code, the extension path is covered in scheduling a WhatsApp group message.

phone face down slate

How do you rename a group or lock it to admins-only?

PATCH /v1/groups/{id} updates group metadata. The body is strict and must contain at least one of name or settings, otherwise you get a 400 telling you exactly that.

r = requests.patch(
    f"{BASE}/v1/groups/{group_id}",
    headers={**H, "Content-Type": "application/json"},
    json={
        "name": "Q4 Planning (locked)",
        "settings": {"announce": True, "restrict": True},
    },
    timeout=60,
)
print(r.json())   # {"success": true, "data": {"ok": true}}

The two flags do different things and people mix them up constantly:

  • announce: true - only admins can post messages in the group
  • restrict: true - only admins can edit the group's subject, description and picture
  • editInfoAdminsOnly - a friendly alias for restrict

If you send both restrict and editInfoAdminsOnly in the same request, the explicit restrict wins. settings.description is also accepted here, 1 to 2048 characters, and it is dispatched to the engine as its own separate call so that a failure setting the admin flags cannot silently swallow a description update, or the other way round.

The group picture is its own route, PUT /v1/groups/{id}/picture, and it is the one group route charged to the tool:media bucket at 30 requests per minute rather than 60. It accepts two content types. As JSON, supply at least one of fileDataUrl (a base64 data URL, up to 20,971,520 characters) or url (https only, up to 2048 characters); fileName caps at 255 characters and fileMimeType at 127. As multipart/form-data, put the bytes in a part named file, capped at 20 MiB by the upload middleware. Both paths return {"ok": true}. The same base64-versus-URL-versus-upload tradeoff applies to message attachments, which is unpacked in the guide to sending images, PDFs and documents.

There is no DELETE /v1/groups/{id} - the service implements no remove handler at all. Deleting a WhatsApp group is not an operation WhatsApp offers anyway; you remove the members and leave, which is what DELETE /v1/groups/{id}/members/me is for.

Two timing details worth planning around. The group fetch and the leave route both pass an explicit 60,000 ms engine timeout, because the engine serializes the entire group metadata payload synchronously and leaving involves a metadata write plus an acknowledgement. On a group with several hundred members, both routes are genuinely slow. Set your HTTP client timeout above 60 seconds for those two calls or your own client will give up before the API does, and a timeout on the API side comes back as 504 with the message WhatsApp engine did not respond in time.

Which groups will reject your send, and why the API treats it as permanent

Community parent groups and announce-only groups where you are not an admin are the two group types that reliably swallow sends on the WhatsApp Web path. The useful move is to detect the second case with a read call before you spend a send.

Here is the predictive check. Fetch the group with participants, then compare the announce flag against your own admin status:

r = requests.get(f"{BASE}/v1/groups/{group_id}", headers=H,
                 params={"include": "participants"}, timeout=90)
g = r.json()["data"]

me = "15555550100@c.us"            # your own number as a JID
announce = g.get("announce", False)
i_am_admin = any(p["chatId"] == me and p["isAdmin"] for p in g.get("participants", []))

if announce and not i_am_admin:
    print("admins-only group, this send will not land")

Read announce from GET /v1/groups/{id}, never from GET /v1/groups, because the list row does not carry it. And read the flag honestly: announce is the group-level "only admins can post" toggle. It is deliberately documented in the schema as distinct from the per-user posting ability, which the engine resolves separately and does not expose on the public object. So announce: true plus isAdmin: false is a strong negative signal, not a proof, and announce: false is not a guarantee either.

The community case is worse, and I would rather say so than sell you a check that does not exist. The public Group object has exactly ten fields: id, name, description, owner, createdAt, lastMessageAt, participantCount, announce, restrict, participants. There is no groupType, no isCommunity, no parentGroupId. You cannot predict a community-parent failure from a /v1 read. The workaround is behavioural, not technical: send to the subgroup, not to the announcement group at the top of the community.

When one of these does fail, the failure is not worth retrying. The internal reliability job classifies failure strings like "cannot send to this group", "not a participant" and "group no longer exists" into a genuine bucket, as opposed to the transient bucket that holds media and timeout errors. That word is an internal analytics category, not a status the API hands back to you. What a caller actually sees on the message object is status: "failed" and a human-readable failureReason string. Read failureReason and stop retrying when it names the group.

empty chairs circle morning

What the Meta Cloud API can and cannot do here, and where this fits

Meta shipped a Groups API for the Cloud API, and any comparison written before late 2025 is out of date. It exists, and it solves a genuinely different problem than the one most people arrive with.

Per Meta's own Groups API documentation, the feature "is now open to all businesses with an Official Business Account (OBA)", and "Groups are not available for WhatsApp Business app phone numbers." The documented flows cover creating and deleting groups, generating and resetting invite links, removing participants, retrieving group information, and sending and receiving messages inside those groups.

The shape of it is the thing to notice. Meta describes groups as "an invite-only experience where participants join using a group invite link you send them", and the group management reference says plainly: "Since you cannot manually add participants to the group, simply send a message with your invite link to WhatsApp users." So the Cloud API path is: get an Official Business Account, create a fresh group on a business number your customers have never saved, and wait for people to accept an invite link.

That is a reasonable product. It is not the thing you were looking for if the group you care about already exists, already contains your suppliers or your class or your ops team, and is attached to the number you have been using for three years.

The whatsapp api own number path answers that instead, and the trade is honest. Your requests drive a real WhatsApp session belonging to your real number, which is exactly why an existing group is reachable at all and exactly why your account's standing is on the line. There is no Meta intermediary absorbing policy risk on your behalf. Every rule about consent, frequency and content applies to you directly, and WhatsApp can restrict any number.

The operational floor matters too. A browser tab driving WhatsApp Web stops the moment the tab closes. If a group send is supposed to fire at 06:00, something has to be awake at 06:00, which is what the hosted gateway is for.

FAQ

Can I send a message to a WhatsApp group via API?

Yes. Put the group's @g.us id in the path of POST /v1/scheduled-messages/{chatId}, exactly where a phone number would go, with type and a body. It returns 201. It runs on your own WhatsApp number through a connected engine, so no Meta Business verification is involved.

How do I get a WhatsApp group id?

Call GET /v1/groups with an optional searchToken for a case-insensitive name match, and read id off each row. GET /v1/chats?kinds=group works too. Ids look like 120363042318711234@g.us, or the legacy 15551234567-1633024800@g.us form built from a number and a timestamp.

Can I create a WhatsApp group from the API?

Yes. POST /v1/groups with name (1 to 100 characters) and participants (1 to 256 entries, each a JID or a +E.164 phone). The body is strict, so unknown keys are rejected rather than ignored. The response is the created group with its participants already inlined.

Do I need Meta Business verification for this?

No. Blueticks drives the WhatsApp number you already use, so there is no Official Business Account, no template approval and no per-message platform fee on the send. Meta's own Groups API does require an Official Business Account and is unavailable on WhatsApp Business app numbers.

Why did my send to a community fail?

Community parent groups and announce-only groups where you are not an admin are known failure cases on this path. The failure is classified as permanent rather than transient, so retrying will not help. Send to the subgroup instead. The public group object exposes no community flag, so you cannot detect it in advance.

How many people can I add to a group at once?

Up to 256 in a single POST /v1/groups/{id}/members call using the participants array. It is one HTTP request but not one atomic operation: the service dispatches one engine call per participant and surfaces the first failure, so a partial add is possible.

Can I schedule a message to a group?

Yes. Add an ISO 8601 sendAt with an offset to the same send body. It must be at least 10 seconds in the future and no more than 365 days out. Something has to be connected when the time arrives, which is what the always-on gateway handles.

Email

The Dispatch, every week.

One sharp WhatsApp growth tactic in your inbox each week. Joined by 238,000+ founders, marketers and support leads.

Free forever. No spam, unsubscribe in one click.