Base URL: https://api.smskit.cloud · Auth header: X-Api-Key: sk_live_…
Send SMS — POST /api/sms/send
Queue an SMS for delivery. Returns immediately with a job id (202 Accepted); delivery
happens asynchronously on your phone.
Request body
Field
Type
Req
Notes
recipient
string
Yes
E.164 (+254712345678). Numbers without + are treated as Kenyan (+254).
message
string
Yes
Up to 1,600 characters. Long messages are split + reassembled.
priority
int (0–10)
—
Lower = more urgent (dispatched first). Omitted = 0, the front of the queue. Give bulk sends a higher value (e.g. 3) so OTPs and transactional messages jump ahead.
devices
array
—
Target specific phones/SIMs. Omit to let SMSKit choose.
idempotencyKey
string
—
Safe retries — resending the same key returns the original job instead of sending twice.
Pass an idempotencyKey (any string unique to the
logical send, e.g. your own message id) and retries become safe: if a job already exists
with that key, the API returns the original job — its jobId and current status — instead of queuing a duplicate. Use it
whenever you retry on timeouts, 429s, or network errors, so an OTP is never sent twice.
Targeting specific phones and SIMs
By default SMSKit picks the best enabled device + SIM on your account. To restrict a send
to specific phones (and optionally specific SIM slots), pass a devices array — ids come from GET /api/devices. Omit simSlots on an entry to allow any enabled SIM on that phone:
Referencing a device from another account returns 403; if none of the targeted channels is
eligible to send, you get 422.
Job status — GET /api/sms/{jobId}/status
Poll a job through its lifecycle: Pending → Assigned → Sent → Delivered, or Failed /
DeadLetter. assignedDevice, sentAt, and deliveredAt are null until the job reaches that stage.
Subscribe a URL and SMSKit POSTs an event the moment something happens — no polling.
Events: sms:sent · sms:delivered · sms:failed · sms:received · archive:import_completed. Which events you can
subscribe to depends on your plan.
The response includes the webhook's signing secret (shown once — store it).
Other endpoints: GET /api/webhooks (list), GET /api/webhooks/{id} (detail), PATCH /api/webhooks/{id} (update url / events / active state), DELETE /api/webhooks/{id}, POST /api/webhooks/{id}/ping (send a test event).
Every delivery is signed with your webhook's secret so you can confirm it came from
SMSKit and wasn't tampered with. Two schemes are sent on each request:
• Recommended:X-SMSKit-Timestamp (unix seconds) + X-SMSKit-Signature-256 — a hex HMAC-SHA256 over the string "{timestamp}.{rawBody}". Recompute it, compare with a constant-time compare, and reject timestamps older than 5 minutes
(replay protection).
• Legacy:X-SMSKit-Signature — a hex HMAC-SHA256 of the
raw request body alone. Kept for existing integrations; it has no replay protection, so
prefer the timestamped scheme.
Always verify against the raw request bytes — re-serializing parsed JSON
can change the byte sequence and break the signature.
import crypto from "node:crypto";
// rawBody must be the exact bytes received — parse JSON only AFTER verifying.
function verifyWebhook(headers, rawBody, secret) {
const ts = headers["x-smskit-timestamp"];
const sig = headers["x-smskit-signature-256"];
if (!ts || !sig) return false;
// Replay protection: reject stale timestamps (older than 5 minutes).
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(sig);
return a.length === b.length && crypto.timingSafeEqual(a, b); // constant-time
}
Delivery semantics
• At-least-once — a delivery can occasionally arrive more than once.
Every payload carries a deliveryId (unique per delivery) and an eventId (unique per event); record processed ids and skip duplicates.
• Retries — a non-2xx response or connection error is retried up to 6 attempts with exponential backoff (starting around a minute and
doubling, with jitter, capped at an hour).
• Auto-pause — an endpoint whose deliveries keep failing is
automatically paused and you get an email notice. Re-enable it from the dashboard, or
via PATCH /api/webhooks/{id} with { "isActive": true }.
• Respond fast — return 2xx immediately and process the event
asynchronously; slow handlers get counted as failures.
Two-way messaging
Inbound SMS are threaded into conversations and surfaced two ways: the sms:received webhook (push, recommended) and the dashboard inbox. To pull
inbound and outbound history programmatically, use the archive / messages API below.
Replies sent from the dashboard route back through the device that owns the conversation.
You are responsible for honoring opt-out requests (e.g. a recipient replying STOP). The sms:received webhook and inbox surface those replies so you can detect and suppress
them — automatic STOP keyword handling is not enforced server-side.
SMS archiving & export
Business SMS Archival keeps a complete, retrievable record of your messages. A device can
import the inbox and sent history already on the phone (how archiving works); from there a pull API exposes your account's unified timeline —
outbound jobs, inbound messages, and imported history, each tagged with an origin — plus a bulk export and a contacts store so peer numbers resolve to
names. Same X-Api-Key auth and rate limit as the send API.
Invalid input (bad number, empty message, message > 1600, priority out of 0–10)
Fix the request; the body explains what.
401 Unauthorized
Missing/invalid key, or suspended client
Check the key and the client's status.
403 Forbidden
Referenced a device/SIM from another account
Use ids from your own GET /api/devices.
404 Not Found
Unknown job id
Verify the id.
422 Unprocessable
No eligible device/SIM to send
Bring a phone online or relax targeting.
429 Too Many Requests
Rate limited
Honor Retry-After; back off.
Rate limits
The client API is limited per API client (currently 100 requests/hour). On 429, read Retry-After and back off. Need more throughput? It's about send capacity (more
phones/SIMs), not request rate — add devices. See also the SMS throttling & pacing guide for the per-device sending limits.
Handling 429 — a retry recipe
Honor the Retry-After header (seconds) when present, fall back to exponential
backoff, and pair retries with an idempotency key so a retried
request can never double-send:
// Honor Retry-After on 429 — safe to retry because payload carries an idempotencyKey.
async function sendSms(payload, maxTries = 5) {
for (let attempt = 0; attempt < maxTries; attempt++) {
const res = await fetch("https://api.smskit.cloud/api/sms/send", {
method: "POST",
headers: { "X-Api-Key": "sk_live_your_key", "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (res.status !== 429) return res;
const waitSeconds = Number(res.headers.get("Retry-After") ?? 2 ** attempt * 5);
await new Promise((r) => setTimeout(r, waitSeconds * 1000));
}
throw new Error("Still rate limited after retries");
}
SDKs & samples
.NET — SMSKit.ApiClient (official reference SDK). Typed methods for send, status,
and devices; handles auth, base URL, JSON, and error mapping.