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”-
Have an approved agent on your account — see Agents. A pending agent 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 |
|---|---|---|---|
agent | string | ✅ | An approved agent on your account. |
destination | string | ✅ | Recipient in international format, no + — 2348020000000. |
content | string | — | The message text. Required unless you send a richCard or carousel. |
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 '{ "agent": "FirstDigits", "destination": "2348020000000", "content": "Your code is 481920. It expires in 5 minutes.", "ttl": "300s" }'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({ agent: 'FirstDigits', destination: '2348020000000', content: 'Your code is 481920. It expires in 5 minutes.', ttl: '300s', }),});
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={ "agent": "FirstDigits", "destination": "2348020000000", "content": "Your code is 481920. It expires in 5 minutes.", "ttl": "300s", }, 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{ "agent": "FirstDigits", "destination": "2348020000000", "content": "Your code is 481920. It expires in 5 minutes.", "ttl": "300s",})
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 |
|---|---|---|
no agent is allocated to this account for "X" | No agent by that name is on your account. | Onboard one. |
the agent for "X" is not live yet (status registered) | Onboarded, still in review. | Wait for approval. |
recipient has opted out of messages from this agent | 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": "the agent for \"FirstDigits\" is not live yet (status registered)" }}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 | RejectedSet your webhook URL in the portal (Settings → Webhooks → RCS) 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.