Send your first RCS message
POSThttps://api.9bits.net/ng/v1/rcs/messages
Sends one RCS message to one recipient.
Before your first call
Section titled “Before your first call”-
Register a sender and wait for approval — see Sender IDs. A pending sender is rejected, not queued.
-
Mint a
9bk_key with thercs:sendscope in the dashboard under Settings → Security. -
Send to your own number first. Every call here is a real, billed send.
Request
Section titled “Request”| Field | Type | Required | Description |
|---|---|---|---|
senderId | string | ✅ | An approved RCS sender on your account. |
destination | string | ✅ | Recipient in international format, no + — 2348020000000. |
content | string | — | The message text. Required unless you send a richCard or carousel. |
callback_url | string | — | Where to POST the delivery report. |
ttl | string | — | Give up after this long, e.g. "300s". |
richCard | object | — | A single card. See Rich messages. |
carousel | object | — | Two or more cards. See Rich messages. |
suggestions | array | — | Tappable chips under the message. Valid alongside plain content. |
Send it
Section titled “Send it”curl -X POST https://api.9bits.net/ng/v1/rcs/messages \ -H "Authorization: Bearer $NINEBITS_RCS_KEY" \ -H "Content-Type: application/json" \ -d '{ "senderId": "FirstDigits", "destination": "2348020000000", "content": "Your code is 481920. It expires in 5 minutes.", "ttl": "300s", "callback_url": "https://example.com/hooks/rcs" }'const res = await fetch('https://api.9bits.net/ng/v1/rcs/messages', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NINEBITS_RCS_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ senderId: 'FirstDigits', destination: '2348020000000', content: 'Your code is 481920. It expires in 5 minutes.', ttl: '300s', callback_url: 'https://example.com/hooks/rcs', }),});
const body = await res.json();
// The HTTP code is not the whole story — check `status` too.if (body.status !== 200) { throw new Error(body.description ?? body.error?.message);}
console.log('queued as', body.referenceId);import os, requests
res = requests.post( "https://api.9bits.net/ng/v1/rcs/messages", headers={"Authorization": f"Bearer {os.environ['NINEBITS_RCS_KEY']}"}, json={ "senderId": "FirstDigits", "destination": "2348020000000", "content": "Your code is 481920. It expires in 5 minutes.", "ttl": "300s", "callback_url": "https://example.com/hooks/rcs", }, timeout=30,)
body = res.json()if body.get("status") != 200: raise RuntimeError(body.get("description") or body.get("error", {}).get("message"))
print("queued as", body["referenceId"])payload, _ := json.Marshal(map[string]any{ "senderId": "FirstDigits", "destination": "2348020000000", "content": "Your code is 481920. It expires in 5 minutes.", "ttl": "300s", "callback_url": "https://example.com/hooks/rcs",})
req, _ := http.NewRequest(http.MethodPost, "https://api.9bits.net/ng/v1/rcs/messages", bytes.NewReader(payload))req.Header.Set("Authorization", "Bearer "+os.Getenv("NINEBITS_RCS_KEY"))req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)if err != nil { return err}defer resp.Body.Close()
var body struct { Status int `json:"status"` ReferenceID string `json:"referenceId"` Description string `json:"description"`}json.NewDecoder(resp.Body).Decode(&body)
if body.Status != 200 { return fmt.Errorf("rcs send refused: %s", body.Description)}Response
Section titled “Response”{ "status": 200, "referenceId": "8f2c41ba9d7e40a1b5c3"}Keep the referenceId. It is your handle on the message — it comes back as
message_id in the delivery report, and it is how you
find the message in the reports.
When it is refused
Section titled “When it is refused”Everything below is checked before your account is debited, so a rejected send costs nothing.
| What you see | What it means | Fix |
|---|---|---|
sender_id "X" is not registered for this account | The name is not on your account at all. | Register it. |
sender_id "X" is not approved (status: Pending) | Registered, still in review. | Wait for approval. |
sender "X" has no RBM agent registered; rich messages need one | You sent a card, carousel or chips under a sender with no agent. | Ask support to map the agent, or send plain text. |
recipient has opted out of messages from this sender | They replied STOP to this brand. | Do not retry. See Consent. |
invalid destination: … | The number did not parse or classify. | Check phone number format. |
a carousel needs at least two cards | A one-card carousel. | Send it as richCard. |
tariff not configured for operator "mtn" on this account | No RCS price for that network on your account. | Contact support — this is a billing setup gap, not a code bug. |
insufficient balance (status: 51) | Balance below the cost, past your credit limit. | Top up. |
Everything down to the carousel rule arrives as HTTP 400 in the structured
error envelope. The tariff gap is a 500 — it is our
configuration, not your request:
{ "error": { "type": "validation_error", "code": "invalid_body", "message": "sender_id \"FirstDigits\" is not approved (status: Pending)" }}What happens next
Section titled “What happens next”The 200 only means the message was accepted and queued. It then moves:
Queued → Dispatched → Delivered └→ Undelivered | RejectedPass a callback_url and you get told; otherwise poll the reports. Both are
covered in Delivery reports.
- Rich messages — cards, carousels and chips.
- Broadcasts — the same message to many people.
- Try it live — run this endpoint from the browser.