# Bulkit > Bulk SMS, WhatsApp and bulk email platform for businesses, schools, churches and organizations across Kenya and East Africa. REST API with delivery reports, two-way shortcodes, branded sender IDs, and mail-merge personalization. Support: +254 777 888 077, hello@bulkitsms.com. > Every documentation page is also available as plain Markdown at https://bulkitsms.com/md/.md (e.g. https://bulkitsms.com/md/quickstart.md). ## Core pages - [Bulk SMS and Bulk Email Platform for Africa](https://bulkitsms.com/): Product overview: branded sender IDs, two-way shortcodes, WhatsApp API, bulk email, mail-merge personalization. - [Pricing](https://bulkitsms.com/pricing): Per-SMS rates by credit-load tier: Ksh 0.90 (1-10k), Ksh 0.60 (100k-250k), Ksh 0.40 (500k+). Standard credits never expire. Smart Bundles: 24-hour discounted bundles from Ksh 100 (125 SMS at Ksh 0.80) to Ksh 2,000 (5,000 SMS at Ksh 0.40). - [Help & Support Center](https://bulkitsms.com/support): Searchable knowledge base: sender ID registration, mobile money and bank top-ups, Smart Bundles, shortcodes, API errors, delivery reports, support contacts. - [Blog](https://bulkitsms.com/blog): Guides on SMS delivery, sender IDs, mail-merge, and messaging strategy in Kenya. - [About Bulkit](https://bulkitsms.com/about): Company mission, infrastructure architecture, direct telco interconnects, compliance with CA Kenya and KDPA, and team values. - [Contact](https://bulkitsms.com/contact): Sales and support contact form; WhatsApp on all pages. - [Terms and Conditions](https://bulkitsms.com/terms): Service terms, refund policy. - [Privacy Policy](https://bulkitsms.com/privacy): Data handling under the Kenya Data Protection Act. ## Blog - [Smart Bundles: 24-Hour Credit Packs for Small Sends](https://bulkitsms.com/blog/smart-bundles-24-hour-credit-packs) (Oct 23, 2026): Discounted SMS bundles from KSh 100 that stay valid for 24 hours: how they sit alongside your standard credits, when they save you money, and how to avoid losing unused units at the deadline. - [Why Branded Sender IDs Outperform Generic Short Numbers](https://bulkitsms.com/blog/branded-sender-id-guide) (Jun 9, 2026): How registered alphanumeric sender names (like BULKIT) foster immediate consumer trust, prevent phishing, and drive significantly higher engagement across Kenya. - [Direct Telco Interconnects vs. Grey Routes](https://bulkitsms.com/blog/sms-deliverability-kenya) (May 28, 2026): Why direct routing into Safaricom, Airtel, and Telkom ensures sub-second delivery, zero message drops, and verifiable delivery receipts (DLRs). - [Integrating the Bulkit REST API in 15 Minutes](https://bulkitsms.com/blog/rest-api-developer-guide) (May 19, 2026): A comprehensive guide for developers sending transactional OTPs, order updates, and batch notifications with clean JSON payloads. - [Mail-Merge Magic: Personalizing 50,000 Messages](https://bulkitsms.com/blog/personalized-sms-merging) (May 7, 2026): How to dynamically insert recipient names, outstanding balances, and dates using Bulkit spreadsheet templates. - [How Schools & Colleges Automate Parent Announcements](https://bulkitsms.com/blog/managing-customer-requests) (Apr 24, 2026): Managing class lists, fee reminders, and urgent weather or event notices with structured contact groups. - [Two-Way Shortcodes: Launching Interactive SMS in Kenya](https://bulkitsms.com/blog/two-way-shortcodes) (Apr 15, 2026): Allow customers to reply with keywords, trigger automated responses, and feed leads straight into your backend. - [Instant Credit Top-Ups via M-Pesa](https://bulkitsms.com/blog/mpesa-paybill-topups) (Apr 2, 2026): Top up your SMS and email units 24/7 with automatic account balance updates and instant downloadable invoices. - [Church & Ministry Broadcasts: Keeping Congregations Connected](https://bulkitsms.com/blog/automated-onboarding) (Mar 21, 2026): How community leaders coordinate weekly services, prayer meetings, and fund drives effortlessly. - [DKIM, SPF, and DMARC: Ensuring 99% Email Inbox Placement](https://bulkitsms.com/blog/email-campaigns-guide) (Mar 10, 2026): Essential DNS configurations to make sure your marketing and transactional emails land in primary inboxes, not spam. - [Bulkit 2.0: Faster Dispatches, Cleaner UI, and Advanced Analytics](https://bulkitsms.com/blog/bulkit-v2-announcement) (Feb 26, 2026): A ground-up redesign of our portal with faster contact importing, live delivery charts, and scheduled campaigns. ## Full documentation --- # Introduction URL: https://bulkitsms.com/docs/introduction --- title: "Introduction" description: "Understand what Bulkit offers and how its developer-facing API is structured." --- # Bulkit Bulkit is a REST-style messaging platform for developers who need to send SMS, dispatch WhatsApp messages, check account credit, and automate messaging workflows from their own applications. It is designed for: - product teams building transactional alerts and multi-channel customer communications - internal systems that need programmatic SMS and WhatsApp delivery - developers integrating balance checks, broadcasts, and inbound message handling into CRMs, ERPs, and web apps ## What you can do with the API Bulkit’s developer API supports: - **Authentication**: Isolated managed API keys with support for headers, Basic Auth, and query params - **Account**: Checking real-time float credit balance and wallet currency - **SMS Messaging**: Sending single SMS, bulk broadcasts, and scheduled messages using registered sender IDs - **WhatsApp Messaging**: Sending direct text chats, rich media attachments (PDFs, images, audio, video up to 50MB), and personalized broadcasts - **Contacts Management**: Programmatically managing contact groups and phone numbers - **Webhooks**: Receiving real-time delivery reports and inbound customer replies via shared account-level endpoints ## Base URL ```bash https://api.bulkitsms.com ``` All examples in this documentation assume the `/api/v2` API namespace. ## Response format Most successful responses follow this structure: ```json { "status": "success", "message": "Request processed successfully", "data": { "id": "7b8f844a-f5e2-4144-84c4-f655e88b2a1a", "cost": 0.03, "currency": "KES" } } ``` Most error responses follow this structure: ```json { "status": "error", "message": "Invalid credentials" } ``` ## API style Bulkit uses: - JSON request and response bodies for `POST` endpoints - predictable error messages for common integration failures - timezone-aware timestamps for scheduled sends - normalized Kenyan mobile numbers in `2547...` or `2541...` format - multipart file upload or remote media URL fetching for rich attachments Bulkit supports both newer managed API keys and legacy client API keys. Existing legacy integrations continue to work while new integrations benefit from simplified key creation and dedicated account-level webhooks. ## Recommended reading order If you are integrating for the first time, read the pages in this order: 1. [Quickstart](/quickstart) 2. [Generating API Keys](/generating-api-keys) 3. [Authentication Overview](/authentication/overview) 4. [Get Account Balance](/account/get-account-balance) 5. [How to Send SMS](/sms/overview) 6. [WhatsApp API Overview](/whatsapp/overview) 7. [Webhooks Overview](/webhooks/overview) --- # Quickstart URL: https://bulkitsms.com/docs/quickstart --- title: "Quickstart" description: "Go from account creation to your first successful SMS request in a few minutes." --- # Quickstart This page shows the fastest path to a working Bulkit integration. Use this flow when you want to: - generate your credentials - verify authentication - confirm available credit - send your first SMS or WhatsApp message ## Step 1: Create your Bulkit account Create a Bulkit account and sign in to the dashboard. Your API credentials are managed from the API Keys section inside the dashboard. ## Step 2: Generate an API key Create a new API key from the dashboard (**API Keys** -> **New API Key**). Simply enter a key name (e.g. `Production Server`) and copy your API Token: ``` bk_live_8n6JQv3K1h9Lp0Md ``` For v2 API endpoints, your API key is used directly as a single Bearer token in the `Authorization` header. No separate secret is needed. ## Step 3: Check your account balance Before sending messages, confirm that your account has available credit: ```bash GET /api/v2/account/credits ``` Example: ```bash curl -X GET "https://api.bulkitsms.com/api/v2/account/credits" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` Response: ```json { "status": "success", "data": { "client_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "balance": 2.97, "last_change": "2026-09-19T06:30:00Z", "currency": "KES" } } ``` ## Step 4: Send your first message ### Option A: Send an SMS Send a single SMS using the dedicated single SMS endpoint: ```bash POST /api/v2/messages/sms ``` ### Option B: Send a WhatsApp Message Ensure your WhatsApp device is linked in the dashboard (**WhatsApp** -> **Connect**), then dispatch via: ```bash POST /api/v2/messages/whatsapp ``` ## Example flow ```bash curl (SMS) # 1. Check Balance curl -X GET "https://api.bulkitsms.com/api/v2/account/credits" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" # 2. Send Single SMS curl -X POST "https://api.bulkitsms.com/api/v2/messages/sms" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -d '{ "sender": "BULK_IT", "mobile": "254700000001", "message": "Welcome to Bulkit. Your integration is now live." }' ``` ```bash curl (WhatsApp) # Send Single WhatsApp Message curl -X POST "https://api.bulkitsms.com/api/v2/messages/whatsapp" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -d '{ "recipient": "254700000001", "message": "Hello from Bulkit WhatsApp API v2!" }' ``` ```python python.py import requests BASE_URL = "https://api.bulkitsms.com" HEADERS = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", } # 1. Check balance balance_resp = requests.get(f"{BASE_URL}/api/v2/account/credits", headers=HEADERS, timeout=30) balance_resp.raise_for_status() print("Balance:", balance_resp.json()["data"]["balance"], balance_resp.json()["data"]["currency"]) # 2. Send SMS sms_resp = requests.post( f"{BASE_URL}/api/v2/messages/sms", headers=HEADERS, json={ "sender": "BULK_IT", "mobile": "254700000001", "message": "Welcome to Bulkit. Your integration is now live.", }, timeout=30, ) sms_resp.raise_for_status() print("SMS Queued:", sms_resp.json()) # 3. Send WhatsApp wa_resp = requests.post( f"{BASE_URL}/api/v2/messages/whatsapp", headers=HEADERS, json={ "recipient": "254700000001", "message": "Hello from Bulkit WhatsApp API v2!", }, timeout=30, ) wa_resp.raise_for_status() print("WhatsApp Queued:", wa_resp.json()) ``` ## Next steps After your first successful request: - review [Generating API Keys](/generating-api-keys) for security guidance - read [Authentication Overview](/authentication/overview) - configure [Webhooks Overview](/webhooks/overview) for real-time delivery and inbox callbacks - explore [WhatsApp API Overview](/whatsapp/overview) - integrate [How to Send SMS](/sms/overview) into your application flow --- # Generating API Keys URL: https://bulkitsms.com/docs/generating-api-keys --- title: "Generating API Keys" description: "Create and manage Bulkit credentials for server-to-server integrations." --- # Generating API Keys Bulkit keys identify your application and authorize requests made on behalf of your account. Use this page when you need to: - create credentials for a new integration - rotate a key after a security incident - separate production and staging access ## Where to generate API keys Generate keys from the Bulkit dashboard in the **API Keys** section (`/api-keys`). ### Creating a Key To create a key, simply click **New API Key** and enter a descriptive **Key Name** (for example, `Production Backend`, `Staging CRM`, or `Zapier Sync`). Each managed API key includes: - `api_key`: Your primary API Token prefixed with `bk_live_`. Pass this as `Authorization: Bearer `. - `api_secret`: An optional secret hash kept for backward compatibility with older legacy dual-header systems. - Usage metadata: real-time last-used timestamp and active toggle status. For all v2 API endpoints (SMS, WhatsApp, Contacts, Credits), you only need the **API Key** passed via `Authorization: Bearer `. You do not need to pass an API Secret. Webhooks are not tied to individual API keys. Bulkit provides a dedicated **Webhooks** tab inside `/api-keys` where you configure two account-level endpoints: **DLR Webhook URL** and **Inbox Webhook URL**. Read more in [Webhooks Overview](/webhooks/overview). ## Why API keys matter API keys allow Bulkit to: - associate requests with your account - enforce access control - track API usage - safely isolate one integration from another ## Recommended key management practices Store your API credentials in environment variables or a secret manager. Never expose your API token in browser-based JavaScript, mobile binaries, or public client-side code. Use the following operational practices: - create separate keys for production and staging - create separate keys for separate applications or services - rotate keys immediately if they are exposed - disable keys you no longer use ## Example environment variable ```bash export BULKIT_API_TOKEN="bk_live_8n6JQv3K1h9Lp0Md" ``` ## Legacy compatibility Bulkit continues to support legacy dual-header credentials (`X-API-Key` and `X-API-Secret`) for older integrations. For new integrations, simply pass `Authorization: Bearer `. ## What to do after key generation After you create a key: 1. save the token in a secure location (e.g. your `.env` file) 2. verify access with [Get Account Balance](/account/get-account-balance) 3. send a test message using [How to Send SMS](/sms/overview) or [WhatsApp Overview](/whatsapp/overview) --- # Authentication Overview URL: https://bulkitsms.com/docs/authentication/overview --- title: "Authentication Overview" description: "Learn how Bulkit authenticates API requests and which method to use in production." --- # Authentication Overview Bulkit v2 authenticates API requests using a simple, single API token passed via the standard `Authorization: Bearer` header. ## Preferred method (Bearer Token) Standard HTTP Bearer authentication is the recommended option for all v2 API endpoints. It keeps requests simple, secure, and compatible with standard HTTP tooling: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` Example request: ```bash curl -X GET "https://api.bulkitsms.com/api/v2/account/credits" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` ## Legacy and alternative methods For backward compatibility, Bulkit continues to support alternative credential formats: ### 1. Dual-header authorization ```http X-API-Key: bk_live_8n6JQv3K1h9Lp0Md X-API-Secret: sk_live_4jPzT5uN8xA1rC6 ``` ### 2. Basic Auth ```http Authorization: Basic ``` ### 3. URL query parameter authorization ```http GET /api/v2/account/credits?apikey=bk_live_8n6JQv3K1h9Lp0Md ``` ## Failed authentication behavior If authentication fails, Bulkit returns an HTTP 401 response: ```json { "status": "error", "message": "Invalid API Key or Secret" } ``` Common causes: - missing `Authorization: Bearer ` header - wrong or deactivated API token - expired session or deleted key ## Choosing the right method - **Always use `Authorization: Bearer `** for new integrations, production services, and high-volume traffic. - Use query parameter `?apikey=` only for quick browser testing when custom headers cannot be set. ## Related pages - [Header Authorization](/authentication/header-authorization) - [URL Authorization](/authentication/url-authorization) --- # Header Authorization URL: https://bulkitsms.com/docs/authentication/header-authorization --- title: "Header Authorization" description: "Authenticate requests using request headers, the preferred method for production integrations." --- # Header Authorization Header authorization is the recommended authentication method for all Bulkit integrations. It keeps your credentials out of URLs and reduces accidental exposure in logs, caches, analytics tools, and browser history. ## Exact header format Bulkit v2 uses a single API token in the standard HTTP `Authorization` header: ```http Authorization: Bearer your_actual_token_here ``` Pass your API Key directly as the Bearer token. No need for dual headers (`X-API-Key` and `X-API-Secret`). For backward compatibility, legacy dual headers and HTTP Basic Auth remain supported. ## Example ```bash curl curl -X GET "https://api.bulkitsms.com/api/v2/account/credits" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` ```python python.py import requests response = requests.get( "https://api.bulkitsms.com/api/v2/account/credits", headers={ "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", }, timeout=30, ) print(response.status_code) print(response.json()) ``` ```go main.go package main import ( "fmt" "io" "net/http" ) func main() { req, err := http.NewRequest(http.MethodGet, "https://api.bulkitsms.com/api/v2/account/credits", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(resp.StatusCode) fmt.Println(string(body)) } ``` ```php php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", ], ]); $response = curl_exec($ch); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); echo $statusCode . PHP_EOL; echo $response . PHP_EOL; ``` ## Legacy Dual Header Support If your system already uses the legacy dual-header approach: ```http X-API-Key: bk_live_8n6JQv3K1h9Lp0Md X-API-Secret: sk_live_4jPzT5uN8xA1rC6 ``` This continues to function seamlessly without breaking existing implementations. ## Best practices - Keep tokens in server-side environment variables (`BULKIT_API_TOKEN`). - Rotate credentials immediately if they are exposed. - Do not reuse the same key across unrelated applications. - Always use HTTPS. --- # URL Authorization URL: https://bulkitsms.com/docs/authentication/url-authorization --- title: "URL Authorization" description: "Authenticate requests with query parameters when header-based access is not practical." --- # URL Authorization Bulkit also supports URL-based authentication for workflows where sending custom headers is difficult. However, using the [`Authorization: Bearer `](/authentication/header-authorization) header is strongly recommended. ## Exact query parameter format ```http ?apikey=your_api_key ``` For backward compatibility, query parameters also accept `?apikey=your_api_key&apisecret=your_api_secret` or `?api_key=your_api_key`. ## Example ```bash GET https://api.bulkitsms.com/api/v2/account/credits?apikey=bk_live_8n6JQv3K1h9Lp0Md ``` ## When to use it URL authorization can be useful for: - simple command-line testing - constrained tools or webhooks that do not support custom headers - quick connectivity checks in internal tooling ## Security caution Avoid URL authorization in production whenever possible. Query parameters are more likely to be captured in logs, proxy traces, analytics tooling, and browser history. ## Example requests ```bash curl curl -X GET "https://api.bulkitsms.com/api/v2/account/credits?apikey=bk_live_8n6JQv3K1h9Lp0Md" ``` ```python python.py import requests response = requests.get( "https://api.bulkitsms.com/api/v2/account/credits", params={ "apikey": "bk_live_8n6JQv3K1h9Lp0Md", }, timeout=30, ) print(response.status_code) print(response.json()) ``` ```go main.go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.bulkitsms.com/api/v2/account/credits?apikey=bk_live_8n6JQv3K1h9Lp0Md" resp, err := http.Get(url) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(resp.StatusCode) fmt.Println(string(body)) } ``` ```php php --- # Get Account Balance URL: https://bulkitsms.com/docs/account/get-account-balance --- title: "Get Account Balance" description: "Retrieve the current credit balance available in your Bulkit wallet." --- # Get Account Balance Use this endpoint to check the current wallet credit balance available on your Bulkit account before submitting SMS or WhatsApp traffic. This is useful for: - pre-flight credit checks before sending campaigns - monitoring available balance in dashboards and billing systems - alerting when your account balance is low ## Endpoint title Get Account Balance ## What this endpoint does Returns the real-time credit balance, last updated timestamp, and wallet currency for the authenticated Bulkit client account. ## HTTP method and path ```http GET /api/v2/account/credits ``` ## Authorization requirements This endpoint supports: - **Bearer token (recommended):** `Authorization: Bearer ` - Legacy headers: `X-API-Key` and `X-API-Secret` - URL query parameter: `?apikey=` Bearer token authorization is preferred. ## Endpoint code block ```http GET https://api.bulkitsms.com/api/v2/account/credits ``` ## Authorization example ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` ## Request examples ```bash curl curl -X GET "https://api.bulkitsms.com/api/v2/account/credits" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` ```python python.py import requests response = requests.get( "https://api.bulkitsms.com/api/v2/account/credits", headers={ "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", }, timeout=30, ) response.raise_for_status() data = response.json() print(f"Available balance: {data['data']['balance']}") ``` ```go main.go package main import ( "encoding/json" "fmt" "net/http" ) type BalanceResponse struct { Status string `json:"status"` Data struct { ClientID string `json:"client_id"` Balance float64 `json:"balance"` Currency string `json:"currency"` LastChange string `json:"last_change,omitempty"` } `json:"data"` } func main() { req, err := http.NewRequest(http.MethodGet, "https://api.bulkitsms.com/api/v2/account/credits", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result BalanceResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Printf("Balance: %.2f %s (Client: %s)\n", result.Data.Balance, result.Data.Currency, result.Data.ClientID) } ``` ```php php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", ], ]); $response = curl_exec($ch); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); $data = json_decode($response, true, 512, JSON_THROW_ON_ERROR); echo "Balance: " . $data["data"]["balance"] . " " . $data["data"]["currency"] . PHP_EOL; ``` ## Parameters This endpoint does not require a JSON request body. ### Authentication parameters | Parameter | Location | Required | Type | Description | | --- | --- | --- | --- | --- | | `Authorization` | Header | Yes (recommended) | string | `Bearer ` | | `X-API-Key` | Header | Optional (legacy) | string | Your Bulkit key | | `X-API-Secret` | Header | Optional (legacy) | string | Your Bulkit secret | | `apikey` | Query | Optional fallback | string | API key used in URL authorization | ## Success response example ```json { "status": "success", "data": { "client_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "balance": 2.97, "last_change": "2026-09-19T06:30:00Z", "currency": "KES" } } ``` ## Error response examples ### Invalid credentials ```json { "status": "error", "message": "Invalid API Key or Secret" } ``` ### Unauthorized request ```json { "status": "error", "message": "Invalid API Key or Secret" } ``` ## Notes and best practices - Check balance before high-volume SMS or WhatsApp broadcasts. - Use `Authorization: Bearer ` in production systems. - Balances are returned as exact floating-point values (e.g. `2.97`), enabling real-time micro-deductions per message unit. - The `currency` field is typically `KES`. --- # Smart Bundles URL: https://bulkitsms.com/docs/account/smart-bundles --- title: "Smart Bundles" description: "Discounted 24-hour credit bundles for small volume senders." --- # Smart Bundles Smart Bundles are low-entry, discounted credit bundles that stay valid for 24 hours from purchase. They let you start sending with as little as KSh 100 without committing to a large, non-expiring credit load. Bundle rates: | Amount (KSh) | Price per SMS (KSh) | SMS included | | ------------ | ------------------- | ------------ | | 100 | 0.80 | 125 | | 300 | 0.70 | 429 | | 500 | 0.60 | 833 | | 1,000 | 0.50 | 2,000 | | 2,000 | 0.40 | 5,000 | ## How bundle credits behave Smart Bundle credits are kept separate from your standard wallet so the expiry rule can never affect regular balances: - **Separate tracking:** Bundle credits are tracked apart from standard, non-expiring credits. Your regular balance and volume rate tier are unchanged. - **Consumed first:** When you send SMS, bundle credits are deducted before standard credits. - **Expiry enforcement:** Unused bundle credits expire at the 24-hour deadline and cannot be recovered. - **Distinct purchase:** Smart Bundles appear as their own option in the dashboard, separate from standard credit top-ups. Plan sends within the 24-hour window. If a bundle is not fully consumed before the deadline, the remaining units expire when the timer ends. ## When to use a Smart Bundle - First campaigns and pilots where you want the lowest possible entry price. - Short, time-boxed broadcasts such as flash offers, event reminders, and same-day alerts. - Testing message content or a new sender ID before loading standard credits. For steady, high-volume traffic, standard credit top-ups remain the better fit: they never expire and unlock lower per-SMS rates as your load tier grows. ## Related resources - Full bundle pricing: [Pricing — Smart Bundles](/pricing#smart-bundles) - Check your balance: [Get Account Balance](/docs/account/get-account-balance) - Standard top-up mechanics: [Instant credit top-ups via mobile money](/blog/mpesa-paybill-topups) --- # How to Send SMS URL: https://bulkitsms.com/docs/sms/overview --- title: "How to Send SMS" description: "Learn the available SMS sending patterns in Bulkit and choose the right integration path." --- # How to Send SMS Bulkit SMS API lets you send one-off or multi-recipient SMS traffic from your own systems using your approved sender IDs and available account balance. Use the SMS API when you need to: - send transactional alerts - send account or workflow notifications - trigger system-generated outbound SMS - schedule a message for later delivery ## Endpoints The main SMS send route documented in this section is: ```http POST /api/v2/messages/sms/bulk ``` ## Authorization Bulkit v2 authenticates SMS requests using a single API token: - **Bearer Token (Recommended)**: `Authorization: Bearer ` - [Header Authorization](/sms/send-sms-using-header-authorization) - [URL Authorization](/sms/send-sms-using-url-params) ## What you need before sending SMS Before calling the SMS API, make sure you have: - an active Bulkit account - a valid API token - enough credit on your account - a valid sender ID assigned to your account - recipient numbers in a supported Kenyan mobile format ## Supported sending patterns Bulkit currently supports: - single-recipient SMS - bulk SMS to multiple recipients - scheduled SMS using timezone-aware timestamps ## Important delivery constraints Bulkit validates several common error conditions before accepting a request. Common API errors include: - `Invalid credentials` - `Insufficient balance` - `Invalid number` - `Invalid sender ID` - `Invalid message` - `Restricted send time` ## Restricted promotional send time Promotional sender IDs cannot send to Safaricom recipients outside the allowed time window. Safaricom promotional traffic is only allowed between 8:00 AM and 6:00 PM Africa/Nairobi time. If you schedule a promotional message to a Safaricom number outside that window, the request will be rejected with: ```json { "status": "error", "message": "Restricted send time" } ``` ## Choose a send flow - [Send SMS Using Header Authorization](/sms/send-sms-using-header-authorization) - [Send SMS Using URL Params](/sms/send-sms-using-url-params) - [Send a Single SMS](/sms/send-a-single-sms) - [Send Bulk SMS](/sms/send-bulk-sms) - [Send SMS to Contacts](/sms/send-sms-to-contacts) - [Send Customized SMS](/sms/send-customized-sms) ## Webhooks and reporting After sending SMS, you can continue with: - [How to Setup SMS Webhook](/sms/how-to-setup-sms-webhook) - [How to Get Delivery Reports](/sms/how-to-get-delivery-reports) - [SMS API Error Messages](/sms/error-messages) ## Error response Typical validation failures use this format: ```json { "status": "error", "message": "Invalid credentials" } ``` --- # Send SMS Using Header Authorization URL: https://bulkitsms.com/docs/sms/send-sms-using-header-authorization --- title: "Send SMS Using Header Authorization" description: "Send SMS requests with API credentials passed in request headers." --- # Send SMS Using Header Authorization Header authorization using a single Bearer token is the recommended way to send SMS from server-side applications because credentials stay out of URLs and logs. ## Endpoints ```http POST /api/v2/messages/sms POST /api/v2/messages/sms/bulk ``` ## Authorization Use standard Bearer authorization: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md Content-Type: application/json ``` Legacy dual headers (`X-API-Key` & `X-API-Secret`) remain supported for existing integrations. ## Request example ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/messages/sms" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -d '{ "sender": "BULK_IT", "mobile": "254700000001", "message": "Your Bulkit verification code is 482193." }' ``` ```python python.py import requests response = requests.post( "https://api.bulkitsms.com/api/v2/messages/sms", headers={ "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", }, json={ "sender": "BULK_IT", "mobile": "254700000001", "message": "Your Bulkit verification code is 482193.", }, timeout=30, ) print(response.json()) ``` ```go main.go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { body, _ := json.Marshal(map[string]interface{}{ "sender": "BULK_IT", "mobile": "254700000001", "message": "Your Bulkit verification code is 482193.", }) req, _ := http.NewRequest(http.MethodPost, "https://api.bulkitsms.com/api/v2/messages/sms", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println(resp.Status) } ``` ```php php "BULK_IT", "mobile": "254700000001", "message": "Your Bulkit verification code is 482193." ]; $ch = curl_init("https://api.bulkitsms.com/api/v2/messages/sms"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR), ]); echo curl_exec($ch); curl_close($ch); ``` ## Notes - use Bearer authorization for backend services and worker processes - pass your sender name directly in `"sender": "BULK_IT"` (or `"sender_id"` with UUID for legacy integrations) - keep API tokens in environment variables, not frontend code - for bulk SMS, use `/api/v2/messages/sms/bulk` with `"mobiles": ["254700000001", "254711111111"]` ## Error response ```json { "status": "error", "message": "Invalid API Key or Secret" } ``` --- # Send SMS Using URL Params URL: https://bulkitsms.com/docs/sms/send-sms-using-url-params --- title: "Send SMS Using URL Params" description: "Send SMS requests when your integration can only pass credentials in query parameters." --- # Send SMS Using URL Params URL parameter authorization is supported for constrained integrations, but it should be used carefully because query strings are easier to log and expose. ## Endpoints ```http POST /api/v2/messages/sms/bulk?apikey=bk_live_... ``` ## Authorization Send your API key as a query parameter: ```http ?apikey=bk_live_... ``` **Legacy compatibility**: Passing both `apikey` and `apisecret` as query parameters (`?apikey=...&apisecret=...`) remains fully supported for backward compatibility. ## Example request ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/messages/sms/bulk?apikey=bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "sender": "BULK_IT", "mobiles": ["254700000001"], "message": "Your order BK-2401 has shipped." }' ``` ```python python.py import requests response = requests.post( "https://api.bulkitsms.com/api/v2/messages/sms/bulk", params={ "apikey": "bk_live_8n6JQv3K1h9Lp0Md", }, json={ "sender": "BULK_IT", "mobiles": ["254700000001"], "message": "Your order BK-2401 has shipped.", }, timeout=30, ) print(response.json()) ``` ```go main.go package main import ( "bytes" "encoding/json" "net/http" ) func main() { body, _ := json.Marshal(map[string]interface{}{ "sender": "BULK_IT", "mobiles": []string{"254700000001"}, "message": "Your order BK-2401 has shipped.", }) req, _ := http.NewRequest(http.MethodPost, "https://api.bulkitsms.com/api/v2/messages/sms/bulk?apikey=bk_live_8n6JQv3K1h9Lp0Md", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") http.DefaultClient.Do(req) } ``` ```php php "BULK_IT", "mobiles" => ["254700000001"], "message": "Your order BK-2401 has shipped." ]; $url = "https://api.bulkitsms.com/api/v2/messages/sms/bulk?apikey=bk_live_8n6JQv3K1h9Lp0Md"; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Content-Type: application/json"], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR), ]); echo curl_exec($ch); curl_close($ch); ``` Prefer header authorization in production. URL params are more likely to appear in logs, browser history, and proxy traces. ## Error response ```json { "status": "error", "message": "Invalid credentials" } ``` --- # Send a Single SMS URL: https://bulkitsms.com/docs/sms/send-a-single-sms --- title: "Send a Single SMS" description: "Send one SMS message to a single recipient." --- # Send a Single SMS Bulkit provides a dedicated single-send SMS endpoint for transactional alerts and OTPs, as well as single-recipient dispatch via the bulk endpoint. ## Primary Endpoint: Single SMS ```http POST /api/v2/messages/sms ``` Send a single SMS using `mobile` as a single phone string. ### Authorization Pass your API token using the standard HTTP `Authorization` header: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` ### Request body ```json { "sender": "BULK_IT", "mobile": "254700000001", "message": "Welcome to Bulkit. Your OTP is 584920." } ``` **Sender Name & Strict Authorization**: Supply your sender ID name directly via `"sender": "BULK_IT"` (or your custom approved brand). For backward compatibility, `"sender_id"` with your UUID or legacy integer ID is also accepted. **No Defaulting**: Senders are strictly authorized against your client account. If you pass an unassigned, unapproved, inactive, or mistyped sender ID or name, the API returns `400 Bad Request` (`Invalid sender ID`) rather than defaulting to the system sender. ### Scheduled Single SMS To schedule delivery for a specific time, supply `scheduled_at` in ISO 8601 format: ```json { "sender": "BULK_IT", "mobile": "254700000001", "message": "Your appointment is tomorrow at 9:00 AM.", "scheduled_at": "2026-03-10T08:00:00+03:00" } ``` ### Success Response ```json { "status": "success", "message": "SMS queued successfully", "data": { "id": "c1f72a6b-8b5d-4f76-92f5-b3c1d94f2910", "cost": 0.40, "units": 1 } } ``` --- ## Alternative: Single Send via Bulk Endpoint You can also send a single message using the bulk route with one number in `mobiles`: ```http POST /api/v2/messages/sms/bulk ``` ### Request body ```json { "sender": "BULK_IT", "mobiles": ["254700000001"], "message": "Welcome to Bulkit." } ``` ### Success response ```json { "status": "success", "message": "Bulk SMS processed", "data": { "requested": 1, "queued": 1, "failed": [] } } ``` ## Error Response ```json { "status": "error", "message": "Invalid sender ID" } ``` ## Common Errors - `Invalid credentials` - `Insufficient balance` - `Invalid number` - `Invalid sender ID` - `Invalid message` - `Restricted send time` --- # Send Bulk SMS URL: https://bulkitsms.com/docs/sms/send-bulk-sms --- title: "Send Bulk SMS" description: "Send SMS messages to multiple recipients simultaneously using your approved sender name." --- # Send Bulk SMS Use the bulk SMS endpoint to deliver messages to multiple recipients in a single API call. It is suitable for: - batch notifications - marketing campaigns and broadcasts - operational announcements - multiple recipient alerts ## Endpoint ```http POST /api/v2/messages/sms/bulk ``` ## Authorization Authenticate using your API token in the `Authorization` HTTP header: ```http Authorization: Bearer Content-Type: application/json ``` **Backward Compatibility**: Legacy `X-API-Key` and `X-API-Secret` headers, as well as credentials passed in the JSON body or query parameters, continue to be supported. ## Request Format Instead of remembering a long UUID `sender_id`, you can simply supply your sender ID name directly via `"sender"`: ```json { "sender": "BULK_IT", "mobiles": [ "254700000001", "254733000002" ], "message": "Hello, thank you for being a valued customer!" } ``` **Sender Flexibility & Strict Authorization**: You can pass your alphanumeric sender name in `"sender"` (e.g. `"BULK_IT"` or `"YOUR_BRAND"`). If you have existing integrations using `"sender_id"` with a UUID or numeric ID, those continue to work. **Strict Authorization**: Senders are strictly verified against your account. If you pass an unassigned, unapproved, inactive, or mistyped sender ID or name, the API returns `400 Bad Request` (`Invalid sender ID`). The system **never** silently defaults to `BULK_IT` unless explicitly requested. ## Scheduled Bulk SMS To schedule a broadcast for future delivery, pass an ISO 8601 timestamp in `scheduled_at`: ```json { "sender": "BULK_IT", "mobiles": [ "254700000001", "254733000002" ], "message": "Flash sale starts at 10:00 AM!", "scheduled_at": "2026-03-10T10:00:00+03:00" } ``` ## Code Examples ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/messages/sms/bulk" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "sender": "BULK_IT", "mobiles": [ "254700000001", "254733000002" ], "message": "Hello! Your monthly statement is ready for viewing." }' ``` ```python python.py import requests headers = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", } payload = { "sender": "BULK_IT", "mobiles": [ "254700000001", "254733000002" ], "message": "Hello! Your monthly statement is ready for viewing." } response = requests.post( "https://api.bulkitsms.com/api/v2/messages/sms/bulk", headers=headers, json=payload, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```go main.go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) type BulkSMSRequest struct { Sender string `json:"sender"` Mobiles []string `json:"mobiles"` Message string `json:"message"` ScheduledAt string `json:"scheduled_at,omitempty"` } func main() { payload := BulkSMSRequest{ Sender: "BULK_IT", Mobiles: []string{"254700000001", "254733000002"}, Message: "Hello! Your monthly statement is ready for viewing.", } body, err := json.Marshal(payload) if err != nil { panic(err) } req, err := http.NewRequest( http.MethodPost, "https://api.bulkitsms.com/api/v2/messages/sms/bulk", bytes.NewReader(body), ) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Printf("%+v\n", result) } ``` ```php php "BULK_IT", "mobiles" => [ "254700000001", "254733000002" ], "message" => "Hello! Your monthly statement is ready for viewing." ]; $ch = curl_init("https://api.bulkitsms.com/api/v2/messages/sms/bulk"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR), ]); $response = curl_exec($ch); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); echo $statusCode . PHP_EOL; echo $response . PHP_EOL; ``` ## Parameters | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `sender` | Yes | string | Approved sender ID name assigned to your account (e.g. `BULK_IT` or `YOUR_BRAND`). | | `mobiles` | Yes | array of strings | List of recipient phone numbers in international or local format (e.g. `["254700000001", "0733000002"]`). | | `message` | Yes | string | SMS text to send. Long messages will automatically be divided into multiple units. | | `scheduled_at` | No | string | Optional ISO 8601 timestamp for scheduled delivery (e.g. `2026-03-10T10:00:00+03:00`). | | `sender_id` | Legacy | string / number | Legacy UUID or numeric ID for backward compatibility with older integrations. | ## Responses ### Success Response ```json { "status": "success", "message": "Bulk SMS processed", "data": { "requested": 2, "queued": 2, "failed": [] } } ``` ### Partial Bulk Failure If one of the numbers is invalid, valid recipients are still queued and invalid ones are reported in `failed`: ```json { "status": "success", "message": "Bulk SMS processed", "data": { "requested": 2, "queued": 1, "failed": [ { "mobile": "12345", "message": "Invalid number" } ] } } ``` ### Error Response ```json { "status": "error", "message": "Invalid sender ID" } ``` ## Common Error Codes - `Invalid credentials`: Missing or invalid Bearer token / API key. - `Insufficient balance`: Account balance is insufficient for the requested message units. - `Invalid number`: None of the supplied numbers are valid phone numbers. - `Invalid sender ID`: Specified sender name or ID does not exist, is inactive, or is not authorized/assigned to your account. - `Restricted send time`: Safaricom promotional window restriction (promotional messages are permitted between 8:00 AM and 6:00 PM Africa/Nairobi time). --- # Send SMS to Contacts URL: https://bulkitsms.com/docs/sms/send-sms-to-contacts --- title: "Send SMS to Contacts" description: "Send SMS to one or more recipients using your Bulkit sender ID and available account credit." --- # Send SMS to Contacts Use this endpoint to send SMS to one or more contact phone numbers from your application. It is suitable for: - transactional SMS - operational alerts - scheduled notifications - sending one message to multiple saved contact numbers ## Endpoint title Send SMS to Contacts ## What this endpoint does Queues an SMS message for one or more recipient numbers using a sender ID assigned to your account. ## Endpoints ```http POST /api/v2/messages/sms/bulk ``` ## Authorization Bulkit recommends authenticating requests using a single Bearer token in the `Authorization` HTTP header: ```http Authorization: Bearer ``` **Legacy compatibility**: Passing `apikey` and `apisecret` directly in the JSON body, or via legacy `X-API-Key` and `X-API-Secret` headers, remains supported for existing integrations. ## HTTP method and path ```http POST /api/v2/messages/sms/bulk ``` ## Authorization requirements - **Recommended**: `Authorization: Bearer ` in the HTTP header. - **Legacy**: `apikey` and `apisecret` provided in the JSON body or custom headers. ## Endpoint code block ```http POST https://api.bulkitsms.com/api/v2/messages/sms/bulk Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md Content-Type: application/json ``` ## Authorization header example ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` ## Error response ```json { "status": "error", "message": "Invalid number" } ``` ## Suggested usage Use this endpoint for both single-recipient and multi-recipient SMS sends when you want one consistent integration pattern. ## Request examples ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/messages/sms/bulk" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "sender": "BULK_IT", "mobiles": [ "254700000001", "254733000002" ], "message": "Hello John, your order #BK-2041 has been dispatched.", "scheduled_at": "2026-03-10T09:15:00+03:00" }' ``` ```python python.py import requests headers = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", } payload = { "sender": "BULK_IT", "mobiles": [ "254700000001", "254733000002" ], "message": "Hello John, your order #BK-2041 has been dispatched.", "scheduled_at": "2026-03-10T09:15:00+03:00" } response = requests.post( "https://api.bulkitsms.com/api/v2/messages/sms/bulk", headers=headers, json=payload, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```go main.go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) type BulkSMSRequest struct { Sender string `json:"sender"` Mobiles []string `json:"mobiles"` Message string `json:"message"` ScheduledAt string `json:"scheduled_at,omitempty"` } func main() { payload := BulkSMSRequest{ Sender: "BULK_IT", Mobiles: []string{"254700000001", "254733000002"}, Message: "Hello John, your order #BK-2041 has been dispatched.", ScheduledAt: "2026-03-10T09:15:00+03:00", } body, err := json.Marshal(payload) if err != nil { panic(err) } req, err := http.NewRequest( http.MethodPost, "https://api.bulkitsms.com/api/v2/messages/sms/bulk", bytes.NewReader(body), ) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println("Status:", resp.StatusCode) var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Printf("%+v\n", result) } ``` ```php php "BULK_IT", "mobiles" => [ "254700000001", "254733000002" ], "message" => "Hello John, your order #BK-2041 has been dispatched.", "scheduled_at" => "2026-03-10T09:15:00+03:00" ]; $ch = curl_init("https://api.bulkitsms.com/api/v2/messages/sms/bulk"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR), ]); $response = curl_exec($ch); $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); echo $statusCode . PHP_EOL; echo $response . PHP_EOL; ``` ## Parameters | Parameter | Required | Type | Description | Validation | | --- | --- | --- | --- | --- | | `sender` | Yes | string | Sender ID name assigned to your account (e.g. `BULK_IT`) | Must exist, be active, and belong to your account | | `mobiles` | Yes | array of strings | One or more recipient mobile numbers | Use normalized Kenyan numbers such as `254700000001` | | `message` | Yes | string | SMS body to send to all recipients in the request | Cannot be blank | | `scheduled_at` | No | string | Future send time | Use timezone-aware ISO 8601 such as `2026-03-10T09:15:00+03:00` | | `sender_id` | Legacy | string / int | Legacy UUID or numeric Sender ID | Backward-compatible alternative to `sender` | | `apikey` | Legacy | string | Legacy body authentication key | Optional when using `Authorization: Bearer` | | `apisecret` | Legacy | string | Legacy secret paired with API key | Optional when using `Authorization: Bearer` | ## Success response example ```json { "status": "success", "message": "Bulk SMS processed", "data": { "requested": 2, "queued": 2, "failed": [] } } ``` ## Error response examples ### Invalid credentials ```json { "status": "error", "message": "Invalid credentials" } ``` ### Insufficient balance ```json { "status": "error", "message": "Insufficient balance" } ``` ### Invalid number ```json { "status": "error", "message": "Invalid number" } ``` ### Invalid sender ID Returned when the specified sender ID or name is invalid, inactive, or not authorized for your account. Bulkit does not silently fall back to system shortcodes. ```json { "status": "error", "message": "Invalid sender ID" } ``` ### Invalid message ```json { "status": "error", "message": "Invalid message" } ``` ### Restricted send time ```json { "status": "error", "message": "Restricted send time" } ``` ### Partial bulk failure example ```json { "status": "success", "message": "Bulk SMS processed", "data": { "requested": 3, "queued": 2, "failed": [ { "mobile": "12345", "message": "Invalid number" } ] } } ``` ## Notes and best practices - validate and normalize phone numbers before sending - keep sender IDs mapped to the right environment and use case - check balance before high-volume sends - use `scheduled_at` with timezone offsets to avoid ambiguity - if you use a promotional sender ID, avoid scheduling Safaricom traffic outside 8:00 AM to 6:00 PM Africa/Nairobi time ## Single-recipient alternative If you are sending to one recipient only, Bulkit also provides: ```http POST /api/v2/messages/sms ``` Use the bulk endpoint when you want one consistent integration for both single and multi-recipient sends. --- # Send Customized SMS URL: https://bulkitsms.com/docs/sms/send-customized-sms --- title: "Send Customized SMS" description: "Send personalized SMS messages using contact data and placeholders." --- # Send Customized SMS Customized SMS lets you merge recipient data into the final message before it is sent. ## Endpoints ```http POST /api/v2/messages/sms/bulk ``` Use the standard bulk SMS endpoint with `custom_data` to render one message per recipient. ## Authorization Pass your API token using standard HTTP Bearer authentication: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md Content-Type: application/json ``` For backward compatibility, legacy body credentials (`apikey` and `apisecret`) or legacy headers (`X-API-Key` & `X-API-Secret`) remain supported. ## Supported placeholder formats Bulkit supports both: - `{{first_name}}` - `__FIRST_NAME__` Common aliases like `Number`, `Phone`, and `Mobile` resolve to the recipient number. ## Example template ```text Hello {{first_name}}, your account __ACCOUNT__ is due on {{due_date}}. ``` ## Example cURL Request ```bash curl -X POST "https://api.bulkitsms.com/api/v2/messages/sms/bulk" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "sender": "BULK_IT", "mobiles": ["254700000001", "254733000002"], "message": "Hello {{first_name}}, your balance is {{balance}}.", "custom_data": [ { "Mobile": "254700000001", "first_name": "Amina", "balance": "KES 1,240" }, { "Mobile": "254733000002", "first_name": "Brian", "balance": "KES 560" } ] }' ``` ## How it works - the backend matches each `custom_data` row to a recipient by mobile number - placeholders are rendered before the message is stored and queued - outbox and history show the final rendered message, not the raw template ## Notes - make sure each custom data row contains the correct recipient number - use consistent field names across all rows - if a placeholder value is missing, the unresolved token may remain in the message ## Error response ```json { "status": "error", "message": "Invalid message" } ``` --- # How to Setup SMS Webhook URL: https://bulkitsms.com/docs/sms/how-to-setup-sms-webhook --- title: "How to Setup SMS Webhook" description: "Configure webhook endpoints for inbound SMS and delivery status callbacks." --- # How to Setup SMS Webhook Bulkit supports webhook-based integrations for inbound SMS workflows and delivery-status updates. Use SMS webhooks when you need to: - receive inbox messages in your application - route shortcode traffic to your backend - react to keyword messages automatically - track final delivery status for outbound SMS ## Configuration Webhooks in Bulkit are configured at the account level in the dashboard under **API Keys** -> **Webhooks** tab (`/api-keys`). You can configure two shared URLs across your account: - **DLR Webhook URL**: Receives delivery reports (`sms.delivered`, `sms.failed`, `sms.blacklisted`) and WhatsApp receipt updates. - **Inbox Webhook URL**: Receives inbound customer replies (`sms.received` / `sms.inbound`) and WhatsApp messages. For full payload schemas, examples, and headers, see the [Webhooks Overview](/webhooks/overview). ## Headers and Authorization Bulkit webhook dispatches include the following standard header: - `X-Bulkit-Webhook-Event`: The specific event type (e.g. `sms.delivered`, `sms.failed`, `sms.blacklisted`, `sms.received`). - `Content-Type`: `application/json` Webhooks should be served over HTTPS. Bulkit no longer enforces complex signing secrets, allowing you to easily process events or use standard authentication tokens in your webhook URL query strings if desired. ## Webhook categories ### Inbox webhooks - [Inbound Messages Webhook Guide](/webhooks/inbound-messages) - [Shortcode Inbox SMS Webhook](/sms/shortcode-inbox-sms-webhook) - [Keyword Inbox SMS Webhook](/sms/keyword-inbox-sms-webhook) ### Delivery webhooks - [Delivery Reports Webhook Guide](/webhooks/delivery-reports) - [How to Get Delivery Reports](/sms/how-to-get-delivery-reports) ## Best practices - Always expose a public HTTPS endpoint. - Inspect the `X-Bulkit-Webhook-Event` header to route payloads internally. - Respond quickly with `200 OK` (within 5 seconds). - Process incoming payloads asynchronously using a background queue or worker. - Handle delivery report events such as `sms.delivered`, `sms.failed`, and `sms.blacklisted`. ## Error response Return a `2xx` status after you accept the webhook payload. Avoid long-running processing before the response, and treat `4xx` and `5xx` responses as failures that may require replay or investigation. --- # Shortcode Inbox SMS Webhook URL: https://bulkitsms.com/docs/sms/shortcode-inbox-sms-webhook --- title: "Shortcode Inbox SMS Webhook" description: "Receive inbound SMS sent to your shortcode in your application." --- # Shortcode Inbox SMS Webhook Use a shortcode inbox webhook when you want inbound messages sent to your Bulkit shortcode delivered directly to your application. ## Endpoints Bulkit sends inbound shortcode payloads to your configured webhook URL. ## Authorization Bulkit does not require you to return API credentials in the webhook response. Secure the receiving endpoint with HTTPS and your own verification checks. ## Typical flow 1. A mobile subscriber sends an SMS to your shortcode. 2. Bulkit receives the inbound message. 3. Bulkit forwards the message payload to your webhook URL. 4. Your application stores or processes the inbound message. ## Example webhook payload ```json { "event": "sms.inbound", "mobile": "254700000001", "shortcode": "BULK_IT", "message": "Hello", "received_at": "2026-03-09T15:00:00+03:00" } ``` ## Expected webhook response Your endpoint should return a `200 OK` as soon as the payload is accepted. ```json { "status": "received" } ``` ## Notes - use a stable public HTTPS URL - keep webhook handlers idempotent - store the raw payload for troubleshooting and replay ## Error response Return a `2xx` response after you accept the inbound message. Non-`2xx` responses should be treated as webhook handling failures. --- # Keyword Inbox SMS Webhook URL: https://bulkitsms.com/docs/sms/keyword-inbox-sms-webhook --- title: "Keyword Inbox SMS Webhook" description: "Receive inbound SMS and route messages by keyword." --- # Keyword Inbox SMS Webhook Keyword inbox webhooks are useful when your application reacts differently based on the first word or command in the incoming message. ## Endpoints Bulkit delivers keyword inbox messages to your configured webhook URL. ## Authorization No API key headers are required on Bulkit webhook callbacks. Secure the endpoint with HTTPS and your own request validation. ## Example If a user sends: ```text BALANCE 12345 ``` Your webhook can inspect the keyword `BALANCE` and trigger the matching workflow. ## Example payload ```json { "event": "sms.inbound", "mobile": "254700000001", "shortcode": "BULK_IT", "keyword": "BALANCE", "message": "BALANCE 12345", "received_at": "2026-03-09T15:04:00+03:00" } ``` ## Implementation tips - normalize keywords to uppercase before routing - validate command arguments before executing business logic - avoid long synchronous work in the webhook response path ## Error response Return `200 OK` once the payload is accepted. A non-`2xx` response means your webhook did not successfully receive the event. --- # How to Get Delivery Reports URL: https://bulkitsms.com/docs/sms/how-to-get-delivery-reports --- title: "How to Get Delivery Reports" description: "Track delivery updates for outbound SMS messages." --- # How to Get Delivery Reports Delivery reports let your application know whether a submitted SMS was delivered, failed, or is still pending. ## Endpoints This page does not expose a Bulkit send endpoint. Delivery reports are posted to the webhook URL you configure in your application. ## Authorization Bulkit does not send your API credentials back to your webhook. Protect the endpoint with your own controls such as: - HTTPS only - IP allowlisting - URL query authorization tokens if desired - Idempotent request handling ## What delivery reports are used for - updating your internal message state - reconciling support tickets - triggering retries or fallback channels - reporting on campaign performance Configure your **DLR Webhook URL** under **API Keys** -> **Webhooks** in the dashboard. Once set, Bulkit forwards all delivery reports directly to that endpoint. For detailed documentation, visit [Delivery Reports Guide](/webhooks/delivery-reports). ## Example delivery report payload ```json { "event": "sms.delivered", "message_id": "451980165", "internal_message_id": 144, "outbox_id": 33, "mobile": "254754424353", "status": "delivered", "description": "DeliveredToTerminal", "response_code": "200", "response_description": "Success", "delivered_at": "2026-03-09T11:21:06Z", "blacklisted": false, "sender_id": 12, "network_id": 2, "tat": "0.45 sec" } ``` ## Failure example ```json { "event": "sms.failed", "message_id": "451980166", "internal_message_id": 145, "outbox_id": 34, "mobile": "254700000001", "status": "failed", "description": "Delivery failed at network", "response_code": "500", "response_description": "Network delivery failure", "delivered_at": null, "blacklisted": false, "sender_id": 12 } ``` ## Blacklisted example If QuickSMS returns `SenderName Blacklisted`, Bulkit forwards a blacklist-aware payload and records that MSISDN against the sender blacklist. ```json { "event": "sms.blacklisted", "message_id": "452719776", "internal_message_id": 146, "outbox_id": 35, "mobile": "254700000001", "status": "blacklisted", "description": "SenderName Blacklisted", "response_code": "200", "response_description": "SenderName Blacklisted", "delivered_at": "2026-03-10T04:35:20Z", "blacklisted": true, "blacklist_reason": "SenderName Blacklisted", "sender_id": 12, "network_id": 1, "tat": "0.18 sec" } ``` ## Recommended handling - store the delivery report against your internal message ID - treat webhook delivery as asynchronous and eventually consistent - keep the provider message ID for support and traceability - handle `status: "blacklisted"` separately if you want to stop future sends for that sender and recipient pair ## Webhook headers Bulkit includes standard HTTP headers when forwarding delivery reports: - `Content-Type: application/json` - `X-Bulkit-Webhook-Event: sms.delivered | sms.failed | sms.blacklisted | whatsapp.receipt.updated` ## Error response Your webhook should return a `2xx` HTTP status code (such as `200 OK`) immediately upon receiving the payload. Any `4xx` or `5xx` response indicates to Bulkit that your server failed to process the callback. --- # SMS API Error Messages URL: https://bulkitsms.com/docs/sms/error-messages --- title: "SMS API Error Messages" description: "Reference the standardized SMS API errors returned by Bulkit and understand how to handle them." --- # SMS API Error Messages Bulkit returns a fixed set of SMS API validation errors for common integration failures. These messages are designed to be stable, readable, and easy to map in application logic. ## Endpoints These errors apply to the documented SMS send endpoints, especially: ```http POST /api/v2/messages/sms/bulk ``` ## Authorization The error payload format is the same whether you authorize by JSON body, headers, or URL query parameters. ## Error response ```json { "status": "error", "message": "Invalid sender ID" } ``` ## Standard SMS API errors ### Invalid credentials Returned when the supplied API key and secret cannot be authenticated. ```json { "status": "error", "message": "Invalid credentials" } ``` ### Insufficient balance Returned when your account does not have enough credit to queue the requested SMS traffic. ```json { "status": "error", "message": "Insufficient balance" } ``` ### Invalid number Returned when one or more recipient phone numbers are not in a supported format. ```json { "status": "error", "message": "Invalid number" } ``` ### Invalid sender ID Returned when the sender ID or sender name is missing, invalid, inactive, or not authorized for your account. ```json { "status": "error", "message": "Invalid sender ID" } ``` **Strict Authorization & No Fallback**: Bulkit strictly validates sender ownership and multi-client assignments across all API endpoints (v2 and legacy). If you specify a sender ID or sender name that does not exist, is inactive, or is not assigned to your account, the API immediately returns HTTP `400 Bad Request` (`Invalid sender ID`). The system **does not** silently fall back or default to system senders (`BULK_IT`). If you intend to use the default system shortcode, you must explicitly supply `"sender": "BULK_IT"`. ### Invalid message Returned when the SMS body is blank or otherwise fails validation. ```json { "status": "error", "message": "Invalid message" } ``` ### Restricted send time Returned when a promotional sender ID attempts to send to a Safaricom recipient outside the allowed send window. ```json { "status": "error", "message": "Restricted send time" } ``` ## Safaricom promotional traffic rule Safaricom promotional messages can only be sent between: - 8:00 AM - 6:00 PM Timezone: - `Africa/Nairobi` This rule applies to: - immediate sends made outside the allowed window - scheduled sends where `scheduled_at` falls outside the allowed window ## Partial failures in bulk SMS Bulk SMS can succeed for some recipients and fail for others. In that case, Bulkit returns a successful top-level response with a per-recipient `failed` array. ```json { "status": "success", "message": "Bulk SMS processed", "data": { "requested": 3, "queued": 2, "failed": [ { "mobile": "12345", "message": "Invalid number" } ] } } ``` ## Best practices - validate phone numbers before sending - verify available balance before high-volume requests - confirm sender IDs are assigned to the right account - use timezone-aware timestamps for scheduled sends - expect recipient-level failures in bulk workflows --- # WhatsApp API Overview URL: https://bulkitsms.com/docs/whatsapp/overview --- title: "WhatsApp API Overview" description: "Send direct WhatsApp messages, media files, and personalized broadcasts via API v2." --- # WhatsApp API Overview The Bulkit WhatsApp API v2 allows businesses and developers to send direct WhatsApp messages, media attachments (images, PDFs, documents, audio, video), and personalized bulk broadcasts directly from their connected WhatsApp accounts. ## Key Capabilities - **Single Message Dispatch**: Send text notifications, OTP codes, order confirmations, and receipts. - **Rich Media**: Send PDFs (invoices, statements), images, audio notes, and video attachments up to 50MB. - **Bulk Broadcasts**: Send personalized broadcasts to multiple recipients with dynamic `{variables}` and rate limiting. - **Two-way Inbox**: Fetch incoming customer replies via REST API or subscribe via real-time webhooks. - **Automated Delivery Tracking**: Receive `whatsapp.message.sent`, `whatsapp.receipt.updated` (delivered, read/blue-tick), and `whatsapp.message.failed` webhook events. ## Prerequisites Before calling the WhatsApp API: 1. **Connect your WhatsApp Account**: Go to the Bulkit dashboard at `/whatsapp` and scan the QR code with your WhatsApp or WhatsApp Business mobile app (**Linked Devices** -> **Link a Device**). 2. **Generate API Token**: Navigate to **API Keys** in the dashboard to generate your API token (`bk_live_...`). 3. **Verify Connection**: Call the `GET /api/v2/whatsapp/status` endpoint to confirm your session is active. Bulkit connects directly through WhatsApp Web protocols. All messages are dispatched from your connected phone number, maintaining your brand identity and allowing seamless two-way chat. ## Endpoints Summary | Method | Endpoint | Description | | --- | --- | --- | | `POST` | `/api/v2/messages/whatsapp` | Send a single WhatsApp message (text, media URL, or file upload) | | `POST` | `/api/v2/messages/whatsapp/bulk` | Send personalized WhatsApp broadcast to multiple recipients | | `GET` | `/api/v2/messages/whatsapp` | Query sent and received message history with filtering | | `GET` | `/api/v2/messages/whatsapp/inbox` | Retrieve inbound customer replies | | `GET` | `/api/v2/messages/whatsapp/:id` | Get single message status and details | | `GET` | `/api/v2/messages/whatsapp/:id/media` | Download decrypted media attachment | | `GET` | `/api/v2/whatsapp/status` | Check device pairing and session connection status | ## Authentication All WhatsApp endpoints authenticate using the standard HTTP `Authorization: Bearer` header: - **Bearer Token (Recommended)**: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` - **Legacy Dual Headers**: ```http X-API-Key: bk_live_8n6JQv3K1h9Lp0Md X-API-Secret: sk_live_4jPzT5uN8xA1rC6 ``` - **HTTP Basic Auth**: ```http Authorization: Basic ``` - **Query Parameter** (for `GET` endpoints): ```http ?apikey=bk_live_8n6JQv3K1h9Lp0Md ``` ## Supported Media Types Bulkit supports sending attachments up to **50MB** in size: - **Images**: JPEG, PNG, WEBP, GIF - **Documents**: PDF, DOCX, XLSX, CSV, TXT - **Audio**: MP3, OGG, WAV, M4A, AAC - **Video**: MP4, MOV, 3GP ## Webhook Events Bulkit automatically emits real-time events to your configured **DLR Webhook** and **Inbox Webhook**: - `whatsapp.message.received`: When a customer messages your connected WhatsApp account. - `whatsapp.receipt.updated`: When a message is delivered or read (`status`: `"delivered"` or `"read"`). See [Webhooks Overview](/webhooks/overview) for details on setting up webhooks. --- # Send WhatsApp Message URL: https://bulkitsms.com/docs/whatsapp/send-message --- title: "Send WhatsApp Message" description: "Send a text message to a single WhatsApp recipient using API v2." --- # Send WhatsApp Message Use this endpoint to dispatch a direct WhatsApp text message to any phone number from your connected WhatsApp account. ## Endpoint ```http POST /api/v2/messages/whatsapp ``` ## Authorization Header authorization is recommended for all requests: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md Content-Type: application/json ``` ## Request Parameters | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `recipient` | Yes | string | Destination phone number in international format without `+` (e.g. `254700000001`). Aliases: `mobile`, `phone`. | | `message` | Yes | string | Plain text message body. Standard WhatsApp markdown is supported (`*bold*`, `_s_`, `~strikethrough~`, `\`\`\`code\`\`\``). Aliases: `text`. | ## Request Examples ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/messages/whatsapp" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "recipient": "254700000001", "message": "Hello! Your payment of KES 1,500 has been received successfully. Receipt: *#REC-8421*" }' ``` ```python Python import requests url = "https://api.bulkitsms.com/api/v2/messages/whatsapp" headers = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", } payload = { "recipient": "254700000001", "message": "Hello! Your payment of KES 1,500 has been received successfully. Receipt: *#REC-8421*", } response = requests.post(url, json=payload, headers=headers, timeout=30) response.raise_for_status() print(response.json()) ``` ```javascript Node.js const response = await fetch("https://api.bulkitsms.com/api/v2/messages/whatsapp", { method: "POST", headers: { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", }, body: JSON.stringify({ recipient: "254700000001", message: "Hello! Your payment of KES 1,500 has been received successfully. Receipt: *#REC-8421*", }), }); const data = await response.json(); console.log(data); ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { payload := map[string]string{ "recipient": "254700000001", "message": "Hello! Your payment of KES 1,500 has been received successfully.", } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", "https://api.bulkitsms.com/api/v2/messages/whatsapp", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() fmt.Println("Status:", resp.Status) } ``` ```php PHP "254700000001", "message" => "Hello! Your payment of KES 1,500 has been received successfully.", ]; $ch = curl_init("https://api.bulkitsms.com/api/v2/messages/whatsapp"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($payload), ]); $response = curl_exec($ch); curl_close($ch); echo $response; ``` ## Success Response Example ```json { "status": "success", "message": "WhatsApp message sent successfully", "data": { "id": "7b8f844a-f5e2-4144-84c4-f655e88b2a1a", "message_id": "3EB0C34B8F56D254", "recipient": "254700000001", "chat_jid": "254700000001@s.whatsapp.net", "text": "Hello! Your payment of KES 1,500 has been received successfully. Receipt: *#REC-8421*", "media_type": "text", "media_url": "", "file_name": "", "file_size": 0, "mime_type": "", "cost": 0.03, "currency": "KES", "status": "sent", "timestamp": "2026-09-19T06:30:00Z" } } ``` ## Error Responses ### WhatsApp Device Disconnected If your WhatsApp account is not paired or disconnected in the dashboard: ```json { "status": "error", "message": "WhatsApp client is not connected. Please scan QR code in settings to reconnect." } ``` ### Insufficient Balance ```json { "status": "error", "message": "Insufficient balance" } ``` ### Missing Recipient or Message ```json { "status": "error", "message": "recipient or mobile number is required" } ``` --- # Send WhatsApp Media URL: https://bulkitsms.com/docs/whatsapp/send-media --- title: "Send WhatsApp Media" description: "Send images, PDF invoices, documents, audio notes, or video attachments via WhatsApp API v2." --- # Send WhatsApp Media Bulkit allows you to send media attachments up to **50MB** to WhatsApp recipients. You can send media using two methods: 1. **Remote Media URL (`application/json`)**: Bulkit fetches the file from your server or cloud storage (e.g. S3, GCS) and delivers it via WhatsApp. 2. **Multipart File Upload (`multipart/form-data`)**: Direct binary upload from your server. ## Endpoint ```http POST /api/v2/messages/whatsapp ``` ## Method 1: Remote Media URL (Recommended) Pass the publicly accessible URL of your file in `media_url`. Bulkit downloads, encrypts, and delivers the file as a native WhatsApp attachment. ### Request Body (`application/json`) ```json { "recipient": "254700000001", "media_url": "https://example.com/invoices/INV-2026-001.pdf", "file_name": "Invoice-September.pdf", "message": "Hi Jane, please find attached your monthly statement." } ``` ### Parameters | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `recipient` | Yes | string | Destination phone number (e.g. `254700000001`). Aliases: `mobile`, `phone`. | | `media_url` | Yes | string | Fully-qualified public HTTP/HTTPS URL of the file (max 50MB). | | `file_name` | No | string | Custom display file name (especially useful for PDFs and documents). | | `message` | No | string | Optional caption sent alongside the media. Aliases: `caption`, `text`. | ### Example with cURL ```bash curl -X POST "https://api.bulkitsms.com/api/v2/messages/whatsapp" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "recipient": "254700000001", "media_url": "https://assets.example.com/flyer.png", "message": "Check out our weekend special offers! 🔥" }' ``` --- ## Method 2: Multipart File Upload Directly upload the binary file from your local disk using `multipart/form-data`. ### Request Parameters (`multipart/form-data`) | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `recipient` | Yes | string | Destination phone number (e.g. `254700000001`). | | `file` | Yes | file | The binary file to send (max 50MB). | | `message` | No | string | Optional caption for the attachment. | ### Example with cURL ```bash curl -X POST "https://api.bulkitsms.com/api/v2/messages/whatsapp" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -F "recipient=254700000001" \ -F "file=@/path/to/receipt.pdf" \ -F "message=Here is your official receipt." ``` ### Python Example (Multipart Upload) ```python import requests url = "https://api.bulkitsms.com/api/v2/messages/whatsapp" headers = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", } data = { "recipient": "254700000001", "message": "Here is your booking confirmation voucher.", } with open("voucher.pdf", "rb") as f: files = {"file": ("voucher.pdf", f, "application/pdf")} response = requests.post(url, data=data, files=files, headers=headers) print(response.json()) ``` --- ## Success Response ```json { "status": "success", "message": "WhatsApp media message sent successfully", "data": { "id": "2d8f993e-c651-46ab-bb12-98442e9fa501", "message_id": "3EB0E56A1B29D381", "recipient": "254700000001", "chat_jid": "254700000001@s.whatsapp.net", "text": "Hi Jane, please find attached your monthly statement.", "media_type": "document", "media_url": "/api/v2/messages/whatsapp/2d8f993e-c651-46ab-bb12-98442e9fa501/media", "file_name": "Invoice-September.pdf", "file_size": 245760, "mime_type": "application/pdf", "cost": 0.03, "currency": "KES", "status": "sent", "timestamp": "2026-09-19T06:35:00Z" } } ``` The `media_url` in the response provides a secure link to re-download the decrypted media attachment via Bulkit API v2 at any time. --- # Send Bulk WhatsApp Broadcast URL: https://bulkitsms.com/docs/whatsapp/send-bulk --- title: "Send Bulk WhatsApp Broadcast" description: "Send personalized WhatsApp broadcast campaigns to multiple mobile numbers simultaneously." --- # Send Bulk WhatsApp Broadcast Use this endpoint to dispatch broadcast messages to multiple recipients. Bulkit supports templating with custom parameters so each contact receives a personalized message. ## Endpoint ```http POST /api/v2/messages/whatsapp/bulk ``` ## Authorization ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md Content-Type: application/json ``` ## Request Parameters | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `mobiles` | Yes | array of strings | List of recipient phone numbers in international format (e.g. `["254700000001", "254711111111"]`). | | `message` | Yes | string | Broadcast message content. You can include placeholders in `{tag}` syntax (e.g. `{first_name}`, `{account}`, `{amount}`). | | `custom_data` | No | array of objects | Optional per-recipient data matching the order of the `mobiles` array. Each key replaces `{key}` in the message. | ## Request Examples ### 1. Simple Broadcast (Identical Message) ```bash curl -X POST "https://api.bulkitsms.com/api/v2/messages/whatsapp/bulk" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "mobiles": ["254700000001", "254711111111", "254722222222"], "message": "Dear customer, our offices will be closed on Monday for the public holiday." }' ``` ### 2. Personalized Broadcast with Custom Data ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/messages/whatsapp/bulk" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "mobiles": [ "254700000001", "254711111111" ], "message": "Hello {first_name}, your policy #{policy_no} renewal of KES {amount} is due on {due_date}.", "custom_data": [ { "first_name": "Alice", "policy_no": "POL-9921", "amount": "3,500", "due_date": "25th Sep" }, { "first_name": "Bob", "policy_no": "POL-4410", "amount": "5,200", "due_date": "28th Sep" } ] }' ``` ```python Python import requests url = "https://api.bulkitsms.com/api/v2/messages/whatsapp/bulk" headers = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", } payload = { "mobiles": ["254700000001", "254711111111"], "message": "Hello {name}, your monthly statement for account #{account} is ready.", "custom_data": [ {"name": "Alice", "account": "ACC-1001"}, {"name": "Bob", "account": "ACC-1002"}, ], } response = requests.post(url, json=payload, headers=headers) print(response.json()) ``` ```javascript Node.js const response = await fetch("https://api.bulkitsms.com/api/v2/messages/whatsapp/bulk", { method: "POST", headers: { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", }, body: JSON.stringify({ mobiles: ["254700000001", "254711111111"], message: "Hello {name}, your package #{tracking} is on the way.", custom_data: [ { name: "Alice", tracking: "TRK-01" }, { name: "Bob", tracking: "TRK-02" }, ], }), }); console.log(await response.json()); ``` ## Success Response Example ```json { "status": "success", "message": "Bulk WhatsApp messages queued for delivery", "data": { "total_recipients": 2, "cost_per_message": 0.03, "total_cost": 0.06, "currency": "KES" } } ``` ## How Delivery Works 1. Bulkit calculates the total required credits and checks your wallet balance. 2. The broadcast job queues asynchronously in the background. 3. Bulkit renders each personalized message dynamically and dispatches them through your paired WhatsApp session. 4. As messages are delivered and read by recipients, delivery receipts are forwarded to your configured **DLR Webhook URL**. --- # WhatsApp Inbox & Messages API URL: https://bulkitsms.com/docs/whatsapp/inbox-and-messages --- title: "WhatsApp Inbox & Messages API" description: "Query message logs, retrieve incoming customer messages, view message status, and download media." --- # WhatsApp Inbox & Messages API Bulkit provides full visibility into your WhatsApp traffic, enabling you to inspect outbound and inbound messages, review customer replies, and retrieve decrypted attachments. --- ## 1. Query Messages History Retrieve a paginated list of sent and received WhatsApp messages with flexible search and filtering. ### Endpoint ```http GET /api/v2/messages/whatsapp ``` ### Query Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `page` | integer | `1` | Page number for pagination. | | `limit` | integer | `50` | Items per page (max `100`). | | `direction` | string | `all` | Filter by message direction: `all`, `inbound` (or `inbox`), `outbound` (or `sent`). | | `mobile` | string | - | Filter by recipient or sender phone number (e.g. `254700000001`). Aliases: `phone`, `chat_jid`. | | `status` | string | - | Filter by status: `sent`, `delivered`, `read`, `failed`, `pending`. | | `start_date` | string | - | Filter messages sent after this ISO 8601 timestamp (e.g. `2026-09-01T00:00:00Z`). | | `end_date` | string | - | Filter messages sent before this ISO 8601 timestamp. | ### Example Request ```bash curl -X GET "https://api.bulkitsms.com/api/v2/messages/whatsapp?direction=outbound&limit=10" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` ### Example Response ```json { "status": "success", "data": [ { "id": "7b8f844a-f5e2-4144-84c4-f655e88b2a1a", "message_id": "3EB0C34B8F56D254", "direction": "outbound", "from": "254700000000", "to": "254700000001@s.whatsapp.net", "chat_jid": "254700000001@s.whatsapp.net", "text": "Hello! Your payment has been received.", "media_type": "text", "media_url": "", "file_name": "", "file_size": 0, "mime_type": "", "status": "delivered", "cost": 0.03, "timestamp": "2026-09-19T06:30:00Z" } ], "meta": { "page": 1, "limit": 10, "total": 45 } } ``` --- ## 2. Retrieve Incoming WhatsApp Inbox A dedicated shortcut endpoint returning only inbound messages sent by your customers to your WhatsApp number. ### Endpoint ```http GET /api/v2/messages/whatsapp/inbox ``` ### Query Parameters Accepts the same pagination (`page`, `limit`) and filtering (`mobile`, `start_date`, `end_date`) parameters as the messages history endpoint. ### Example Request ```bash curl -X GET "https://api.bulkitsms.com/api/v2/messages/whatsapp/inbox?page=1&limit=20" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` --- ## 3. Get Single Message Details Fetch the full metadata and status for a specific message using either its internal UUID or the WhatsApp message ID. ### Endpoint ```http GET /api/v2/messages/whatsapp/:id ``` ### Path Parameter | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `:id` | Yes | string | Internal Bulkit message UUID or WhatsApp message ID (e.g. `3EB0C34B8F56D254`). | ### Example Request ```bash curl -X GET "https://api.bulkitsms.com/api/v2/messages/whatsapp/7b8f844a-f5e2-4144-84c4-f655e88b2a1a" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` --- ## 4. Download Decrypted Media Attachment When receiving or sending files (images, PDFs, documents, audio, video), the media content is stored securely encrypted. Use this endpoint with your API credentials to download or stream the decrypted binary data. ### Endpoint ```http GET /api/v2/messages/whatsapp/:id/media ``` ### Query Parameters | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `download` | string | `0` | If set to `1`, sets `Content-Disposition: attachment` to trigger a file download in browsers. Defaults to `inline`. | ### Example Request ```bash curl -X GET "https://api.bulkitsms.com/api/v2/messages/whatsapp/7b8f844a-f5e2-4144-84c4-f655e88b2a1a/media" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ --output downloaded_attachment.pdf ``` --- # WhatsApp Connection Status URL: https://bulkitsms.com/docs/whatsapp/connection-status --- title: "WhatsApp Connection Status" description: "Check if your WhatsApp phone session is actively paired and connected." --- # WhatsApp Connection Status Before dispatching time-sensitive messages or large campaigns, you can verify that your WhatsApp device session is paired and connected. ## Endpoint ```http GET /api/v2/whatsapp/status ``` ## Authorization ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` ## Request Example ```bash curl curl -X GET "https://api.bulkitsms.com/api/v2/whatsapp/status" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` ```python Python import requests response = requests.get( "https://api.bulkitsms.com/api/v2/whatsapp/status", headers={ "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", }, timeout=10, ) print(response.json()) ``` ```javascript Node.js const res = await fetch("https://api.bulkitsms.com/api/v2/whatsapp/status", { headers: { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", }, }); const status = await res.json(); console.log(status); ``` ## Success Response (Connected) When your phone is paired and the socket connection is healthy: ```json { "status": "success", "data": { "connected": true, "phone": "254712345678", "push_name": "My Business", "status": "connected", "last_active_at": "2026-09-19T06:38:15Z" } } ``` ## Success Response (Disconnected) When no active device is linked, or the phone was unlinked from the WhatsApp mobile app: ```json { "status": "success", "data": { "connected": false, "status": "disconnected" } } ``` ## How to Reconnect If `connected` is `false`, log in to the Bulkit dashboard at `/whatsapp` and scan the QR code using your WhatsApp mobile app under **Linked Devices**. --- # Webhooks Overview URL: https://bulkitsms.com/docs/webhooks/overview --- title: "Webhooks Overview" description: "Configure account-level real-time HTTP callbacks for delivery receipts and inbound messages across SMS and WhatsApp." --- # Webhooks Overview Bulkit uses webhooks to notify your server in real time when events occur on your account. Rather than polling the API continuously, Bulkit dispatches an HTTP POST request to your public webhook endpoint whenever: - an outbound SMS or WhatsApp message changes status (e.g. delivered, failed, read) - an inbound SMS or WhatsApp message arrives from a customer ## Shared Account-Level Webhooks Webhooks in Bulkit are **shared at the account level** across all your API keys and campaigns. You configure two endpoints in the Bulkit dashboard under **API Keys -> Webhooks**: 1. **DLR Webhook URL (Delivery Reports)**: Receives status updates (`sms.delivered`, `sms.failed`, `sms.blacklisted`, `whatsapp.receipt.updated`). 2. **Inbox Webhook URL (Inbound Messages)**: Receives customer replies (`sms.received`, `whatsapp.message.received`). Each endpoint has an **Enable** switch in the dashboard so you can activate or pause notifications at any time. ## Webhook Headers Bulkit sends the following standard HTTP headers with every webhook POST request: | Header | Description | Example | | --- | --- | --- | | `Content-Type` | MIME format of the request body | `application/json` | | `X-Bulkit-Webhook-Event` | The specific event identifier | `sms.delivered`, `whatsapp.message.received` | ## Receiving Webhook Requests When Bulkit triggers a webhook, your endpoint should: 1. **Return HTTP 200 OK promptly**: Respond with an HTTP `200` status code as soon as the payload is received. 2. **Process asynchronously**: Hand off heavy processing, database updates, or external API calls to background job workers (e.g. Celery, Redis queue, BullMQ, Go channels) so your HTTP response is not delayed. 3. **Handle retries idempotently**: Store the unique `outbox_id` (SMS) or `message_id` (WhatsApp) so duplicate deliveries can be detected and handled safely. ```bash // Expected response from your server: HTTP/1.1 200 OK Content-Type: application/json {"status": "ok"} ``` ## Supported Events ### Delivery Reports (DLR) - `sms.delivered`: SMS successfully delivered to the recipient's mobile terminal. - `sms.failed`: SMS delivery failed at the operator network. - `sms.blacklisted`: Outgoing SMS was rejected because the recipient is on the sender blacklist. - `whatsapp.receipt.updated`: WhatsApp delivery status changed (`delivered` or `read`). Detailed documentation and JSON payloads are available in [Delivery Report (DLR) Webhooks](/webhooks/delivery-reports). ### Inbound Messages - `sms.received` / `sms.inbound`: Incoming SMS sent to your shortcode or two-way number. - `whatsapp.message.received`: Inbound WhatsApp customer message (text, image, document, audio, video). Detailed documentation and JSON payloads are available in [Inbound Message Webhooks](/webhooks/inbound-messages). --- # Delivery Report (DLR) Webhooks URL: https://bulkitsms.com/docs/webhooks/delivery-reports --- title: "Delivery Report (DLR) Webhooks" description: "Handle real-time delivery status callbacks for SMS and WhatsApp messages." --- # Delivery Report (DLR) Webhooks When you set your **DLR Webhook URL** in the Bulkit dashboard (**API Keys -> Webhooks**), Bulkit dispatches real-time delivery status updates for all outbound messages. ## Events Dispatched | Event Identifier | Channel | When Dispatched | | --- | --- | --- | | `sms.delivered` | SMS | SMS has been delivered to the recipient handset. | | `sms.failed` | SMS | SMS delivery failed or expired at the network. | | `sms.blacklisted` | SMS | Recipient has blacklisted the sender ID. | | `whatsapp.receipt.updated` | WhatsApp | WhatsApp message was delivered or read by the recipient. | --- ## 1. SMS Delivered (`sms.delivered`) ```json { "event": "sms.delivered", "message_id": "451980165", "internal_message_id": 144, "outbox_id": "8fa84362-e1a1-4355-89f4-cfbbd2b38031", "mobile": "254700000001", "status": "delivered", "description": "DeliveredToTerminal", "response_code": "200", "response_description": "Success", "delivered_at": "2026-09-19T06:30:15Z", "blacklisted": false, "blacklist_reason": "", "sender_id": 12, "network_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "tat": "0.85 sec" } ``` ### Payload Fields | Field | Type | Description | | --- | --- | --- | | `event` | string | Constant: `sms.delivered`. | | `message_id` | string | External provider message reference ID. | | `internal_message_id` | integer | Numeric sequence ID if configured. | | `outbox_id` | string | Unique Bulkit outbox message UUID. | | `mobile` | string | Recipient phone number (e.g. `254700000001`). | | `status` | string | Delivery status: `delivered`. | | `description` | string | Carrier delivery description (e.g. `DeliveredToTerminal`). | | `response_code` | string | Provider response code (`200` = success). | | `delivered_at` | string | Timestamp when delivery occurred. | | `blacklisted` | boolean | Boolean indicating blacklist status (`false`). | | `sender_id` | integer/string | Sender ID used for the message. | | `tat` | string | Turnaround delivery time (e.g. `0.85 sec`). | --- ## 2. SMS Failed (`sms.failed`) ```json { "event": "sms.failed", "message_id": "451980166", "internal_message_id": 145, "outbox_id": "18f5d054-9457-410e-a89e-2dc7665fa1a0", "mobile": "254700000002", "status": "failed", "description": "AbsentSubscriber", "response_code": "500", "response_description": "Subscriber not reachable", "delivered_at": null, "blacklisted": false, "sender_id": 12 } ``` --- ## 3. SMS Blacklisted (`sms.blacklisted`) When a recipient has opted out or blacklisted the sender ID: ```json { "event": "sms.blacklisted", "message_id": "452719776", "outbox_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "mobile": "254700000003", "status": "blacklisted", "description": "SenderName Blacklisted", "response_code": "200", "response_description": "SenderName Blacklisted", "delivered_at": "2026-09-19T06:31:00Z", "blacklisted": true, "blacklist_reason": "SenderName Blacklisted", "sender_id": 12 } ``` --- ## 4. WhatsApp Receipt Updated (`whatsapp.receipt.updated`) When a WhatsApp message is successfully delivered to the recipient's phone or opened and read: ```json { "event": "whatsapp.receipt.updated", "message_id": "3EB0C34B8F56D254", "status": "read", "chat_jid": "254700000001@s.whatsapp.net", "timestamp": "2026-09-19T06:32:05Z" } ``` ### WhatsApp Statuses - `delivered`: The message was delivered to the contact's device (double checkmarks). - `read`: The contact opened and viewed the message (blue checkmarks). --- # Inbound Message Webhooks URL: https://bulkitsms.com/docs/webhooks/inbound-messages --- title: "Inbound Message Webhooks" description: "Process incoming customer replies across SMS and WhatsApp in real time." --- # Inbound Message Webhooks When customers reply to your SMS shortcode, two-way numbers, or your connected WhatsApp account, Bulkit immediately forwards the message to your configured **Inbox Webhook URL** (**API Keys -> Webhooks** in the dashboard). ## Events Dispatched | Event Identifier | Channel | Description | | --- | --- | --- | | `sms.received` / `sms.inbound` | SMS | Inbound SMS message sent to your shortcode or alphanumeric number. | | `whatsapp.message.received` | WhatsApp | Incoming customer WhatsApp message (text, media, or document). | --- ## 1. Incoming SMS Payload When a subscriber texts your shortcode or inbound SMS route: ```json { "event": "sms.received", "mobile": "254700000001", "shortcode": "BULKIT", "message": "YES please send me the quotation", "received_at": "2026-09-19T06:40:00+03:00" } ``` ### Payload Fields | Field | Type | Description | | --- | --- | --- | | `event` | string | `sms.received` or `sms.inbound`. | | `mobile` | string | Sender mobile number in international format (`2547...`). | | `shortcode` | string | Destination shortcode or sender name that received the SMS. | | `message` | string | The full SMS text sent by the subscriber. | | `received_at` | string | Timestamp when the message reached Bulkit. | --- ## 2. Incoming WhatsApp Message Payload When a customer messages your connected WhatsApp account: ```json { "event": "whatsapp.message.received", "id": "c1f7a072-4a0b-4835-bd57-f6498ec5bbdc", "message_id": "3EB042F803DA86B4", "from": "254700000001", "to": "254712345678@s.whatsapp.net", "sender_jid": "254700000001@s.whatsapp.net", "chat_jid": "254700000001@s.whatsapp.net", "text": "Hello, could you please send me an updated invoice for this month?", "media_type": "text", "media_url": "", "file_name": "", "file_size": 0, "mime_type": "", "timestamp": "2026-09-19T06:41:22Z" } ``` ### Inbound WhatsApp Media Attachment Example If the customer sends a photo, PDF, or audio voice note: ```json { "event": "whatsapp.message.received", "id": "e88383a8-4bb9-4d69-a1b7-a36c841fa909", "message_id": "3EB09395AB6E6FF1", "from": "254700000001", "to": "254712345678@s.whatsapp.net", "sender_jid": "254700000001@s.whatsapp.net", "chat_jid": "254700000001@s.whatsapp.net", "text": "Here is the proof of payment screenshot.", "media_type": "image", "media_url": "/api/v2/messages/whatsapp/e88383a8-4bb9-4d69-a1b7-a36c841fa909/media", "file_name": "receipt_screenshot.jpg", "file_size": 184502, "mime_type": "image/jpeg", "timestamp": "2026-09-19T06:42:01Z" } ``` ### Media Types - `text`: Regular text chat. - `image`: Photo or graphic file (JPEG, PNG). - `document`: PDF, Office document, CSV, etc. - `audio`: Recorded voice note or music file. - `video`: Video clip (MP4). ### Downloading Received Media The `media_url` field contains the path to download the decrypted file. You can fetch it using your API key: ```bash curl -X GET "https://api.bulkitsms.com/api/v2/messages/whatsapp/e88383a8-4bb9-4d69-a1b7-a36c841fa909/media" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ --output proof_of_payment.jpg ``` --- # Contacts API Overview URL: https://bulkitsms.com/docs/contacts/overview --- title: "Contacts API Overview" description: "Manage contact groups and contacts programmatically with the Bulkit Contacts API." --- # Contacts API Overview Bulkit Contacts API lets you retrieve contact groups, create contacts, create contacts in bulk, and delete contacts from your Bulkit account. ## Available endpoints - `GET /api/v2/contacts/groups` - `POST /api/v2/contacts` - `POST /api/v2/contacts/bulk` - `DELETE /api/v2/contacts/:id` ## Authentication Contacts API uses standard Bearer authentication across all endpoints: - **Bearer Token (Recommended)**: `Authorization: Bearer ` - Legacy headers: `X-API-Key` and `X-API-Secret` - URL query parameters (`?apikey=...`) for `GET` and `DELETE` requests where needed ## Current group behavior Contacts currently support a single group assignment in the backend. The API accepts `group_ids` for forward compatibility, but only one group is supported at a time. If you send more than one group ID, Bulkit returns: ```json { "status": "error", "message": "Only one group is supported for now" } ``` ## Custom metadata The API accepts `custom_metadata` in the request shape for integration compatibility. At the moment, Bulkit accepts it but does not persist it in the contact record. --- # Fetch Groups URL: https://bulkitsms.com/docs/contacts/fetch-groups --- title: "Fetch Groups" description: "Retrieve the contact groups available on your Bulkit account." --- # Fetch Groups Use this endpoint to list the contact groups that belong to your account, including contact counts for each group. ## Short description Retrieve the contact groups available to the authenticated Bulkit account. ## HTTP method and path ```http GET /api/v2/contacts/groups ``` ## Authorization requirements This endpoint supports: - **Bearer token (recommended):** `Authorization: Bearer ` - Legacy headers: `X-API-Key` and `X-API-Secret` - URL query parameter: `?apikey=` ## Authorization example ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` ## Parameters This endpoint does not require a request body. ### Authentication parameters | Parameter | Location | Required | Type | Description | | --- | --- | --- | --- | --- | | `Authorization` | Header | Yes (recommended) | string | `Bearer ` | | `X-API-Key` | Header | Optional (legacy) | string | Your Bulkit key | | `X-API-Secret` | Header | Optional (legacy) | string | Your Bulkit secret | | `apikey` | Query | Optional fallback | string | API key used in URL authorization | ## Request examples ```bash curl curl -X GET "https://api.bulkitsms.com/api/v2/contacts/groups" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` ```python python.py import requests response = requests.get( "https://api.bulkitsms.com/api/v2/contacts/groups", headers={ "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", }, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```go main.go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, err := http.NewRequest(http.MethodGet, "https://api.bulkitsms.com/api/v2/contacts/groups", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Printf("%+v\n", result) } ``` ```php php true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", ], ]); $response = curl_exec($ch); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); echo $response . PHP_EOL; ``` ## Success response example ```json { "status": "success", "data": [ { "id": 14, "name": "Customers", "contact_count": 248 } ] } ``` ## Error response examples ```json { "status": "error", "message": "Invalid credentials" } ``` ## Notes - call this endpoint first if your integration needs to validate or look up a destination group ID - `contact_count` can be used to power sync dashboards and reconciliation jobs --- # Create Contact URL: https://bulkitsms.com/docs/contacts/create-contact --- title: "Create Contact" description: "Create a single contact in your Bulkit account and optionally assign it to one group." --- # Create Contact Use this endpoint to create a single contact from your application. ## HTTP method and path ```http POST /api/v2/contacts ``` ## Authorization requirements Authenticate using standard HTTP Bearer token: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md Content-Type: application/json ``` For backward compatibility, Bulkit also accepts `apikey` and `apisecret` in the JSON request body or `X-API-Key` / `X-API-Secret` headers. ## Request example ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/contacts" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "name": "John Doe", "phone_number": "0700000000", "email": "john@doe.com", "group_ids": [14] }' ``` ```python python.py import requests headers = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", } payload = { "name": "John Doe", "phone_number": "0700000000", "email": "john@doe.com", "group_ids": [14], } response = requests.post( "https://api.bulkitsms.com/api/v2/contacts", json=payload, headers=headers, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```go main.go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { payload := map[string]interface{}{ "name": "John Doe", "phone_number": "0700000000", "email": "john@doe.com", "group_ids": []int{14}, } body, err := json.Marshal(payload) if err != nil { panic(err) } req, _ := http.NewRequest(http.MethodPost, "https://api.bulkitsms.com/api/v2/contacts", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Printf("%+v\n", result) } ``` ```php php "John Doe", "phone_number" => "0700000000", "email" => "john@doe.com", "group_ids" => [14] ]; $ch = curl_init("https://api.bulkitsms.com/api/v2/contacts"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR), ]); $response = curl_exec($ch); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); echo $response . PHP_EOL; ``` ## Parameters | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `name` | Yes | string | Contact full name | | `phone_number` | Yes | string | Contact mobile number | | `email` | No | string | Contact email address | | `group_ids` | No | array of integers/UUIDs | Group IDs for assignment. Only one group is supported for now | | `custom_metadata` | No | object | Integration metadata payload accepted for compatibility | ## Success response example ```json { "status": "success", "message": "Contact created successfully", "data": { "id": 442, "first_name": "John", "last_name": "Doe", "mobile": "254700000000", "email": "john@doe.com" } } ``` ## Error response examples ```json { "status": "error", "message": "Invalid phone number" } ``` ```json { "status": "error", "message": "Contact already exists" } ``` ```json { "status": "error", "message": "Group not found" } ``` --- # Create Multiple Contacts URL: https://bulkitsms.com/docs/contacts/create-multiple-contacts --- title: "Create Multiple Contacts" description: "Create many contacts in a single request with partial-success response handling." --- # Create Multiple Contacts Use this endpoint to create multiple contacts in one request. ## HTTP method and path ```http POST /api/v2/contacts/bulk ``` ## Authorization requirements Authenticate using standard HTTP Bearer token: ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md Content-Type: application/json ``` For backward compatibility, Bulkit also accepts `apikey` and `apisecret` in the JSON request body or `X-API-Key` / `X-API-Secret` headers. ## Parameters | Parameter | Required | Type | Description | | --- | --- | --- | --- | | `contacts` | Yes | array of objects | List of contacts to create | | `contacts[].name` | Yes | string | Contact full name | | `contacts[].phone_number` | Yes | string | Contact mobile number | | `contacts[].email` | No | string | Contact email address | | `contacts[].group_ids` | No | array of integers/UUIDs | Only one group is supported for now | | `contacts[].custom_metadata` | No | object | Accepted for compatibility | ## Request example ```bash curl curl -X POST "https://api.bulkitsms.com/api/v2/contacts/bulk" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "name": "Jane Doe", "phone_number": "0710000000", "email": "jane@doe.com", "group_ids": [14] }, { "name": "Mark Otieno", "phone_number": "0722000000" } ] }' ``` ```python python.py import requests headers = { "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type": "application/json", } payload = { "contacts": [ {"name": "Jane Doe", "phone_number": "0710000000", "email": "jane@doe.com", "group_ids": [14]}, {"name": "Mark Otieno", "phone_number": "0722000000"} ] } response = requests.post( "https://api.bulkitsms.com/api/v2/contacts/bulk", json=payload, headers=headers, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```go main.go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { payload := map[string]interface{}{ "contacts": []map[string]interface{}{ {"name": "Jane Doe", "phone_number": "0710000000", "group_ids": []int{14}}, {"name": "Mark Otieno", "phone_number": "0722000000"}, }, } body, err := json.Marshal(payload) if err != nil { panic(err) } req, _ := http.NewRequest(http.MethodPost, "https://api.bulkitsms.com/api/v2/contacts/bulk", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Printf("%+v\n", result) } ``` ```php php [ ["name" => "Jane Doe", "phone_number" => "0710000000", "group_ids" => [14]], ["name" => "Mark Otieno", "phone_number" => "0722000000"] ] ]; $ch = curl_init("https://api.bulkitsms.com/api/v2/contacts/bulk"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR), ]); $response = curl_exec($ch); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); echo $response . PHP_EOL; ``` ## Success response example ```json { "status": "success", "message": "Contacts processed", "data": { "created_count": 2, "failed_count": 1, "contacts": [ { "id": 501, "first_name": "Jane", "last_name": "Doe", "mobile": "254710000000" } ], "failures": [ { "index": 2, "phone_number": "12345", "message": "Invalid phone number" } ], "warnings": [] } } ``` ## Error response examples ```json { "status": "error", "message": "Invalid credentials" } ``` ```json { "status": "error", "message": "Invalid input" } ``` ## Notes - bulk create supports partial success, so valid contacts can still be created when some rows fail - use the `failures` array to identify records that should be corrected and retried --- # Delete Contact URL: https://bulkitsms.com/docs/contacts/delete-contact --- title: "Delete Contact" description: "Delete an existing contact from your Bulkit account." --- # Delete Contact Use this endpoint to remove a contact from your Bulkit account by contact ID. ## HTTP method and path ```http DELETE /api/v2/contacts/:id ``` ## Authorization requirements This endpoint supports: - **Bearer token (recommended):** `Authorization: Bearer ` - Legacy headers: `X-API-Key` and `X-API-Secret` - URL query parameter: `?apikey=` ## Authorization example ```http Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md ``` ## Parameters | Parameter | Location | Required | Type | Description | | --- | --- | --- | --- | --- | | `id` | Path | Yes | string / integer | Contact ID or UUID to delete | | `Authorization` | Header | Yes (recommended) | string | `Bearer ` | | `X-API-Key` | Header | Optional (legacy) | string | Your Bulkit key | | `X-API-Secret` | Header | Optional (legacy) | string | Your Bulkit secret | | `apikey` | Query | Optional fallback | string | API key used in URL authorization | ## Request examples ```bash curl curl -X DELETE "https://api.bulkitsms.com/api/v2/contacts/442" \ -H "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md" ``` ```python python.py import requests response = requests.delete( "https://api.bulkitsms.com/api/v2/contacts/442", headers={ "Authorization": "Bearer bk_live_8n6JQv3K1h9Lp0Md", }, timeout=30, ) response.raise_for_status() print(response.json()) ``` ```go main.go package main import ( "fmt" "io" "net/http" ) func main() { req, err := http.NewRequest(http.MethodDelete, "https://api.bulkitsms.com/api/v2/contacts/442", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer bk_live_8n6JQv3K1h9Lp0Md") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(string(body)) } ``` ```php php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer bk_live_8n6JQv3K1h9Lp0Md", ], ]); $response = curl_exec($ch); if ($response === false) { throw new Exception(curl_error($ch)); } curl_close($ch); echo $response . PHP_EOL; ``` ## Success response example ```json { "status": "success", "message": "Contact deleted successfully" } ``` ## Error response examples ```json { "status": "error", "message": "Invalid credentials" } ``` ```json { "status": "error", "message": "Contact not found" } ``` ## Notes - delete operations are scoped to the authenticated account - deleting a contact removes it from future contact-based send workflows --- # Guides URL: https://bulkitsms.com/docs/guides/overview --- title: "Guides" description: "Step-by-step implementation guides for integrating with Bulkit." --- # Guides Use the guides section when you want practical implementation help instead of raw endpoint reference. These pages are designed for: - first-time integrations - secure API key setup - choosing the right authentication method - following the recommended integration sequence ## Recommended path 1. [Quickstart](/quickstart) 2. [Generating API Keys](/generating-api-keys) 3. [Authentication Overview](/authentication/overview) 4. [Header Authorization](/authentication/header-authorization) 5. [URL Authorization](/authentication/url-authorization) ## When to use guides vs API reference Use **Guides** when you want implementation direction. Use **API Reference** when you need: - exact endpoint paths - parameter requirements - success and error payloads - code examples by language --- # API Reference URL: https://bulkitsms.com/docs/api-reference/overview --- title: "API Reference" description: "Reference documentation for Bulkit endpoints, request schemas, responses, and error behavior." --- # API Reference The API reference documents Bulkit’s production endpoints in an implementation-focused format. Use this section when you need: - endpoint URLs and methods - request payloads - response examples - validation rules - common error messages ## Endpoint groups ### Account - [Get Account Balance](/account/get-account-balance) ### SMS - [How to Send SMS](/sms/overview) - [Send SMS Using Header Authorization](/sms/send-sms-using-header-authorization) - [Send SMS Using URL Params](/sms/send-sms-using-url-params) - [Send a Single SMS](/sms/send-a-single-sms) - [Send Customized SMS](/sms/send-customized-sms) - [How to Setup SMS Webhook](/sms/how-to-setup-sms-webhook) - [Shortcode Inbox SMS Webhook](/sms/shortcode-inbox-sms-webhook) - [Keyword Inbox SMS Webhook](/sms/keyword-inbox-sms-webhook) - [How to Get Delivery Reports](/sms/how-to-get-delivery-reports) - [SMS API Error Messages](/sms/error-messages) ### Contacts - [Contacts Overview](/contacts/overview) - [Fetch Groups](/contacts/fetch-groups) - [Create Contact](/contacts/create-contact) - [Create Multiple Contacts](/contacts/create-multiple-contacts) - [Delete Contact](/contacts/delete-contact) ### WhatsApp - [WhatsApp API Overview](/whatsapp/overview) - [Send WhatsApp Message](/whatsapp/send-message) - [Send Media & Attachments](/whatsapp/send-media) - [Send Bulk WhatsApp Broadcast](/whatsapp/send-bulk) - [Inbox & Message History](/whatsapp/inbox-and-messages) - [Connection Status](/whatsapp/connection-status) ### Webhooks - [Webhooks Overview](/webhooks/overview) - [Delivery Reports (DLR)](/webhooks/delivery-reports) - [Inbound Messages](/webhooks/inbound-messages) ## Conventions Bulkit API follows these conventions: - JSON request and response bodies for `POST` endpoints - API key authentication for all endpoints - stable validation messages for common errors - timezone-aware scheduling where scheduling is supported ## Common response format Successful responses typically look like this: ```json { "status": "success", "message": "Request processed successfully", "data": {} } ``` Error responses typically look like this: ```json { "status": "error", "message": "Invalid credentials" } ``` --- # Changelog URL: https://bulkitsms.com/docs/changelog --- title: "Changelog" description: "Product updates, API improvements, and developer-facing changes in Bulkit." --- # Product Updates Stay up to date with Bulkit product updates, API improvements, platform enhancements, and documentation changes. ## Smart Bundles Smart Bundles lower the entry price for small sends without discounting standard volume rates: - **Bundle Rates**: KSh 100 (125 SMS @ KSh 0.80), KSh 300 (429 @ KSh 0.70), KSh 500 (833 @ KSh 0.60), KSh 1,000 (2,000 @ KSh 0.50), and KSh 2,000 (5,000 @ KSh 0.40). - **Separate Credit Tracking**: Bundle credits are tracked apart from standard, non-expiring credits, so expiry logic never affects regular balances. - **Expiry Enforcement**: Unused bundle credits expire at the 24-hour deadline. - **Consumed First**: The send flow deducts bundle credits before standard credits. - **Distinct Purchase Flow**: Smart Bundles appear as their own option in the dashboard, separate from standard credit top-ups. See the [Smart Bundles guide](/docs/account/smart-bundles) and [bundle pricing](/pricing#smart-bundles) for details. ## Strict Sender ID Authorization on SMS API To prevent unauthorized sender usage and protect brand integrity, Bulkit now strictly enforces account-level Sender ID ownership across all SMS endpoints (`/api/v2/messages/sms`, `/api/v2/messages/sms/bulk`, and legacy endpoints): - **No Silent Defaulting**: Requests specifying an unauthorized, unassigned, inactive, or mistyped sender ID or sender name now return HTTP `400 Bad Request` (`Invalid sender ID`) rather than silently defaulting to the system shortcode (`BULK_IT`). - **Explicit System Sender**: To send using Bulkit's default system shortcode, callers must explicitly provide `"sender": "BULK_IT"`. - **Defense-in-Depth Verification**: Sender resolution and queueing layers verify client authorization before deducting wallet credits or enqueueing messages. ## Multi-Client Sender ID Assignment Platform administrators can now assign a single custom or dedicated Sender ID to multiple client accounts simultaneously: - **One-to-Many Assignment**: Map a shared Sender ID across multiple sub-accounts or organizations. - **Admin Multi-Client Dialog**: Search, multi-select, and assign/unassign clients directly from `/admin/sender-ids`. - **Seamless Resolution**: Assigned clients can immediately send using either the alphanumeric name or the assigned Sender UUID. ## WhatsApp API v2 Bulkit now offers native WhatsApp integration for automated notifications, customer support, and high-throughput broadcasts: - **Direct Text Messaging**: Send WhatsApp messages using `POST /api/v2/messages/whatsapp`. - **Media & Documents**: Stream media attachments up to 50MB via remote URL or direct multipart file upload (`images`, `videos`, `audio`, `PDF documents`). - **Bulk Broadcasts**: Dispatch personalized campaigns with curly brace placeholders (`{first_name}`) and custom variable substitution. - **Inbound Message Streaming**: Query chat history and receive inbound customer replies with decrypted media downloads. - **Device Session Management**: Real-time QR code linking and connection health monitoring (`/api/v2/whatsapp/status`). ## Account-Level Shared Webhooks Webhook configuration has been simplified: - **Dedicated Webhooks Tab**: Set global **DLR Webhook URL** and **Inbox Webhook URL** under `/api-keys` across all keys. - **No Webhook Secrets**: Removed complex signing secrets in favor of frictionless integration. - **Standardized Headers**: Webhook payloads are tagged with `X-Bulkit-Webhook-Event` (`sms.delivered`, `sms.failed`, `sms.blacklisted`, `whatsapp.receipt.updated`, `sms.received`, `whatsapp.message.received`). ## High-Precision Float Balances & API v2 Refinements - **Float Wallet Balances**: Account credit balances now return exact floating-point numbers (e.g. `2.97 KES`), supporting fractional micro-billing. - **Simplified API Key Creation**: Generating new keys only requires a descriptive Key Name. - **Dedicated Single SMS Endpoint**: OTPs and alerts can now be posted directly to `POST /api/v2/messages/sms` with a single `mobile` string. - **UUID & Flex Shortcode Support**: `sender_id` seamlessly accepts both modern UUID strings and legacy shortcode integers. ## Contacts API Bulkit now supports contact management via API. New endpoints include: - fetching contact groups - creating a single contact - creating contacts in bulk - deleting contacts ## SMS API validation improvements SMS requests now return clearer, standardized errors for common send failures: - `Invalid credentials` - `Insufficient balance` - `Invalid number` - `Invalid sender ID` - `Invalid message` - `Restricted send time` ## Documentation improvements The documentation site now includes: - top-level sections for Documentation, Guides, API Reference, and Changelog - a contextual AI menu with one-click copy support - expanded Contacts API coverage ## Scheduled SMS reliability improvements Scheduled SMS processing was improved to ensure due messages are picked up correctly and handed off to the active queue provider. ## QuickSMS request and response logging Bulkit now stores QuickSMS API request and response logs for better operational visibility and support workflows. ## API key management upgrades Managed API keys now support: - multiple keys per account - enable and disable controls - webhook configuration fields - last-used metadata ## Outbox filtering improvements The SMS outbox gained improved filtering for: - network - sender ID - single date - date range - sort order ## Notifications improvements Dashboard notifications now better reflect SMS sent and failed events, with clearer unread counts and better status visibility. ## Message rendering consistency Personalized SMS placeholders are now rendered earlier in the send flow so outbox and history views reflect final message content more reliably.