1. Guides
  2. Static recipient groups

​
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

FieldBehaviour
keyYour identifier, lowercase and hyphenated. Immutable, unique inside one environment, and reserved forever once used.
display_name, descriptionYours to change whenever you like.
statusactive, or archived — which is what deleting a group means here.
membership_revisionAdvances 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"}'
whooshbang:verify=fixture:recipient-experience-v1/requests/create-recipient-group.json
{
  "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.

whooshbang:verify=fixture:recipient-experience-v1/requests/create-subscriber-with-groups.json
{
  "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."}}'
whooshbang:verify=fixture:recipient-experience-v1/success/group-broadcast-accepted.json
{
  "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.

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:

CountMeaning
audienceMembership generations active at the snapshotted revision.
expandedMembers expansion has decided about.
skippedMembers with no usable channel, plus children stopped by a removal before any provider attempt.
provider_accepted, failed, expired, cancelledOrdinary child-message outcomes.
pendingEverything 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.