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.
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.
Send your first SMS in under 2 minutes.
Get your API key
Log in to your SureConnect dashboard → and generate an API key from the Account page. Keys start with sc_.
Check your balance
Call GET /V1/sms/balance with your key to verify authentication and see your available SMS credits.
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
# 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
{
"success": true,
"message_id": "SC-20240601-00001",
"balance": 142,
"message": "SMS queued"
}
All requests must include your API key in the X-API-Key header.
X-API-Key: sc_your_key_here
Error responses
{ "success": false, "error": "API key required" }
{ "success": false, "error": "Invalid or revoked API key" }
Three endpoints cover every SMS use case.
Returns current credit balance, quota, and usage for the authenticated account. No request body required.
curl -s "https://api.sms.ultimatesolutions.co.ke/v1/sms/balance" \
-H "X-API-Key: $SC_KEY"
{
"success": true,
"balance": 150,
"quota": 500,
"used": 350
}
Send a single SMS immediately. Deducts one credit on success and returns a message_id for delivery tracking.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | required | Recipient phone. 0712345678 or 254712345678 |
| message | string | required | SMS text body (max 918 chars / 6 segments) |
| scheduled_at | string | optional | ISO 8601 datetime to schedule. Omit to send immediately. |
{
"to": "0712345678",
"message": "Your OTP is 482910. Expires in 5 min."
}
{
"success": true,
"message_id": "SC-20240601-00001",
"balance": 149,
"message": "SMS sent"
}
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
| Field | Type | Required | Description |
|---|---|---|---|
| to | string[] | one of | Array of phone numbers (max 10,000 per request) |
| group_id | integer | one of | ID of a saved contact group (alternative to to) |
| message | string | required | SMS text body |
| campaign | string | optional | Campaign label for analytics (max 100 chars) |
| scheduled_at | string | optional | ISO 8601 datetime to schedule delivery |
to (number array) or group_id — not both.{
"to": ["0712345678","0723456789","254733000111"],
"message": "Flash sale — 30% off today!",
"campaign": "Flash Sale June"
}
{
"success": true,
"job_id": 1042,
"queued": 3,
"balance": 147,
"message": "Bulk SMS job queued"
}
{
"group_id": 5,
"message": "Your monthly statement is ready.",
"campaign": "June Statements"
}
Production-ready client classes for every major language. Copy, drop in, ship.
jq for pretty JSON output.Setup
export SC_KEY="sc_your_key_here"
export SC_BASE="https://api.sms.ultimatesolutions.co.ke/v1"
Check balance
curl -s "$SC_BASE/sms/balance" -H "X-API-Key: $SC_KEY" | jq .
Send a single SMS
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
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
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 .
curl_* functions. Compatible with Laravel, Symfony, CodeIgniter, and plain PHP.Environment (.env)
SURECONNECT_API_KEY=sc_your_key_here
Client class
<?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');
Client class · Browser / Node 18+ (native fetch)
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`);
https module — no axios, node-fetch, or got required.Client module
'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
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}`);
})();
requests (pip install requests). Session reuses the TCP connection and retries automatically on server errors.Install
pip install requests python-dotenv
Client class
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")
http.Transport reuses idle connections across requests — create one client per process.Client package
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
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"])
}
com.fasterxml.jackson.core:jackson-databind to your build (Maven / Gradle).Client class
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");
}
}
}
Client class
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");
// }
Net::HTTP#start.Client class
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