API Reference
Send SMS and WhatsApp messages, check your credit balance, and manage sender identities from any application.
All requests go to the base URL:
https://bulkiemsg.com/api/v1
Content-Type: application/json. Successful responses include "success": true. Errors include "error".
Authentication
Every request must include your API key. Generate one from your dashboard under Settings → API Keys.
Authorization: Bearer bm_live_your_key_here
X-API-Key: bm_live_your_key_here
Keys look like bm_live_ followed by 64 characters. You can have up to 3 keys, each with its own name, access level and optional IP allowlist, so every app gets its own key and one leak never forces you to change them all.
Security
The API is built so that a mistake on one side does not become a loss on the other.
Keys are shown once
We store only a scrambled (SHA-256) copy of your key, so even we cannot read it back. If you lose it, create a new one and revoke the old one.
IP allowlist
Limit a key to your server's IP address (or a range like 203.0.113.0/24). Requests from anywhere else get 403, even with the right key.
Read-only keys
Give apps that only check balance, delivery status or Sender IDs a Read-only key. It cannot send messages or spend credits.
Rate limits and lockout
Every key is limited per minute, and an address that sends many wrong keys is locked out for a few minutes. See Rate Limits.
Activity log
Your dashboard (API Settings) lists the last 30 requests made with your keys, with time, result and IP address. Revoke any key you do not recognise.
Safe retries
Send an Idempotency-Key so a retried request can never send, or charge, twice. See Safe Retries.
Checklist
- Keep the key in an environment variable or secret store, never in code you commit.
- Use one key per app and name it, so you know which one to revoke.
- Add your server's IP address to the allowlist.
- Use Read-only keys wherever sending is not needed.
- Always call the API over HTTPS, so your key is never sent in the clear.
Quick Start
Get your API key
Go to Settings → API Keys in your dashboard and click Generate Key. Save it — it's shown once.
Get a Sender ID approved
Go to SMS → Sender IDs and request your brand name as a Sender ID. You can't send without one.
Send your first message
curl -X POST https://bulkiemsg.com/api/v1/sms/send \ -H "Authorization: Bearer bm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender_id": "YourBrand", "message": "Hello from BulkieMsg!", "to": ["233201234567"] }'
$ch = curl_init('https://bulkiemsg.com/api/v1/sms/send'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer bm_live_YOUR_KEY', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'sender_id' => 'YourBrand', 'message' => 'Hello from BulkieMsg!', 'to' => ['233201234567'], ]), ]); $result = json_decode(curl_exec($ch), true);
import requests r = requests.post( "https://bulkiemsg.com/api/v1/sms/send", headers={"Authorization": "Bearer bm_live_YOUR_KEY"}, json={ "sender_id": "YourBrand", "message": "Hello from BulkieMsg!", "to": ["233201234567"], } ) print(r.json())
const res = await fetch('https://bulkiemsg.com/api/v1/sms/send', { method: 'POST', headers: { 'Authorization': 'Bearer bm_live_YOUR_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ sender_id: 'YourBrand', message: 'Hello from BulkieMsg!', to: ['233201234567'], }), }); console.log(await res.json());
Send SMS
Send a message to one number or thousands. Credits are deducted at 1 per SMS page per recipient (160 chars = 1 page).
Headers
| Header | Type | Description | |
|---|---|---|---|
| Authorization | string | required | Bearer bm_live_… with a Full access key. |
| Idempotency-Key | string | recommended | A unique value per message you intend to send (8 to 80 characters). A retry with the same value is answered with the first result and is not sent or charged again. Details |
Parameters
| Parameter | Type | Description | |
|---|---|---|---|
| sender_id | string | required | Your approved Sender ID (e.g. MyBrand). |
| message | string | required | The SMS body. Over 160 chars splits into multiple pages. |
| to | string | array | required | One phone number or an array. Accepts 233XXXXXXXXX or local 0XXXXXXXXX — normalised automatically. |
curl -X POST https://bulkiemsg.com/api/v1/sms/send \ -H "Authorization: Bearer bm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender_id": "MyBrand", "message": "Your OTP is 482910. Valid 5 minutes.", "to": ["233201234567", "233271234567"] }'
$ch = curl_init('https://bulkiemsg.com/api/v1/sms/send'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer bm_live_YOUR_KEY', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'sender_id' => 'MyBrand', 'message' => 'Your OTP is 482910.', 'to' => ['233201234567', '233271234567'], ]), ]); $res = json_decode(curl_exec($ch), true);
import requests r = requests.post( "https://bulkiemsg.com/api/v1/sms/send", headers={"Authorization": "Bearer bm_live_YOUR_KEY"}, json={ "sender_id": "MyBrand", "message": "Your OTP is 482910.", "to": ["233201234567", "233271234567"], } ) print(r.json())
const res = await fetch('https://bulkiemsg.com/api/v1/sms/send', { method: 'POST', headers: { 'Authorization': 'Bearer bm_live_YOUR_KEY', 'Content-Type': 'application/json', }, body: JSON.stringify({ sender_id: 'MyBrand', message: 'Your OTP is 482910.', to: ['233201234567', '233271234567'], }), }); console.log(await res.json());
Responses
{ "success": true, "message": "Queued 2 recipient(s) for delivery.", "campaign_id": 1042, "recipients": 2, "status": "queued" }{ "success": false, "error": "Insufficient credits. Need 5, have 2." }Check Balance
Returns the current SMS credit balance. No request body needed.
curl https://bulkiemsg.com/api/v1/sms/balance \ -H "Authorization: Bearer bm_live_YOUR_KEY"
$ch = curl_init('https://bulkiemsg.com/api/v1/sms/balance'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer bm_live_YOUR_KEY'], ]); $res = json_decode(curl_exec($ch), true);
import requests r = requests.get( "https://bulkiemsg.com/api/v1/sms/balance", headers={"Authorization": "Bearer bm_live_YOUR_KEY"} ) print(r.json())
const res = await fetch('https://bulkiemsg.com/api/v1/sms/balance', { headers: { 'Authorization': 'Bearer bm_live_YOUR_KEY' } }); console.log(await res.json());
{ "success": true, "sms_credits": 500 }Sender IDs
All SMS must be sent with an approved Sender ID. Submit one for review — approval typically takes up to 48 hours.
| Parameter | Type | Description | |
|---|---|---|---|
| sender_id | string | required | 3–11 alphanumeric characters, no spaces. e.g. MyBrand. |
curl -X POST https://bulkiemsg.com/api/v1/sms/sender_ids \ -H "Authorization: Bearer bm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "sender_id": "MyBrand" }'
{ "success": true, "message": "Sender ID submitted for review." }Returns all your Sender IDs and their status: pending, approved, or rejected.
curl https://bulkiemsg.com/api/v1/sms/sender_ids \ -H "Authorization: Bearer bm_live_YOUR_KEY"
{
"success": true,
"sender_ids": [
{ "id": 42, "sender_id": "MyBrand", "status": "approved" }
]
}WhatsApp — Send Message
Sends a plain text WhatsApp message via your connected device. Costs 1 credit per recipient.
| Parameter | Type | Description | |
|---|---|---|---|
| to | string | array | required | Phone number(s). Use international format: 233XXXXXXXXX. |
| message | string | required | Message text. Max 4096 characters. |
curl -X POST https://bulkiemsg.com/api/v1/whatsapp/send \ -H "Authorization: Bearer bm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "233201234567", "message": "Your appointment is tomorrow at 10am." }'
{ "success": true, "message": "Message sent to 1 recipient(s)." }WhatsApp — Link Device
Returns a QR code you scan with WhatsApp on your phone to link it. Expires in 60 seconds.
curl -X POST https://bulkiemsg.com/api/v1/whatsapp/session \ -H "Authorization: Bearer bm_live_YOUR_KEY"
{
"success": true,
"qr_code": "data:image/png;base64,...",
"expires_in": 60
}Safe Retries
If your request times out you cannot tell whether the messages were sent. Retrying blindly could send them twice. Add an Idempotency-Key header to POST /sms/send and a retry returns the first answer instead of sending again.
curl -X POST https://bulkiemsg.com/api/v1/sms/send \ -H "Authorization: Bearer bm_live_your_key_here" \ -H "Idempotency-Key: order-1001-confirmation" \ -H "Content-Type: application/json" \ -d '{"sender_id":"MyBrand","to":"233201234567","message":"Your order is confirmed"}'
- Use one new value per message you mean to send, such as an order number or a UUID. Letters, numbers and
. _ : -, 8 to 80 characters. - A repeat with the same key and the same body returns the original response with the header
Idempotent-Replay: true. Nothing is sent or charged. - The same key with a different body returns
422. A repeat while the first request is still running returns409. - A request that failed (for example
402not enough credits) frees its key, so you can retry it after fixing the problem. - Keys are remembered for 24 hours.
Rate Limits
Limits are counted per API key, per minute:
| Type of request | Limit |
|---|---|
Sending (/sms/send, /whatsapp/send, device linking) | 30 per minute |
Reading (/sms/balance, /sms/status, /sms/sender_ids) | 120 per minute |
| Wrong or revoked keys, from one IP address | 20 per 10 minutes, then locked out for the rest of the window |
Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. When you go over, you get 429 with a Retry-After header (seconds). Wait that long, then retry.
{ "success": false, "error": "Rate limit exceeded (30 requests per minute). Retry in 23s." }Errors & Codes
All errors return "success": false and an error string. Use the HTTP status code to categorise the failure.
| Code | Meaning | Common cause |
|---|---|---|
| 200 | OK | Request succeeded. |
| 400 | Bad Request | Missing or invalid parameter. |
| 401 | Unauthorized | API key missing, invalid, or revoked. |
| 402 | Payment Required | Not enough SMS credits. |
| 403 | Forbidden | Read-only key used to send, your IP address is not on the key's allowlist, or the account is suspended. |
| 404 | Not Found | Resource doesn't exist. |
| 405 | Method Not Allowed | Wrong HTTP verb. |
| 422 | Unprocessable | Validation failed, or an Idempotency-Key was reused with a different request. |
| 409 | Conflict | A request with the same Idempotency-Key is still being processed. |
| 429 | Rate Limited | Rate limit reached, or too many wrong keys from your IP address. Wait for Retry-After seconds. |
| 500 | Server Error | Internal error. Contact support. |
| 503 | Unavailable | Sending is temporarily paused. Nothing was charged; retry later. |
| 502 | Gateway Error | SMS gateway unreachable. Credits not deducted. |
Common error messages
// Missing key { "success": false, "error": "API key is required." } // Not enough credits { "success": false, "error": "Insufficient credits. Need 10, have 3." } // Sender ID not approved { "success": false, "error": "Sender ID 'MyBrand' is not approved." }