- Guides
- Static recipient groups
Guides
Static recipient groups
Put a subscriber in a few named groups and send one message to all of them, without adopting a campaign product.
Static recipient groups
A group is a named set of your own subscribers. You decide who is in it, you send one message to it, and WhooshBang turns that into ordinary messages — the same resource, the same delivery states, the same diagnostics you already read for a one-recipient send.
That is the whole primitive. There are no rules, filters, segments, nested groups, schedules, or campaigns, and there will not be: those are a different product, and the boundary is deliberate.
What a group is
| Field | Behaviour |
|---|---|
key | Your identifier, lowercase and hyphenated. Immutable, unique inside one environment, and reserved forever once used. |
display_name, description | Yours to change whenever you like. |
status | active, or archived — which is what deleting a group means here. |
membership_revision | Advances on every membership change. A broadcast records the number it saw. |
Test and live are separate. The same key in both is two groups with no shared membership, and a credential for one can never see the other.
Create a group and put someone in it
curl -sS -X POST "https://api.whooshbang.com/v1/recipient-groups" \
-H "authorization: Bearer $WHOOSHBANG_API_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: $(uuidgen)" \
-d '{"key":"trial-users","display_name":"Trial users"}'
{
"key": "trial-users",
"display_name": "Trial users",
"description": "Accounts currently evaluating Acme."
}
A new subscriber can join groups as it is created. The subscriber and every
membership commit together: if one key is missing, archived, or belongs to
another environment, nothing is created and you get one
recipient_group_conflict that does not say which key was the problem.
{
"subscriber_id": "user_123",
"locale": "es",
"groups": ["account-updates", "trial-users"]
}
groups is creation-only input. Creating a subscriber that already exists is a
subscriber_conflict, not a quiet membership replacement — use the membership
operations for that:
curl -sS -X PUT \
"https://api.whooshbang.com/v1/recipient-groups/trial-users/members/user_123" \
-H "authorization: Bearer $WHOOSHBANG_API_KEY"
curl -sS -X DELETE \
"https://api.whooshbang.com/v1/recipient-groups/trial-users/members/user_123" \
-H "authorization: Bearer $WHOOSHBANG_API_KEY"
Both are safe to repeat. Adding somebody already in the group changes nothing;
removing somebody who is not there answers 204 and changes nothing.
Send to the group
curl -sS -X POST \
"https://api.whooshbang.com/v1/recipient-groups/trial-users/broadcasts" \
-H "authorization: Bearer $WHOOSHBANG_API_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: $EVENT_ID" \
-d '{"content":{"type":"text","text":"Your trial ends tomorrow."}}'
{
"id": "broadcast_trial_ending",
"project_id": "project_synthetic",
"environment": "test",
"group_id": "group_trial_users",
"group_key": "trial-users",
"audience_revision": 1,
"status": "accepted",
"aggregate": {
"audience": 1,
"expanded": 0,
"pending": 1,
"provider_accepted": 0,
"delivered": 0,
"skipped": 0,
"failed": 0,
"cancelled": 0,
"expired": 0
},
"accepted_at": "2026-08-22T18:00:00Z",
"updated_at": "2026-08-22T18:00:00Z",
"expires_at": "2026-08-23T18:00:00Z",
"diagnostic_id": "diag_n17_broadcast_accepted"
}
The envelope is the ordinary one: content, optional interaction, notifier,
expiry, correlation, metadata. The reply is 202 and a Broadcast you can poll
at /v1/group-broadcasts/{id}.
What the audience is, exactly
A broadcast records the group’s membership_revision at the moment it is
accepted, and that number is its audience. From then on:
- somebody added afterwards is not included — there is no window in which it is ambiguous;
- somebody removed before their message reaches the provider is not sent to;
- somebody re-added is a new membership, and no earlier broadcast can reach it — re-adding never revives work you cancelled by removing them; and
- a message the provider already accepted cannot be recalled, by anybody.
Retrying the same broadcast with the same idempotency-key returns the same
Broadcast and creates no second message for anybody.
Membership is addressing, not consent
Being in a group does not give WhooshBang permission to message somebody. Every
child resolves the recipient’s own channel choice exactly as a direct send
does, and a recipient who paused, unsubscribed, or never connected is recorded
under the same refusal you would have seen sending to them directly —
subscriber_unbound or preferred_binding_unavailable. It is a skipped
member, not a delivery failure, and it never falls back to another channel.
Reading what happened
GET /v1/group-broadcasts/{id} reports a projection over expansion and the
child messages — never a second delivery state machine:
| Count | Meaning |
|---|---|
audience | Membership generations active at the snapshotted revision. |
expanded | Members expansion has decided about. |
skipped | Members with no usable channel, plus children stopped by a removal before any provider attempt. |
provider_accepted, failed, expired, cancelled | Ordinary child-message outcomes. |
pending | Everything not yet settled. |
Every child is an ordinary message: read it at /v1/messages/{id}, with its
usual delivery timeline and diagnostics, plus a broadcast block naming the
broadcast, the group, and the exact membership generation it was sent for.
Archiving
DELETE /v1/recipient-groups/{key} archives the group. It is repeat-safe,
refuses new membership and new broadcasts with recipient_group_archived, and
leaves already-accepted broadcasts and their history intact. The key stays
reserved: archiving never frees it for a different group.
How long a big broadcast takes
Two different things stand between your 202 and the last person hearing from
you, and only one of them is ours.
Making the messages is fast. They are created in bulk, five hundred at a
time, so a fifty-thousand-person group becomes fifty thousand accepted messages
in well under a minute. expanded on the broadcast tells you how far that has
got.
Your channel then sets the pace. Telegram’s published guidance is around
thirty messages a second to different people from one bot, so fifty thousand
takes about half an hour to hand over however fast we produce them — and every
recipient is pinned to the exact channel they connected, so we cannot spread
them across bots to go quicker. Watch provider_accepted climb; the broadcast
reaches complete when the last one settles.
For a group of a few hundred the whole thing is over in seconds.
Who else can reach a group
Everything above is a project credential’s, on the flat paths — that is the integrator’s surface and the one to build against. Two other people sometimes need the same group, and neither can hold a credential:
- Somebody supporting a customer, signed in to the dashboard. Recipient groups appears on each environment, listing the groups, who is currently in one, and an archive confirmation that says what archiving costs before it does it.
- A coding agent, connected through the hosted MCP endpoint. It can create a subscriber and a group, add and remove members, broadcast, and read a broadcast’s aggregate.
Both name the project and environment in the path instead of taking it from a
credential:
/v1/projects/{project_id}/environments/{environment_id}/recipient-groups/….
Every operation above has one of these twins, with the same request, the same
response, and the same permission. What it is not is a way around anything: the
service checks the named project against the signed-in person’s own organization
membership before it acts, so a project outside that organization is refused exactly
as one that does not exist.
Limits
V1 bounds groups per environment, initial groups per subscriber, active members per group, fan-out per broadcast, and broadcasts per group per hour. A group holds up to fifty thousand people and one broadcast reaches all of them.
The initial-groups bound is the published groups array bound, so exceeding it
is an ordinary schema refusal. The rest are state: they answer 409 recipient_group_conflict and name the closed category in field_errors. Only
the hourly one resets on its own, and it is the only one that answers 429
with a retry_at.