Sakura SMS API reference

One REST API over HTTPS and JSON. Send SMS, follow delivery, manage contacts, send one-time passcodes and WhatsApp template messages. Everything on this page describes what the API does today.

Getting started

All requests go to https://sakurasms.com over HTTPS and use JSON. A new account starts on a trial with a few free credits, so you can test before you pay. Credits do not expire.

  1. Create an account and open Developer in your portal to make an API key.
  2. Register a Sender ID for your brand from the Sender IDs page. Messages can only be sent from an approved Sender ID.
  3. Send your first message in sandbox mode, then for real.
Send your first message
curl -X POST https://sakurasms.com/api/v1/messages \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "255712345678", "sender": "ACMELTD", "message": "Your order #1234 has shipped" }'

Authentication and scopes

Send your key in the Authorization header on every request: Authorization: Bearer sk_live_.... Keys are shown once when created. We store only a fingerprint, so a lost key cannot be recovered, only replaced. Keep keys on your server, never in a mobile app or web page.

A key can be limited to scopes. A request that needs a scope the key does not have gets 403.

sms:sendSend SMS, list your Sender IDs, send and check one-time passcodes.
sms:readGranted to keys by default. Reading messages and balance only needs a valid key.
contacts:readList contacts.
contacts:writeCreate, update and delete contacts.
contacts:manageImport contacts, manage groups and message templates.
whatsapp:sendSend approved WhatsApp template messages.

Sandbox mode

Add "test": true to the body of POST /api/v1/messages, or ?test=true to the URL. The request is checked and answered as normal, but nothing is sent and no credits are used. The response has status sandbox and an id starting sandbox-.

Avoiding duplicate sends

Give each send your own unique idempotency_key. If your server retries after a timeout, the same key returns the first result with duplicate: true and does not send again or charge again.

Message length and cost

One credit pays for one message of up to 160 characters to one recipient. A message of 161 to 320 characters costs 2 credits per recipient, and so on, one more credit for each further 160 characters. Emoji and accented characters each count as one character for billing. The segments field in a response is this billing count, and cost is segments multiplied by the number of recipients.

Handsets may show very long messages, or messages with non-Latin characters, split into more parts than we bill. Keep important messages short.

Rate limits

Requests per minute, by plan. Each response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. When you go over, you get 429 and a Retry-After header. One request can carry many recipients, so send a list in to instead of many separate calls. For very large lists we suggest batches of about 1,000 recipients.

Trial10 / min
Pay-as-you-go SMS60 / min
Enterprise300 / min

Errors

Errors use normal HTTP status codes and a JSON body such as { "error": "Insufficient credits" }. If a send is accepted by us but the network refuses it, the response includes error.code GATEWAY_SEND_FAILED and no credits are charged.

400The request is malformed or a required field is missing.
401Missing, invalid, disabled or expired API key.
402Not enough credits. Top up from your portal.
403The key lacks the needed scope, or the account is not yet approved.
404The thing you asked for is not on your account.
422A field is invalid: for example no valid Tanzanian number, or a scheduled time in the past.
429Too many requests, or a plan limit. Wait for Retry-After.
5xxA problem on our side. Retry with the same idempotency_key.

Endpoints

Messages

POST/api/v1/messages

Send an SMS

Needs the sms:send scope.

Send one message to one or many recipients. Numbers may be 255XXXXXXXXX or 07XXXXXXXX. Recipients who opted out are skipped and reported. Add scheduled_at to send later.

Request body (JSON)

to
string or string[]required

One number or an array of numbers.

message
stringrequired

The text. Long messages are split into parts; see Message length and cost.

sender
string

One of your approved Sender IDs. If omitted, your default Sender ID is used.

idempotency_key
string

Your own unique key. Sending the same key again returns the first result and does not send twice.

scheduled_at
string (ISO 8601)

A future time. The message is sent then. Must be in the future.

test
boolean

true runs the request in sandbox mode: nothing is sent and nothing is charged. You can also use ?test=true.

Example request
curl -X POST https://sakurasms.com/api/v1/messages \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"VALUE","message":"VALUE"}'
Response 201
{
  "id": "clx1a2b3c4d5e6f7",
  "status": "sent",
  "accepted": 1,
  "rejected": 0,
  "results": [
    {
      "to": "255712345678",
      "status": "accepted"
    }
  ],
  "to": [
    "255712345678"
  ],
  "from": "ACMELTD",
  "cost": 1,
  "segments": 1,
  "statusUrl": "/api/v1/messages/clx1a2b3c4d5e6f7"
}
  • 401 Missing, invalid, disabled or expired key.
  • 402 Not enough credits.
  • 403 The key lacks sms:send, or the account is not yet approved (it may only send to its own verified number).
  • 422 Missing to or message, no valid Tanzanian number, a past scheduled_at, or every recipient has opted out.
  • 429 Rate limit or plan limit reached. See the Retry-After header.
POST/api/v1/sms/send

Send an SMS (alternative form)

Needs the sms:send scope.

The same job as POST /api/v1/messages, kept for existing integrations. It names the Sender ID field sender_id. New integrations should use POST /api/v1/messages.

Request body (JSON)

to
string or string[]required

One number or an array of numbers.

message
stringrequired

The text.

sender_id
string

One of your approved Sender IDs.

idempotency_key
string

Prevents a duplicate send.

Example request
curl -X POST https://sakurasms.com/api/v1/sms/send \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"VALUE","message":"VALUE"}'
Response 200
{
  "success": true,
  "messageId": "clx1a2b3c4d5e6f7",
  "status": "sent",
  "accepted": 1,
  "rejected": 0,
  "results": [
    {
      "to": "255712345678",
      "status": "accepted"
    }
  ],
  "to": "255712345678",
  "recipients": 1,
  "segments": 1,
  "creditsUsed": 1,
  "statusUrl": "/api/v1/messages/clx1a2b3c4d5e6f7"
}
GET/api/v1/messages

List your messages

Newest first. Page through results with next_cursor.

Parameters

limit
integer

Results per page. Default 20, maximum 100.

cursor
string

The next_cursor from the previous page.

Example request
curl https://sakurasms.com/api/v1/messages \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
{
  "data": [
    {
      "id": "clx1a2b3c4d5e6f7",
      "status": "sent"
    }
  ],
  "next_cursor": null
}
GET/api/v1/messages/{id}

Get one message and its delivery summary

Parameters

id
stringrequired

The message id returned when you sent it.

Example request
curl https://sakurasms.com/api/v1/messages/ID \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
{
  "id": "clx1a2b3c4d5e6f7",
  "status": "sent",
  "from": "ACMELTD",
  "message": "Your order #1234 has shipped",
  "recipients": 1,
  "cost": 1,
  "segments": 1,
  "scheduled_at": null,
  "created_at": "2026-10-06T10:00:00.000Z",
  "delivery": {
    "delivered": 1
  },
  "results": [
    {
      "to": "255712345678",
      "status": "delivered"
    }
  ]
}
  • 404 No message with that id on your account.
GET/api/v1/messages/{id}/deliveries

Per-recipient delivery results

Parameters

id
stringrequired

The message id returned when you sent it.

limit
integer

Results per page. Default 100, maximum 500.

cursor
string

The next_cursor from the previous page.

Example request
curl https://sakurasms.com/api/v1/messages/ID/deliveries \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
{
  "data": [
    {
      "id": "d_1",
      "recipient": "255712345678",
      "status": "delivered",
      "error_code": null,
      "delivered_at": "2026-10-06T10:00:03.000Z",
      "updated_at": "2026-10-06T10:00:03.000Z"
    }
  ],
  "next_cursor": null
}
  • 404 No message with that id on your account.

Account

GET/api/v1/balance

Check your credit balance

Example request
curl https://sakurasms.com/api/v1/balance \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
{
  "balance": 4200,
  "reserved": 0,
  "used": 800
}
GET/api/v1/status

Service status

No key needed. Useful for a quick health check from your own monitoring.

Example request
curl https://sakurasms.com/api/v1/status \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
{
  "status": "operational",
  "timestamp": "2026-10-06T10:00:00.000Z",
  "gateways": {
    "sms": true,
    "whatsapp": true,
    "ai": true
  }
}

Sender IDs

GET/api/v1/senderids

List your Sender IDs

Needs the sms:send scope.

Only an ACTIVE Sender ID can be used to send. Register new Sender IDs from the Sender IDs page of your portal, where you upload your business documents. Each brand you send for needs its own approved Sender ID, and you choose which one per message with the sender field.

Parameters

all
boolean

Include Sender IDs that are not yet active.

Example request
curl https://sakurasms.com/api/v1/senderids \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
[
  {
    "id": "sid_1",
    "label": "ACMELTD",
    "status": "ACTIVE",
    "isDefault": true
  }
]

Contacts

GET/api/v1/contacts

List contacts

Needs the contacts:read scope.

Parameters

page
integer

Page number, starting at 1.

limit
integer

Results per page.

q
string

Search by name or number.

Example request
curl https://sakurasms.com/api/v1/contacts \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
{
  "data": [
    {
      "id": "c_1",
      "firstName": "Asha",
      "lastName": "Mushi",
      "mobile": "255712345678"
    }
  ],
  "metadata": {
    "total": 1,
    "page": 1,
    "totalPages": 1
  }
}
POST/api/v1/contacts

Create a contact

Needs the contacts:write scope.

Request body (JSON)

firstName
stringrequired

First name.

lastName
string

Last name.

mobile
stringrequired

Mobile number.

customFields
object

Your own fields, for message personalisation.

Example request
curl -X POST https://sakurasms.com/api/v1/contacts \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firstName":"VALUE","mobile":"VALUE"}'
Response 201
{
  "id": "c_1",
  "firstName": "Asha",
  "lastName": "Mushi",
  "mobile": "255712345678"
}
PATCH/api/v1/contacts/{id}

Update a contact

Needs the contacts:write scope.

Path

id
stringrequired

The contact id.

Request body (JSON)

firstName
string

First name.

lastName
string

Last name.

customFields
object

Your own fields.

Example request
curl -X PATCH https://sakurasms.com/api/v1/contacts/{id} \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response 200
{
  "id": "c_1",
  "firstName": "Asha",
  "lastName": "Mushi",
  "mobile": "255712345678"
}
DELETE/api/v1/contacts/{id}

Delete a contact

Needs the contacts:write scope.

Path

id
stringrequired

The contact id.

Example request
curl -X DELETE https://sakurasms.com/api/v1/contacts/ID \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
{
  "success": true
}
POST/api/v1/contacts/import

Import many contacts at once

Needs the contacts:manage scope.

Request body (JSON)

contacts
object[]required

Each item has firstName, lastName and mobile.

groupName
string

Add every imported contact to this group (created if new).

Example request
curl -X POST https://sakurasms.com/api/v1/contacts/import \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contacts":"VALUE"}'
Response 200
{
  "success": true,
  "count": 250
}
GET/api/v1/audience/groups

List contact groups

Needs the contacts:manage scope.

Example request
curl https://sakurasms.com/api/v1/audience/groups \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
[
  {
    "id": "g_1",
    "name": "VIP customers"
  }
]
POST/api/v1/audience/groups

Create a contact group

Needs the contacts:manage scope.

Request body (JSON)

name
stringrequired

Group name.

description
string

Optional description.

Example request
curl -X POST https://sakurasms.com/api/v1/audience/groups \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"VALUE"}'
Response 201
{
  "id": "g_1",
  "name": "VIP customers"
}
GET/api/v1/audience/groups/{id}/contacts

List the contacts in a group

Needs the contacts:manage scope.

Parameters

id
stringrequired

The group id.

Example request
curl https://sakurasms.com/api/v1/audience/groups/ID/contacts \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
[
  {
    "id": "c_1",
    "firstName": "Asha",
    "mobile": "255712345678"
  }
]

Templates

GET/api/v1/templates

List saved message templates

Needs the contacts:manage scope.

Example request
curl https://sakurasms.com/api/v1/templates \
  -H "Authorization: Bearer sk_live_YOUR_KEY"
Response 200
[
  {
    "id": "t_1",
    "title": "Order shipped",
    "content": "Hi {{firstName}}, your order has shipped."
  }
]
POST/api/v1/templates

Save a message template

Needs the contacts:manage scope.

Request body (JSON)

title
stringrequired

A name for the template.

content
stringrequired

The message text.

Example request
curl -X POST https://sakurasms.com/api/v1/templates \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"VALUE","content":"VALUE"}'
Response 201
{
  "id": "t_1",
  "title": "Order shipped",
  "content": "Hi {{firstName}}, your order has shipped."
}

One-time passcodes

POST/api/v1/verify/request

Send a one-time passcode

Needs the sms:send scope.

Sends a code to the number and returns a request_id. The code itself is never shown to you. Check what the user types with the next call.

Request body (JSON)

phone
stringrequired

A Tanzanian number, for example +255758309999.

Example request
curl -X POST https://sakurasms.com/api/v1/verify/request \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"VALUE"}'
Response 200
{
  "request_id": "req_8f2c1d",
  "status": "pending"
}
  • 422 Not a valid Tanzanian number.
POST/api/v1/verify/check

Check a one-time passcode

Needs the sms:send scope.

Request body (JSON)

request_id
stringrequired

From the request call.

code
stringrequired

What the user entered.

Example request
curl -X POST https://sakurasms.com/api/v1/verify/check \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"request_id":"VALUE","code":"VALUE"}'
Response 200
{
  "status": "approved"
}
  • 400 request_id or code missing.

WhatsApp

POST/api/v1/whatsapp/send

Send an approved WhatsApp template message

Needs the whatsapp:send scope.

Business-started WhatsApp messages must use a template that is already approved. Your account needs a connected WhatsApp number.

Request body (JSON)

to
stringrequired

Recipient number.

template
stringrequired

The approved template name.

language
string

Template language code, for example en or sw.

components
object[]

Template variables, in WhatsApp's components format.

Example request
curl -X POST https://sakurasms.com/api/v1/whatsapp/send \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"VALUE","template":"VALUE"}'
Response 200
{
  "success": true,
  "messageId": "wamid.HBgM...",
  "to": "255712345678",
  "template": "order_shipped"
}
  • 502 WhatsApp could not send the message.

Webhooks

Add an endpoint URL and choose events in the Developer page of your portal. We send each event as a POST with a JSON body: { "event": "...", "data": { ... }, "timestamp": "..." }.

message.sentA message was handed to the network.
message.deliveredThe handset confirmed delivery. Carrier delivery reports are not always returned, so do not depend on this alone.
message.failedDelivery failed.
campaign.completedEvery message in a send has been processed.
credit.lowYour balance dropped below your alert threshold.
credit.topupCredits were added to your account.

Verify the signature

Every delivery carries X-Sakura-Event (the event name) and X-Sakura-Signature: the HMAC-SHA256 of the raw request body, written as lowercase hexadecimal, using your endpoint's secret as the key. There is no prefix. Compute the same value over the exact bytes you received and compare. Reject anything that does not match.

Node.js
import crypto from "node:crypto";

// rawBody must be the exact request body string, before any JSON parsing
function isValid(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hmac, hashlib

def is_valid(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")
PHP
<?php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, $secret);
$ok = hash_equals($expected, $_SERVER['HTTP_X_SAKURA_SIGNATURE'] ?? '');

Delivery and retries

Answer with any 2xx status within 10 seconds. Otherwise we retry up to 6 attempts in total, waiting 1, 4, 15, 30 and then 60 minutes between tries. Events can arrive more than once and out of order, so treat the message id as the key and make your handler safe to run twice.

Sending for several brands

One account can hold several approved Sender IDs, and you choose one per message with the sender field. Each brand needs its own Sender ID registered with its own business documents. All messages draw on one credit balance and appear in one account.

There are no separate client sub-accounts, per-client balances or per-client billing today. If you resell messaging to your own customers, keep your own customer records and balances in your system and send through your account. If you need separate accounts for separate clients, create one account per client. Talk to our sales team about volume pricing and your reseller needs.

Found something unclear or wrong? Tell us at hello@sakuragroup.co.tz and we will fix it.