Overview

WhichSMS API Reference

The WhichSMS REST API gives developers, businesses, and SaaS platforms direct programmatic access to bulk SMS, OTP verification, and delivery reporting — all without ever touching our upstream provider. Our API is designed for production reliability, security, and ease of integration.

HMAC-Signed

Every v2 request is signed with HMAC-SHA256. Replay attacks are blocked with nonces.

Sub-second Latency

Atomic credit deductions and async BMS relay. No blocking round trips.

E.164 Ready

Send to any phone number worldwide in E.164 format.

Multi-language

Node.js, Python, PHP, cURL — examples for every stack.

Base URL
TEXT
https://whichsms.com

Quick Start

Send Your First SMS in 5 Minutes

Follow these steps to send your first SMS using the v1 API (no signing required — great for testing).

  1. 1
    Create an API Key

    Go to your Developer Dashboard → API Keys → Create Key. Copy your Secret Key (shown only once).

  2. 2
    Register a Sender ID

    Go to Sender IDs and submit a registration. Once approved, you can use it in API calls.

  3. 3
    Send an SMS

    Use the v1 API with your secret key as a Bearer token:

    cURL
    curl -X POST https://whichsms.com/api/v1/sms/send \
      -H "Authorization: Bearer wh_sec_your_secret_key" \
      -H "Content-Type: application/json" \
      -d '{
        "to": ["+233241234567"],
        "message": "Hello from WhichSMS!",
        "sender_id": "MyBrand"
      }'
  4. 4
    Check the Response
    Success Response
    {
      "success": true,
      "message": "Messages sent successfully.",
      "data": {
        "campaign_id": "cm_8f9c2...",
        "cost": 1,
        "recipients_queued": 1,
        "recipients_skipped": 0
      }
    }

Security

Authentication

WhichSMS provides two authentication methods. Choose based on your use case.

v1 — Bearer Token

Simpler

Pass your secret key as a Bearer token. Best for server-side integration or quick testing.

BASH
Authorization: Bearer wh_sec_your_secret_key

⚠️ Only use over HTTPS. Never in browser JavaScript.

v2 — HMAC-SHA256

Recommended

Every request is signed with HMAC-SHA256. Provides tamper-proof requests and replay attack prevention.

TEXT
X-API-Key: wh_pub_your_public_key
X-Signature: <hmac_sha256_hex>
X-Timestamp: <unix_timestamp>
X-Nonce: <unique_random_string>

How to Sign a v2 Request

The signature is computed as:

Signature Payload Format
HMAC-SHA256(secret_key, "<timestamp>\n<nonce>\n<METHOD>\n<path>\n<body_sha256_hex>")
Node.js
const crypto = require('crypto');
const fetch = require('node-fetch'); // or use built-in in Node 18+

const BASE_URL = 'https://whichsms.com';
const PUBLIC_KEY = 'wh_pub_your_public_key';
const SECRET_KEY = 'wh_sec_your_secret_key';

async function sendSms(to, message, senderId) {
  const body = JSON.stringify({ to, message, sender_id: senderId });
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomBytes(16).toString('hex');
  const path = '/api/v2/sms/send';

  const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
  const payload = `${timestamp}\n${nonce}\nPOST\n${path}\n${bodyHash}`;
  const signature = crypto.createHmac('sha256', SECRET_KEY).update(payload).digest('hex');

  const res = await fetch(`${BASE_URL}${path}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': PUBLIC_KEY,
      'X-Signature': signature,
      'X-Timestamp': timestamp,
      'X-Nonce': nonce,
    },
    body,
  });

  return res.json();
}

sendSms(['+233241234567'], 'Hello from WhichSMS!', 'MyBrand')
  .then(console.log);
Timestamp Drift — Requests with a timestamp more than ±5 minutes from server time are rejected. Always sync your system clock with an NTP server.

Rate Limits

Rate Limits

Rate limits protect the platform from abuse and ensure fair usage across all customers.

EndpointLimitWindowError Code
POST /sms/send (v1 & v2)60 requestsper minute, per keySMS_RATE_LIMIT
POST /otp/send10 requestsper minute, per keyOTP_API_RATE_LIMIT
All other API endpoints500 requestsper 15 minutes, per IPRATE_LIMIT
Auth endpoints (login, register)10 requestsper 15 minutes, per IPAUTH_RATE_LIMIT

Rate limit headers are included in every response: RateLimit-Remaining, RateLimit-Reset.

You can also set a daily request limit per API key in your Developer Dashboard.


SMS

Send SMS

Send an SMS message to one or multiple recipients immediately.

POST/api/v2/sms/send

Required Permissions

sms:send

Request Body

FieldTypeRequiredDescription
tostring | string[]Recipient phone number(s). Comma-separated string or array. E.164 format recommended.
messagestringSMS message content. Max 1600 characters.
sender_idstringYour approved Sender ID. Must be registered and approved on your account.
Request Body
{
  "to": ["+233241234567", "+233201234567"],
  "message": "Your order has been shipped. Track it at track.myshop.com",
  "sender_id": "MyShop"
}
Success Response (200)
{
  "success": true,
  "message": "Messages sent successfully.",
  "data": {
    "campaign_id": "a3f7b2...",
    "cost": 2,
    "recipients_queued": 2,
    "recipients_skipped": 0,
    "bms_campaign_id": "bms_uuid..."
  }
}
Interactive Console
POST /api/v2/sms/send

SMS

Schedule SMS

Schedule an SMS to be sent at a specific date and time in the future.

POST/api/v2/sms/schedule

Additional Fields

FieldTypeRequiredDescription
schedule_datestringFuture date/time in format: YYYY-MM-DD HH:mm (UTC)
Request Body
{
  "to": ["+233241234567"],
  "message": "Flash sale starts NOW! Use code SAVE20.",
  "sender_id": "MyShop",
  "schedule_date": "2025-12-25 09:00"
}
Response
{
  "success": true,
  "message": "SMS scheduled successfully.",
  "data": {
    "campaign_id": "cm_x9b...",
    "cost": 1,
    "recipients_queued": 1,
    "recipients_skipped": 0,
    "scheduled": true
  }
}

SMS

Sender IDs

A Sender ID is the name or number that recipients see as the message source (e.g. "MyBrand"). You must register and receive approval for each Sender ID before using it.

Approval Required — All Sender IDs require admin approval before they can send messages. Register at dashboard/sender-ids. Approval typically takes 24–48 hours.
GET/api/v2/sender-ids

List all approved Sender IDs on your account.

Response
{
  "success": true,
  "data": [
    {
      "id": "sid_abc123",
      "name": "MyBrand",
      "status": "APPROVED",
      "created_at": "2025-01-10T12:00:00Z"
    }
  ]
}

OTP

Send OTP

WhichSMS manages the full OTP lifecycle — generation, storage (hashed), delivery, and verification. You don't need to store anything yourself.

POST/api/v2/otp/send

Request Body

FieldTypeRequiredDefaultDescription
phonestringRecipient phone number in E.164 format
sender_idstringYour approved Sender ID
lengthnumber6OTP digit count (4–8)
ttlnumber300Expiry in seconds (60–1800)
formatstringNUMERICNUMERIC or ALPHANUMERIC
Request Body
{
  "phone": "+233241234567",
  "sender_id": "MyApp",
  "length": 6,
  "ttl": 300
}
Success Response (200)
{
  "success": true,
  "message": "OTP sent successfully.",
  "data": {
    "reference": "otp_8f9c2b4a...",
    "phone": "+233241234567",
    "expires_at": "2025-07-05T13:55:00Z"
  }
}
Interactive Console
POST /api/v2/otp/send

OTP

Verify OTP

Submit the OTP code entered by the user. The system verifies it against the stored hash and returns a verified status.

POST/api/v2/otp/verify
FieldTypeRequiredDescription
referencestringThe reference returned from /otp/send
codestringThe OTP code entered by the user
Request Body
{
  "reference": "otp_8f9c2b4a...",
  "code": "482910"
}
Success Response (200)
{
  "success": true,
  "message": "OTP verified successfully.",
  "data": {
    "reference": "otp_8f9c2b4a...",
    "phone": "+233241234567",
    "verified_at": "2025-07-05T13:54:22Z"
  }
}
Error — Invalid Code (400)
{
  "success": false,
  "message": "Invalid OTP code.",
  "error_code": "INVALID_CODE",
  "data": {
    "attempts": 2,
    "max_attempts": 5,
    "locked": false
  }
}
Lockout Protection — After 5 failed attempts, the OTP reference is permanently locked. The user must request a new OTP. Expired OTPs also cannot be verified.

Contacts

Contacts & Groups

Store contacts in WhichSMS to use with Group SMS. Requires the contacts:write permission on your API key.

POST/api/v2/contacts
Create Contact
{
  "phone": "+233241234567",
  "firstname": "Kwame",
  "lastname": "Mensah",
  "email": "kwame@example.com",
  "group_id": "grp_abc123"
}
GET/api/v2/contacts

List contacts. Filter by group: ?group_id=grp_abc123. Paginate with ?page=1&limit=50.

POST/api/v2/contacts/groups
Create Group
{ "name": "Loyal Customers" }
GET/api/v2/contacts/groups

Returns all groups with a contact count.


Reports

Delivery Reports

Retrieve campaign and message-level delivery data. Requires reports:read.

GET/api/v2/reports/sms

List all campaigns. Filter by status: ?status=COMPLETED.

Response
{
  "success": true,
  "data": [
    {
      "id": "cm_abc123",
      "title": "July Promo",
      "sender_id": "MyShop",
      "type": "QUICK",
      "status": "COMPLETED",
      "total_recipients": 1500,
      "total_cost": 1500,
      "created_at": "2025-07-05T09:00:00Z"
    }
  ],
  "meta": { "total": 24, "page": 1, "limit": 20, "totalPages": 2 }
}
GET/api/v2/reports/campaign/:id

Get detailed message-level delivery data for a specific campaign including individual recipient statuses.


Webhooks

Webhooks

Register a public HTTPS URL to receive real-time event notifications. All webhook payloads are signed with X-WhichSMS-Signature for verification.

Available Events

EventTrigger
sms.sentSMS campaign dispatched via API
sms.deliveredDelivery confirmed by carrier
sms.failedMessage delivery failed
otp.sentOTP delivered to recipient
payment.successWallet top-up successful
*All events
POST/api/v2/webhooks
Register Webhook
{
  "url": "https://yourapp.com/webhooks/whichsms",
  "events": ["sms.delivered", "sms.failed"],
  "description": "Production SMS delivery handler"
}

Verifying Webhook Signatures

Node.js / Express
const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhooks/whichsms', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-whichsms-signature'];
  const webhookSecret = process.env.WHICHSMS_WEBHOOK_SECRET;

  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body.toString());
  console.log('Webhook received:', event.type, event.data);
  
  res.status(200).send('OK');
});

Reference

Error Code Reference

All errors return a consistent JSON shape with an error_code field for programmatic handling.

Error Response Shape
{
  "success": false,
  "message": "Human-readable explanation",
  "error_code": "MACHINE_READABLE_CODE"
}
Error CodeHTTPCauseSolution
MISSING_API_KEY401No API key in request headersAdd Authorization: Bearer key or X-API-Key
INVALID_API_KEY401Key doesn't exist or is revokedRegenerate your API key in the dashboard
INVALID_SIGNATURE401HMAC signature mismatchVerify your signing implementation exactly matches our format
TIMESTAMP_EXPIRED401Request timestamp too old/futureSync your server clock with NTP. Max drift: ±5 minutes
NONCE_REPLAY401Nonce was already usedGenerate a fresh random nonce per request
MISSING_AUTH_HEADERS401v2 auth headers missingInclude X-API-Key, X-Signature, X-Timestamp, X-Nonce
ACCOUNT_SUSPENDED403User account is suspendedContact support
SENDER_ID_NOT_APPROVED403Sender ID not found or not approvedRegister and get approval for a Sender ID first
IP_NOT_WHITELISTED403Request IP not in key's whitelistAdd your IP to the key's whitelist or disable whitelist
DAILY_LIMIT_EXCEEDED429API key daily request limit reachedWait until midnight UTC or increase limit in dashboard
SMS_RATE_LIMIT429SMS send rate limit exceededSlow down to 60 requests/minute
OTP_API_RATE_LIMIT429OTP rate limit exceededSlow down to 10 OTP sends/minute
INSUFFICIENT_CREDITS402Not enough SMS creditsTop up your wallet in the dashboard
MISSING_FIELDS400Required field missing from bodyInclude all required fields per endpoint
INVALID_RECIPIENTS400No valid phone numbers foundUse E.164 format: +233XXXXXXXXX
MESSAGE_TOO_LONG400Message exceeds 1600 charactersShorten message or split into parts
INVALID_CODE400Submitted OTP code is wrongUser must re-enter code
EXPIRED400OTP has expiredCall /otp/send again to get a new code
LOCKED403OTP locked after too many attemptsCall /otp/send again for a new reference
SMS_SEND_FAILED502Upstream provider errorCredits refunded. Retry the request.
SERVER_ERROR500Internal server errorRetry. Contact support if persistent.
API Documentation | WhichSMS Developer Platform | WhichSMS