Developers

API Documentation

Integrate SMS, OTP, payments, airtime and data bundles into your own software. Every request is authenticated with your API key and returns JSON.

Getting started

Base URL
https://api.bulkclix.com/api/v1

All paths in this reference are relative to the base URL. Use HTTPS only.

Authentication

Send your key in the x-api-key header on every request. Keys are created on the API keys page and can be revoked at any time.

x-api-key: YOUR_API_KEY

Response format

Every endpoint answers with JSON. Single-object endpoints wrap the result in a message and data envelope. List endpoints return a paginated envelope with data, links and meta, and accept page and page_size query parameters.

application/json
// single object
{ "message": "Balance retrieved successfully", "data": { "wallet_balance": 250.75, "sms_balance": 1200, "currency": "GHS" } }

// paginated list
{ "data": [ ... ], "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta": { "current_page": 1, "per_page": 15, "total": 42, "last_page": 3 } }

Errors

Errors use the same envelope with a non-2xx status and data set to null. Validation errors (422) additionally include an errors object listing the failing fields. Always branch on the HTTP status first, then read message for a human-readable reason.

application/json
{ "message": "Insufficient wallet balance", "data": null }

{ "message": "The amount field is required.", "errors": { "amount": ["The amount field is required."] } }

Your reference (idempotency)

Every request that moves money takes a reference you generate (transaction_id or client_reference, 12 to 36 characters). It must be unique per account: a repeated reference is rejected with 422, so a retried request can never charge you twice. Store it before you call us and use it later to check status or find the row in history.

Webhooks (callback_url)

Where an endpoint accepts a callback_url, we POST a JSON payload to it when the transaction reaches a final state (success or failed). The request carries an X-Signature header containing an HMAC-SHA256 of the raw JSON body. Respond with any 2xx status to acknowledge. Delivery is a single attempt, so treat the status endpoints as the source of truth if your endpoint was unreachable. Mobile money and bank transfers, collections and airtime all send webhooks; data bundles do not, so poll their status endpoint instead.

POST <your callback_url>
{
  "amount": "50.00",
  "charges": "0.50",              // collections and transfers
  "net_amount": 50,               // collections only: what was credited to you after the fee
  "fee_bearer": "customer",       // collections only: who absorbed the fee (account setting)
  "status": "success",            // or "failed"
  "transaction_id": "ORDER-2026-000123",     // the reference you sent
  "ext_transaction_id": "839201746512",
  "phone_number": "233241234567"  // collections and airtime only
}

Rate limits

Sender ID requests and momo-funded airtime purchases are limited to 60 requests per hour per account. Status checks are limited to 60 requests per minute. When you exceed a limit you receive 429 with a Retry-After header. Prefer webhooks over tight polling loops.

HTTP status codes

CodeMeaning
200Request succeeded. The body carries message and data.
201Returned by the airtime networks list. Treat exactly like 200.
400The request could not be fulfilled, most often an insufficient wallet balance or a failed provider call that was refunded.
401Missing or unknown x-api-key header.
402Insufficient SMS credit. data holds your balance and the cost of the request.
403Your account is blocked, not enabled for this product, or the caller IP is not whitelisted.
404The referenced record (transaction, group, contact, sender) was not found.
422Validation failed. The body has message plus an errors object keyed by field.
429Rate limit exceeded. Retry after the number of seconds in the Retry-After header.
500Unexpected error on our side. Retry with the same reference; duplicates are rejected.

API reference