Developer Documentation

SureConnect SMS API

Send single and bulk SMS messages to any Kenyan number. Built for developers — simple REST API, real-time delivery, millisecond-fast responses.

3 Endpoints
60 Req / min
20 Max bulk / batch
REST JSON over HTTPS
Overview

Everything you need to know before making your first request.

Base URL

All endpoints are relative to: https://api.sms.ultimatesolutions.co.ke/v1

Authentication

Pass your API key in every request:
X-API-Key: sc_your_key_here

Rate Limit

60 requests / minute per API key. Exceeding returns 429 with a retry_after field.

Phone number format Both 0712345678 (local) and 254712345678 (E.164) are accepted. The API normalises all numbers automatically.
Quick Start

Send your first SMS in under 2 minutes.

1

Get your API key

Log in to your SureConnect dashboard → and generate an API key from the Account page. Keys start with sc_.

2

Check your balance

Call GET /V1/sms/balance with your key to verify authentication and see your available SMS credits.

3

Send your first SMS

POST to /V1/sms/send with to and message. You'll get back a message_id and updated balance.

30-second cURL test

shell
# 1. Store credentials
export SC_KEY="sc_your_key_here"
export SC_BASE="https://api.sms.ultimatesolutions.co.ke/v1"

# 2. Check balance
curl -s "$SC_BASE/sms/balance" -H "X-API-Key: $SC_KEY" | jq .

# 3. Send a test SMS
curl -s -X POST "$SC_BASE/sms/send" \
  -H "X-API-Key: $SC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"0712345678","message":"Hello from SureConnect!"}' | jq .

Expected response

json — success
{
  "success":    true,
  "message_id": "SC-20240601-00001",
  "balance":    142,
  "message":    "SMS queued"
}
Authentication

All requests must include your API key in the X-API-Key header.

http header
X-API-Key: sc_your_key_here
Keep your key secret. Never expose it in front-end JavaScript, mobile app bundles, or public repositories. Rotate it immediately from the dashboard if compromised.

Error responses

json — 401 missing key
{ "success": false, "error": "API key required" }
json — 403 invalid key
{ "success": false, "error": "Invalid or revoked API key" }
Endpoint Reference

Three endpoints cover every SMS use case.

GET /V1/sms/balance Account credit balance

Returns current credit balance, quota, and usage for the authenticated account. No request body required.

200 OK 401 Unauthorized 403 Forbidden 429 Too Many Requests
request
curl -s "https://api.sms.ultimatesolutions.co.ke/v1/sms/balance" \
  -H "X-API-Key: $SC_KEY"
json response
{
  "success": true,
  "balance": 150,
  "quota":   500,
  "used":    350
}
POST /V1/sms/send Single SMS — synchronous

Send a single SMS immediately. Deducts one credit on success and returns a message_id for delivery tracking.

Request body

FieldTypeRequiredDescription
tostringrequiredRecipient phone. 0712345678 or 254712345678
messagestringrequiredSMS text body (max 918 chars / 6 segments)
scheduled_atstringoptionalISO 8601 datetime to schedule. Omit to send immediately.
200 Sent 400 Bad Request 402 Insufficient Credits 429 Rate Limited
request body
{
  "to":      "0712345678",
  "message": "Your OTP is 482910. Expires in 5 min."
}
json response
{
  "success":    true,
  "message_id": "SC-20240601-00001",
  "balance":    149,
  "message":    "SMS sent"
}
POST /V1/sms/bulk Bulk SMS — async job

Enqueue an SMS campaign to an explicit list of numbers or a saved contact group. Returns a job_id immediately — messages are dispatched in the background.

Request body

FieldTypeRequiredDescription
tostring[]one ofArray of phone numbers (max 10,000 per request)
group_idintegerone ofID of a saved contact group (alternative to to)
messagestringrequiredSMS text body
campaignstringoptionalCampaign label for analytics (max 100 chars)
scheduled_atstringoptionalISO 8601 datetime to schedule delivery
Provide either to (number array) or group_id — not both.
202 Accepted 400 Bad Request 402 Insufficient Credits 429 Rate Limited
request — number list
{
  "to": ["0712345678","0723456789","254733000111"],
  "message":  "Flash sale — 30% off today!",
  "campaign": "Flash Sale June"
}
json response
{
  "success":  true,
  "job_id":   1042,
  "queued":   3,
  "balance":  147,
  "message":  "Bulk SMS job queued"
}
request — contact group
{
  "group_id": 5,
  "message":  "Your monthly statement is ready.",
  "campaign": "June Statements"
}
Client Libraries

Production-ready client classes for every major language. Copy, drop in, ship.

Ideal for testing, CI pipelines, and shell scripts. Pipe to jq for pretty JSON output.

Setup

bash
export SC_KEY="sc_your_key_here"
export SC_BASE="https://api.sms.ultimatesolutions.co.ke/v1"

Check balance

bash
curl -s "$SC_BASE/sms/balance" -H "X-API-Key: $SC_KEY" | jq .

Send a single SMS

bash
curl -s -X POST "$SC_BASE/sms/send" \
  -H "X-API-Key: $SC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"0712345678","message":"Your OTP is 482910. Expires in 5 minutes."}' | jq .

Bulk send — number list

bash
curl -s -X POST "$SC_BASE/sms/bulk" \
  -H "X-API-Key: $SC_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to":       ["0712345678","0723456789","254733000111"],
    "message":  "Flash sale — 30% off today only!",
    "campaign": "Flash Sale June"
  }' | jq .

Bulk send — contact group

bash
curl -s -X POST "$SC_BASE/sms/bulk" \
  -H "X-API-Key: $SC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"group_id":5,"message":"Your monthly statement is ready.","campaign":"June Statements"}' | jq .
PHP 8.1+ · no Composer needed. Uses the built-in curl_* functions. Compatible with Laravel, Symfony, CodeIgniter, and plain PHP.

Environment (.env)

dotenv
SURECONNECT_API_KEY=sc_your_key_here

Client class

SureConnectClient.php
<?php
class SureConnectClient
{
    private string $base;
    private string $key;
    private ?\CurlHandle $ch = null;

    public function __construct(
        string $apiKey,
        string $baseUrl = 'https://api.sms.ultimatesolutions.co.ke/v1'
    ) {
        $this->key  = $apiKey;
        $this->base = rtrim($baseUrl, '/');
    }

    private function request(string $method, string $path, array $body = []): array
    {
        if ($this->ch === null) {
            $this->ch = curl_init();
            curl_setopt_array($this->ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_CONNECTTIMEOUT => 5,
                CURLOPT_TIMEOUT        => 30,
                CURLOPT_HTTPHEADER     => [
                    'X-API-Key: '    . $this->key,
                    'Content-Type: application/json',
                    'Accept: application/json',
                ],
            ]);
        }

        curl_setopt($this->ch, CURLOPT_URL, $this->base . $path);

        if ($method === 'POST') {
            curl_setopt($this->ch, CURLOPT_POST,       true);
            curl_setopt($this->ch, CURLOPT_POSTFIELDS,  json_encode($body));
        } else {
            curl_setopt($this->ch, CURLOPT_HTTPGET, true);
        }

        $raw  = curl_exec($this->ch);
        $code = (int) curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($this->ch);

        if ($err) throw new \RuntimeException("cURL error: $err");

        $data = json_decode($raw, true);
        if ($data === null) throw new \RuntimeException("Invalid JSON (HTTP $code)");

        $data['_httpCode'] = $code;
        return $data;
    }

    public function balance(): array
    {
        return $this->request('GET', '/sms/balance');
    }

    public function send(string $to, string $message): array
    {
        return $this->request('POST', '/sms/send', compact('to', 'message'));
    }

    public function bulk(array $to, string $message, string $campaign = ''): array
    {
        $p = compact('to', 'message');
        if ($campaign !== '') $p['campaign'] = $campaign;
        return $this->request('POST', '/sms/bulk', $p);
    }

    public function bulkToGroup(int $groupId, string $message, string $campaign = ''): array
    {
        $p = ['group_id' => $groupId, 'message' => $message];
        if ($campaign !== '') $p['campaign'] = $campaign;
        return $this->request('POST', '/sms/bulk', $p);
    }

    public function __destruct()
    {
        if ($this->ch !== null) curl_close($this->ch);
    }
}

// ── Usage ─────────────────────────────────────────────────────────────────────
$sms = new SureConnectClient($_ENV['SURECONNECT_API_KEY']);

// Check balance
$bal = $sms->balance();
echo "Balance: {$bal['balance']} credits\n";

// Single send
$res = $sms->send('0712345678', 'Your OTP is 482910. Expires in 5 minutes.');
if ($res['success']) {
    echo "Sent! ID: {$res['message_id']}, balance: {$res['balance']}\n";
} else {
    echo "Error ({$res['_httpCode']}): {$res['error']}\n";
    if ($res['_httpCode'] === 402) {
        echo "Top up needed. Balance: {$res['balance']}\n";
    }
}

// Bulk send to explicit numbers
$res = $sms->bulk(
    ['0712345678', '0723456789', '0733000111'],
    'Flash sale — 30% off today only!',
    'Flash Sale June'
);
echo "Queued {$res['queued']} messages (job #{$res['job_id']})\n";

// Bulk send to a saved contact group
$res = $sms->bulkToGroup(5, 'Your invoice is ready.', 'June Invoices');
Browser security: Never hardcode your API key in front-end JavaScript. Make calls from your backend and proxy results to the browser, or use a short-lived token pattern.

Client class · Browser / Node 18+ (native fetch)

sureconnect.js
class SureConnectClient {
  constructor(apiKey, baseUrl = 'https://api.sms.ultimatesolutions.co.ke/v1') {
    this.apiKey  = apiKey;
    this.baseUrl = baseUrl.replace(/\/$/, '');
  }

  async #request(method, path, body = null) {
    const res = await fetch(this.baseUrl + path, {
      method,
      headers: {
        'X-API-Key':    this.apiKey,
        'Content-Type': 'application/json',
        'Accept':       'application/json',
      },
      ...(body ? { body: JSON.stringify(body) } : {}),
    });

    const data = await res.json();
    data._httpCode = res.status;

    if (!res.ok && !data.success) {
      const err = new Error(data.error ?? `HTTP ${res.status}`);
      err.httpCode = res.status;
      err.response = data;
      throw err;
    }
    return data;
  }

  balance()                              { return this.#request('GET',  '/sms/balance'); }
  send(to, message)                      { return this.#request('POST', '/sms/send',  { to, message }); }
  bulk(to, message, campaign = '')       { return this.#request('POST', '/sms/bulk',  { to, message, ...(campaign ? { campaign } : {}) }); }
  bulkToGroup(groupId, message, campaign = '') {
    return this.#request('POST', '/sms/bulk', {
      group_id: groupId, message, ...(campaign ? { campaign } : {})
    });
  }
}

// ── Usage ──────────────────────────────────────────────────────────────────────
const sms = new SureConnectClient(process.env.SURECONNECT_API_KEY);

const { balance } = await sms.balance();
console.log(`Balance: ${balance} credits`);

try {
  const res = await sms.send('0712345678', 'Your OTP is 482910. Expires in 5 minutes.');
  console.log(`Sent! ID: ${res.message_id}, balance: ${res.balance}`);
} catch (err) {
  if (err.httpCode === 402) console.error(`Out of credits. Balance: ${err.response.balance}`);
  else if (err.httpCode === 429) console.error(`Rate limited. Retry in ${err.response.retry_after ?? 60}s`);
  else console.error(`Error (${err.httpCode}): ${err.message}`);
}

const result = await sms.bulk(
  ['0712345678', '0723456789'],
  'Flash sale — 30% off today!',
  'Flash Sale June'
);
console.log(`Job #${result.job_id} — ${result.queued} messages queued`);
Node 14+ · built-in https · zero npm packages. Uses Node's native https module — no axios, node-fetch, or got required.

Client module

sureconnect.js
'use strict';
const https = require('https');
const { URL } = require('url');

class SureConnectClient {
  constructor(apiKey, baseUrl = 'https://api.sms.ultimatesolutions.co.ke/v1') {
    this.apiKey  = apiKey;
    this.baseUrl = baseUrl.replace(/\/$/, '');
  }

  _request(method, path, body = null) {
    return new Promise((resolve, reject) => {
      const url     = new URL(this.baseUrl + path);
      const payload = body ? JSON.stringify(body) : null;

      const req = https.request({
        hostname: url.hostname,
        port:     url.port || 443,
        path:     url.pathname,
        method,
        timeout:  30_000,
        headers: {
          'X-API-Key':    this.apiKey,
          'Content-Type': 'application/json',
          'Accept':       'application/json',
          ...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
        },
      }, (res) => {
        let raw = '';
        res.on('data', chunk => raw += chunk);
        res.on('end', () => {
          try {
            const data = JSON.parse(raw);
            data._httpCode = res.statusCode;
            resolve(data);
          } catch {
            reject(new Error(`Non-JSON response (HTTP ${res.statusCode})`));
          }
        });
      });

      req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
      req.on('error', reject);
      if (payload) req.write(payload);
      req.end();
    });
  }

  balance()                              { return this._request('GET',  '/sms/balance'); }
  send(to, message)                      { return this._request('POST', '/sms/send',  { to, message }); }
  bulk(to, message, campaign = '')       { return this._request('POST', '/sms/bulk',  { to, message, ...(campaign ? { campaign } : {}) }); }
  bulkToGroup(groupId, message, campaign = '') {
    return this._request('POST', '/sms/bulk', { group_id: groupId, message, ...(campaign ? { campaign } : {}) });
  }
}

module.exports = SureConnectClient;

Usage

index.js
require('dotenv').config();
const SureConnectClient = require('./sureconnect');

const sms = new SureConnectClient(process.env.SURECONNECT_API_KEY);

(async () => {
  const { balance } = await sms.balance();
  console.log(`Balance: ${balance} credits`);

  const res = await sms.send('0712345678', 'Hello from Node.js!');
  console.log(`Sent! ID: ${res.message_id}`);
})();
Python 3.8+. Requires requests (pip install requests). Session reuses the TCP connection and retries automatically on server errors.

Install

bash
pip install requests python-dotenv

Client class

sureconnect.py
import os
from typing import Optional
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


class SureConnectError(Exception):
    def __init__(self, http_code: int, error: str, response: dict):
        super().__init__(f"HTTP {http_code}: {error}")
        self.http_code = http_code
        self.response  = response


class SureConnectClient:
    def __init__(
        self,
        api_key:  str,
        base_url: str = "https://api.sms.ultimatesolutions.co.ke/v1",
        timeout:  int = 30,
    ):
        self.base_url = base_url.rstrip("/")
        self.timeout  = timeout

        retry = Retry(
            total=3, backoff_factor=1,
            status_forcelist=[500, 502, 503, 504],
            allowed_methods=["GET", "POST"],
        )
        self._session = requests.Session()
        self._session.mount("https://", HTTPAdapter(max_retries=retry))
        self._session.headers.update({
            "X-API-Key":    api_key,
            "Content-Type": "application/json",
            "Accept":       "application/json",
        })

    def _request(self, method: str, path: str, json: Optional[dict] = None) -> dict:
        resp = self._session.request(method, self.base_url + path, json=json, timeout=self.timeout)
        data = resp.json()
        data["_http_code"] = resp.status_code
        if not data.get("success") and resp.status_code >= 400:
            raise SureConnectError(resp.status_code, data.get("error", "Unknown error"), data)
        return data

    def balance(self) -> dict:
        return self._request("GET", "/sms/balance")

    def send(self, to: str, message: str) -> dict:
        return self._request("POST", "/sms/send", {"to": to, "message": message})

    def bulk(self, to: list[str], message: str, campaign: str = "") -> dict:
        payload = {"to": to, "message": message}
        if campaign: payload["campaign"] = campaign
        return self._request("POST", "/sms/bulk", payload)

    def bulk_to_group(self, group_id: int, message: str, campaign: str = "") -> dict:
        payload = {"group_id": group_id, "message": message}
        if campaign: payload["campaign"] = campaign
        return self._request("POST", "/sms/bulk", payload)

    def close(self): self._session.close()
    def __enter__(self): return self
    def __exit__(self, *_): self.close()


# ── Usage ─────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
    from dotenv import load_dotenv
    load_dotenv()

    with SureConnectClient(os.environ["SURECONNECT_API_KEY"]) as sms:
        bal = sms.balance()
        print(f"Balance: {bal['balance']} credits")

        try:
            res = sms.send("0712345678", "Your OTP is 482910. Expires in 5 minutes.")
            print(f"Sent! ID: {res['message_id']}, balance: {res['balance']}")
        except SureConnectError as e:
            if e.http_code == 402: print(f"Out of credits: {e.response['balance']} remaining")
            elif e.http_code == 429: print(f"Rate limited. Retry in {e.response.get('retry_after', '?')}s")
            else: print(f"Error: {e}")

        res = sms.bulk(["0712345678", "0723456789"], "Flash sale — 30% off today!", "Flash Sale June")
        print(f"Job #{res['job_id']} — {res['queued']} messages queued")
Go 1.18+ · stdlib only. Goroutine-safe. The http.Transport reuses idle connections across requests — create one client per process.

Client package

sureconnect/client.go
package sureconnect

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

const DefaultBase = "https://api.sms.ultimatesolutions.co.ke/v1"

type Client struct {
	apiKey  string
	baseURL string
	http    *http.Client
}

func New(apiKey string) *Client {
	return &Client{
		apiKey:  apiKey,
		baseURL: DefaultBase,
		http: &http.Client{
			Timeout: 30 * time.Second,
			Transport: &http.Transport{
				MaxIdleConns:    10,
				IdleConnTimeout: 90 * time.Second,
			},
		},
	}
}

type APIError struct {
	HTTPCode int
	Message  string
	Raw      map[string]any
}

func (e *APIError) Error() string {
	return fmt.Sprintf("sureconnect: HTTP %d — %s", e.HTTPCode, e.Message)
}

func (c *Client) request(ctx context.Context, method, path string, body any) (map[string]any, error) {
	var r io.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil { return nil, err }
		r = bytes.NewReader(b)
	}
	req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, r)
	if err != nil { return nil, err }
	req.Header.Set("X-API-Key",    c.apiKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept",       "application/json")

	resp, err := c.http.Do(req)
	if err != nil { return nil, err }
	defer resp.Body.Close()

	var result map[string]any
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err }
	result["_httpCode"] = float64(resp.StatusCode)

	if success, _ := result["success"].(bool); !success && resp.StatusCode >= 400 {
		msg, _ := result["error"].(string)
		return nil, &APIError{HTTPCode: resp.StatusCode, Message: msg, Raw: result}
	}
	return result, nil
}

func (c *Client) Balance(ctx context.Context) (map[string]any, error) {
	return c.request(ctx, http.MethodGet, "/sms/balance", nil)
}

func (c *Client) Send(ctx context.Context, to, message string) (map[string]any, error) {
	return c.request(ctx, http.MethodPost, "/sms/send", map[string]string{"to": to, "message": message})
}

func (c *Client) Bulk(ctx context.Context, to []string, message, campaign string) (map[string]any, error) {
	body := map[string]any{"to": to, "message": message}
	if campaign != "" { body["campaign"] = campaign }
	return c.request(ctx, http.MethodPost, "/sms/bulk", body)
}

func (c *Client) BulkToGroup(ctx context.Context, groupID int, message, campaign string) (map[string]any, error) {
	body := map[string]any{"group_id": groupID, "message": message}
	if campaign != "" { body["campaign"] = campaign }
	return c.request(ctx, http.MethodPost, "/sms/bulk", body)
}

Usage

main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"yourmodule/sureconnect"
)

func main() {
	c   := sureconnect.New(os.Getenv("SURECONNECT_API_KEY"))
	ctx := context.Background()

	bal, err := c.Balance(ctx)
	if err != nil { log.Fatal(err) }
	fmt.Println("Balance:", bal["balance"])

	res, err := c.Send(ctx, "0712345678", "Your OTP is 482910.")
	if err != nil {
		var apiErr *sureconnect.APIError
		if errors.As(err, &apiErr) {
			log.Printf("API error %d: %s", apiErr.HTTPCode, apiErr.Message)
		}
		return
	}
	fmt.Println("Sent! ID:", res["message_id"])
}
Java 11+ · Jackson · java.net.http.HttpClient. Add com.fasterxml.jackson.core:jackson-databind to your build (Maven / Gradle).

Client class

SureConnectClient.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;

public class SureConnectClient {

    public static class ApiException extends Exception {
        public final int httpCode;
        public final Map<String, Object> response;

        public ApiException(int code, String msg, Map<String, Object> resp) {
            super("HTTP " + code + ": " + msg);
            this.httpCode = code;
            this.response = resp;
        }
    }

    private static final String DEFAULT_BASE = "https://api.sms.ultimatesolutions.co.ke/v1";
    private final String     apiKey;
    private final String     baseUrl;
    private final HttpClient http;
    private final ObjectMapper mapper = new ObjectMapper();

    public SureConnectClient(String apiKey) { this(apiKey, DEFAULT_BASE); }

    public SureConnectClient(String apiKey, String baseUrl) {
        this.apiKey  = apiKey;
        this.baseUrl = baseUrl;
        this.http    = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(5)).build();
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> request(String method, String path, Object body) throws Exception {
        String json = body != null ? mapper.writeValueAsString(body) : "";
        HttpRequest.Builder req = HttpRequest.newBuilder()
            .uri(URI.create(baseUrl + path))
            .timeout(Duration.ofSeconds(30))
            .header("X-API-Key",    apiKey)
            .header("Content-Type", "application/json")
            .header("Accept",       "application/json");

        if ("POST".equals(method)) req.POST(HttpRequest.BodyPublishers.ofString(json));
        else                       req.GET();

        HttpResponse<String> resp = http.send(req.build(), HttpResponse.BodyHandlers.ofString());
        Map<String, Object> data  = mapper.readValue(resp.body(), Map.class);
        data.put("_httpCode", resp.statusCode());

        Boolean success = (Boolean) data.get("success");
        if ((success == null || !success) && resp.statusCode() >= 400) {
            String msg = (String) data.getOrDefault("error", "Unknown error");
            throw new ApiException(resp.statusCode(), msg, data);
        }
        return data;
    }

    public Map<String, Object> balance() throws Exception { return request("GET",  "/sms/balance", null); }
    public Map<String, Object> send(String to, String message) throws Exception {
        return request("POST", "/sms/send", Map.of("to", to, "message", message));
    }
    public Map<String, Object> bulk(List<String> to, String message, String campaign) throws Exception {
        Map<String, Object> body = new HashMap<>();
        body.put("to", to); body.put("message", message);
        if (campaign != null && !campaign.isEmpty()) body.put("campaign", campaign);
        return request("POST", "/sms/bulk", body);
    }
    public Map<String, Object> bulkToGroup(int groupId, String message, String campaign) throws Exception {
        Map<String, Object> body = new HashMap<>();
        body.put("group_id", groupId); body.put("message", message);
        if (campaign != null && !campaign.isEmpty()) body.put("campaign", campaign);
        return request("POST", "/sms/bulk", body);
    }

    public static void main(String[] args) throws Exception {
        var sms = new SureConnectClient(System.getenv("SURECONNECT_API_KEY"));
        var bal = sms.balance();
        System.out.println("Balance: " + bal.get("balance"));

        try {
            var res = sms.send("0712345678", "Your OTP is 482910.");
            System.out.println("Sent! ID: " + res.get("message_id"));
        } catch (ApiException e) {
            System.err.println("Error " + e.httpCode + ": " + e.getMessage());
            if (e.httpCode == 429)
                System.err.println("Retry after: " + e.response.get("retry_after") + "s");
        }
    }
}
.NET 6+ · HttpClient · System.Text.Json · no NuGet packages. Register as a singleton in your DI container — never create a new instance per request.

Client class

SureConnectClient.cs
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace SureConnect;

public class ApiException : Exception
{
    public int HttpCode { get; }
    public Dictionary<string, JsonElement>? Response { get; }
    public ApiException(int code, string message, Dictionary<string, JsonElement>? resp = null)
        : base($"HTTP {code}: {message}") { HttpCode = code; Response = resp; }
}

public sealed class SureConnectClient : IDisposable
{
    private static readonly JsonSerializerOptions JsonOpts = new()
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        PropertyNamingPolicy   = JsonNamingPolicy.SnakeCaseLower,
    };
    private readonly HttpClient _http;

    public SureConnectClient(string apiKey, string baseUrl = "https://api.sms.ultimatesolutions.co.ke/v1")
    {
        _http = new HttpClient
        {
            BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/'),
            Timeout     = TimeSpan.FromSeconds(30),
        };
        _http.DefaultRequestHeaders.Add("X-API-Key", apiKey);
        _http.DefaultRequestHeaders.Add("Accept",    "application/json");
    }

    private async Task<Dictionary<string, JsonElement>> RequestAsync(
        HttpMethod method, string path, object? body = null, CancellationToken ct = default)
    {
        using var req = new HttpRequestMessage(method, path)
        {
            Content = body is null ? null : JsonContent.Create(body, options: JsonOpts),
        };
        using var res = await _http.SendAsync(req, ct);
        var data = await res.Content
            .ReadFromJsonAsync<Dictionary<string, JsonElement>>(JsonOpts, ct)
            ?? throw new InvalidOperationException("Empty response");

        if (!res.IsSuccessStatusCode)
        {
            var msg = data.TryGetValue("error", out var e) ? e.GetString() : "Unknown error";
            throw new ApiException((int)res.StatusCode, msg ?? "Unknown error", data);
        }
        return data;
    }

    public Task<Dictionary<string, JsonElement>> BalanceAsync(CancellationToken ct = default)
        => RequestAsync(HttpMethod.Get, "sms/balance", ct: ct);

    public Task<Dictionary<string, JsonElement>> SendAsync(string to, string message, CancellationToken ct = default)
        => RequestAsync(HttpMethod.Post, "sms/send", new { to, message }, ct);

    public Task<Dictionary<string, JsonElement>> BulkAsync(
        string[] to, string message, string? campaign = null, CancellationToken ct = default)
        => RequestAsync(HttpMethod.Post, "sms/bulk", new { to, message, campaign }, ct);

    public Task<Dictionary<string, JsonElement>> BulkToGroupAsync(
        int groupId, string message, string? campaign = null, CancellationToken ct = default)
        => RequestAsync(HttpMethod.Post, "sms/bulk", new { group_id = groupId, message, campaign }, ct);

    public void Dispose() => _http.Dispose();
}

// ── Usage (Program.cs) ────────────────────────────────────────────────────────
// using SureConnect;
// using var sms = new SureConnectClient(Environment.GetEnvironmentVariable("SURECONNECT_API_KEY")!);
// var bal = await sms.BalanceAsync();
// Console.WriteLine($"Balance: {bal["balance"]}");
// try {
//     var res = await sms.SendAsync("0712345678", "Your OTP is 482910.");
//     Console.WriteLine($"Sent! ID: {res["message_id"]}");
// } catch (ApiException ex) when (ex.HttpCode == 429) {
//     Console.WriteLine($"Rate limited. Retry in {ex.Response?["retry_after"].GetInt32() ?? 60}s");
// }
Ruby 2.7+ · stdlib Net::HTTP · no gems required. The HTTP connection is kept alive across calls via Net::HTTP#start.

Client class

sureconnect_client.rb
require 'net/http'
require 'json'
require 'uri'

module SureConnect
  class ApiError < StandardError
    attr_reader :http_code, :response

    def initialize(http_code, message, response = {})
      super("HTTP #{http_code}: #{message}")
      @http_code = http_code
      @response  = response
    end
  end

  class Client
    DEFAULT_BASE = 'https://api.sms.ultimatesolutions.co.ke/v1'.freeze

    def initialize(api_key, base_url = DEFAULT_BASE)
      @api_key  = api_key
      @base_uri = URI.parse(base_url)
      @http              = Net::HTTP.new(@base_uri.host, @base_uri.port)
      @http.use_ssl      = @base_uri.scheme == 'https'
      @http.open_timeout = 5
      @http.read_timeout = 30
      @http.start
    end

    def balance                           = request(:get,  '/sms/balance')
    def send_sms(to:, message:)           = request(:post, '/sms/send',  { to: to, message: message })

    def bulk(to:, message:, campaign: nil)
      body = { to: to, message: message }
      body[:campaign] = campaign if campaign
      request(:post, '/sms/bulk', body)
    end

    def bulk_to_group(group_id:, message:, campaign: nil)
      body = { group_id: group_id, message: message }
      body[:campaign] = campaign if campaign
      request(:post, '/sms/bulk', body)
    end

    def close = @http.finish if @http.started?

    private

    def request(method, path, body = nil)
      full_path = @base_uri.path.chomp('/') + path
      headers   = { 'X-API-Key' => @api_key, 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
      req = case method
            when :get  then Net::HTTP::Get.new(full_path, headers)
            when :post then Net::HTTP::Post.new(full_path, headers).tap { |r| r.body = body.to_json }
            end
      resp = @http.request(req)
      data = JSON.parse(resp.body)
      data['_http_code'] = resp.code.to_i
      raise ApiError.new(resp.code.to_i, data['error'] || 'Unknown error', data) unless data['success']
      data
    end
  end
end

# ── Usage ─────────────────────────────────────────────────────────────────────
if __FILE__ == $PROGRAM_NAME
  sms = SureConnect::Client.new(ENV.fetch('SURECONNECT_API_KEY'))

  puts "Balance: #{sms.balance['balance']} credits"

  begin
    res = sms.send_sms(to: '0712345678', message: 'Your OTP is 482910.')
    puts "Sent! ID: #{res['message_id']}, balance: #{res['balance']}"
  rescue SureConnect::ApiError => e
    case e.http_code
    when 402 then puts "Out of credits: #{e.response['balance']} remaining"
    when 429 then puts "Rate limited. Retry in #{e.response['retry_after']}s"
    else          puts "Error: #{e}"
    end
  end

  res = sms.bulk(to: ['0712345678', '0723456789'], message: 'Flash sale!', campaign: 'June')
  puts "Job ##{res['job_id']} — #{res['queued']} messages queued"
  sms.close
end