Broadcasts
A broadcast sends one campaign to many recipients. You name it, we resolve the recipients, and a worker fans the messages out and bills them one by one.
POST /ng/v1/rcs/broadcasts/estimate — price it, no send, no charge
POST /ng/v1/rcs/broadcasts — send it
Price it first
Section titled “Price it first”The estimate resolves exactly the same recipients the real send would, prices each against your RCS tariff for that operator, and compares the total to your balance.
curl -X POST https://api.9bits.net/ng/v1/rcs/broadcasts/estimate \ -H "Authorization: Bearer $NINEBITS_RCS_KEY" \ -H "Content-Type: application/json" \ -d '{ "broadcastType": "multi", "broadcastName": "march-promo", "senderId": "FirstDigits", "content": "Half price this weekend only.", "destination": "lagos-customers" }'{ "status": 200, "validMsisdns": 1840, "inValidMsisdns": 12, "balance": 250000, "cost": 18400, "currency": "NGN", "numberOfMessages": 1840, "isThereEnoughBalance": true}isThereEnoughBalance: false is a warning, not a block — the send will still be
accepted and will simply start failing partway through with status: 51. Check
it and top up first.
Three ways to say who
Section titled “Three ways to say who”broadcastType decides how destination is read:
| Type | destination is | Message |
|---|---|---|
single | Comma-separated numbers | content — the same for everyone |
multi | A contact group name | content — the same for everyone |
variable | A dynamic contact group name | Each contact carries its own text |
variable is how you personalise: the message lives on the contact row, not in
the request, so content is ignored. Upload a dynamic group in the dashboard,
then reference it by name.
Send it
Section titled “Send it”curl -X POST https://api.9bits.net/ng/v1/rcs/broadcasts \ -H "Authorization: Bearer $NINEBITS_RCS_KEY" \ -H "Content-Type: application/json" \ -d '{ "broadcastType": "multi", "broadcastName": "march-promo", "senderId": "FirstDigits", "content": "Half price this weekend only.", "destination": "lagos-customers", "callback_url": "https://example.com/hooks/rcs" }'const res = await fetch('https://api.9bits.net/ng/v1/rcs/broadcasts', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NINEBITS_RCS_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ broadcastType: 'multi', broadcastName: 'march-promo', senderId: 'FirstDigits', content: 'Half price this weekend only.', destination: 'lagos-customers', callback_url: 'https://example.com/hooks/rcs', }),});
const body = await res.json();if (body.status !== 200) throw new Error(body.description);
console.log(body.broadcastId, 'to', body.targetBase, 'recipients');res = requests.post( "https://api.9bits.net/ng/v1/rcs/broadcasts", headers={"Authorization": f"Bearer {os.environ['NINEBITS_RCS_KEY']}"}, json={ "broadcastType": "multi", "broadcastName": "march-promo", "senderId": "FirstDigits", "content": "Half price this weekend only.", "destination": "lagos-customers", "callback_url": "https://example.com/hooks/rcs", }, timeout=30,)
body = res.json()if body.get("status") != 200: raise RuntimeError(body["description"])
print(body["broadcastId"], "to", body["targetBase"], "recipients"){ "status": 200, "message": "Your request is being processed", "broadcastId": "4c1f9a72be0d5836a7e1", "targetBase": 1840, "scheduledFor": null}Schedule it
Section titled “Schedule it”Set scheduled: true and a defer_until of YYYY-MM-DD HH:MM, in your
account’s local time:
{ "broadcastType": "single", "broadcastName": "friday-reminder", "senderId": "FirstDigits", "content": "Your appointment is tomorrow at 10am.", "destination": "2348020000000,2348030000000", "scheduled": true, "defer_until": "2026-09-12 09:30"}{ "status": 200, "message": "Your broadcast has been scheduled", "broadcastId": "4c1f9a72be0d5836a7e1", "targetBase": 2, "scheduledFor": "2026-09-12 09:30"}Two behaviours to know:
- A
defer_untilin the past falls through to an immediate send. It is not an error, so a timezone mistake sends now rather than telling you. - An unparseable
defer_untilis an error:invalid defer_until (expected 'YYYY-MM-DD HH:MM'). Note the space — this is not ISO 8601.
Request fields
Section titled “Request fields”| Field | Type | Required | Description |
|---|---|---|---|
broadcastType | string | ✅ | single, multi or variable. |
broadcastName | string | ✅ | Your name for the campaign. This is how you find it in the reports, so make it unique. |
senderId | string | ✅ | An approved RCS sender. |
destination | string | ✅ | Numbers or a group name, per broadcastType. |
content | string | — | Required for single and multi. Ignored for variable. |
callback_url | string | — | Delivery reports for every message in the campaign. |
scheduled | boolean | — | Defaults to false. |
defer_until | string | — | YYYY-MM-DD HH:MM. Only read when scheduled is true. |
When it is refused
Section titled “When it is refused”All of these come back as 400 with a description:
| Description | Cause |
|---|---|
invalid broadcastType (expected single|multi|variable) | Typo, or an empty broadcastType. |
senderId and broadcastName are required | One of the two is missing. |
content is required | A single or multi send with no text. |
group not found | The group name does not match one of yours. Names are per-user. |
no valid recipients | The group is empty, or the number list parsed to nothing. |
invalid defer_until (expected 'YYYY-MM-DD HH:MM') | Wrong timestamp format. |
Tracking a campaign
Section titled “Tracking a campaign”Use broadcastName against the report endpoints:
curl -X POST https://api.9bits.net/ng/v1/rcs/reports/broadcasts/messages \ -H "Authorization: Bearer $NINEBITS_RCS_KEY" \ -H "Content-Type: application/json" \ -d '{ "broadcastName": "march-promo" }'The campaign list, per-message drill-down and CSV export are all in the API reference.