Delivery reports
A 200 from the send endpoint means queued. A delivery report (DLR) is how
you find out what actually happened.
Set delivery_report: true when you send, then either poll or receive a push.
Poll the status endpoint
Section titled “Poll the status endpoint”GEThttps://api.9bits.net/ng/v1/sendsms/status/{‘{reference_id}’}
curl https://api.9bits.net/ng/v1/sendsms/status/fdab8f46df049e58b0af7c2d7 \ -H "Authorization: Bearer $NINEBITS_TOKEN"{ "reference_id": "fdab8f46df049e58b0af7c2d7", "total": 3, "counts": { "delivered": 2, "pending": 1 }, "recipients": [ { "destination": "2348020000000", "status": "delivered", "operator": "mtn", "message_id": "4f2a9c1b7e" }, { "destination": "2348030000000", "status": "failed", "status_reason": "absent subscriber", "operator": "mtn", "message_id": "7b3d5e2a9f" }, { "destination": "2349010000000", "status": "pending", "operator": "airtel", "message_id": "1c8e4a6b2d" } ]}counts gives you campaign progress in one field — usually all a dashboard
needs. recipients gives you the per-number detail.
Status values
Section titled “Status values”| Status | Meaning | Final? |
|---|---|---|
pending | Queued with us, not yet handed to the operator. | No |
submitted | Handed to the operator, awaiting their receipt. | No |
delivered | Confirmed on the handset. | ✅ Yes |
failed | Rejected or undeliverable. Check status_reason. | ✅ Yes |
expired | Validity period elapsed before it could be delivered — typically the phone was off or out of coverage for the whole window. | ✅ Yes |
Only delivered, failed and expired are terminal. Anything else will still
change.
Or receive a webhook (recommended)
Section titled “Or receive a webhook (recommended)”Set callback_url on the send and we POST each receipt to you as it arrives —
no polling, no rate limit, no delay.
{ "from": "9bits", "to": ["2348020000000"], "content": "Your code is 481920.", "delivery_report": true, "callback_url": "https://example.com/webhooks/9bits/dlr"}Each receipt arrives as:
{ "reference_id": "fdab8f46df049e58b0af7c2d7", "destination": "2348020000000", "status": "delivered", "operator": "mtn", "message_id": "4f2a9c1b7e"}app.post('/webhooks/9bits/dlr', async (req, res) => { const { message_id, destination, status, status_reason } = req.body;
// Acknowledge immediately — do the work off the request path. res.sendStatus(200);
// You will receive duplicates. De-duplicate on message_id. const { rowCount } = await db.query( `INSERT INTO sms_receipts (message_id, destination, status, reason, received_at) VALUES ($1, $2, $3, $4, now()) ON CONFLICT (message_id) DO NOTHING`, [message_id, destination, status, status_reason ?? null], ); if (rowCount === 0) return; // already seen
if (status === 'failed' || status === 'expired') { await flagUndeliverable(destination, status_reason); }});@app.post("/webhooks/9bits/dlr")async def dlr(payload: dict, background: BackgroundTasks): # Acknowledge first; process after. background.add_task(handle_receipt, payload) return Response(status_code=200)
async def handle_receipt(evt: dict) -> None: # De-duplicate — retries mean you will see this more than once. inserted = await db.execute( """INSERT INTO sms_receipts (message_id, destination, status, reason) VALUES ($1, $2, $3, $4) ON CONFLICT (message_id) DO NOTHING""", evt["message_id"], evt["destination"], evt["status"], evt.get("status_reason"), ) if not inserted: return
if evt["status"] in ("failed", "expired"): await flag_undeliverable(evt["destination"], evt.get("status_reason"))Return a 2xx and de-duplicate on message_id — retries mean the same receipt
can arrive more than once. The retry schedule and the rules for acknowledging are
in the Webhooks guide.
Acting on failures
Section titled “Acting on failures”status_reason carries the operator’s explanation. The common ones:
| Reason | What it means | What to do |
|---|---|---|
absent subscriber | Phone off or out of coverage. | Transient. Retry later — often succeeds. |
unknown subscriber | The number isn’t active on the network. | Permanent. Stop sending to it. |
rejected | The operator blocked it — often sender ID or content policy. | Check your sender ID and message content. |
Only REST batches are visible
Section titled “Only REST batches are visible”The status endpoint only knows about batches sent through this API. Messages sent
over SMPP, or through the dashboard’s bulk tools, have no REST reference_id and
return batch_not_found.