Build reliable messaging workflows with a clear, live reference.
Authentication, request examples, permissions, aliases, response behaviour and error handling are arranged so developers can move from first call to production without hunting through scattered notes.
Introduction
SwalaSMS Developer API lets your system send and track SMS messages through the same routing, sender ID, wallet, spam-control, and country-credit rules used by the web portal.
Use a sandbox API key while testing. Sandbox keys return realistic queued/delivered responses but never send real SMS or consume SMS credits. Use a live key only after your sender IDs, SMS route, wallet balance, and provider credits are ready.
Every request must include Authorization: Bearer YOUR_API_KEY. Send endpoints also support Idempotency-Key; pass a stable unique value from your own system when retrying a request so the same SMS is not queued twice.
Phone numbers should be sent in E.164 format, for example +255712345678. A successful send returns 202 Accepted because delivery is queued and processed asynchronously. Use the status endpoint to check the current state of a message.
Common statuses are queued, processing, sent, delivered, failed, and scheduled. Validation, sender ID, route, wallet, provider-credit, spam-policy, and recipient opt-out errors return JSON with success: false and a readable message.
Every request carries the key issued with your application as a bearer token. A key is shown once, when it is created. The base URL is:
https://api.swalasms.com
Authorization: Bearer swl_live_YOUR_API_KEY
A key carries only the permissions granted to its application. Each endpoint below
names the one it needs; a key without it is answered with 401, the same as a
key that is missing or revoked. Live keys begin swl_live_ and sandbox
keys swl_test_; the placeholder above is not a key and is refused.
Messaging
Send messages and check delivery and balance.
Check SMS balance
Also answers at
https://api.swalasms.com/v1/sms/balance with identical behaviour.
Return the customer SMS credit balance for the API key owner.
The top-level balance is the total available SMS units across all enabled country wallets. The countries array breaks the balance down per country with wallet UID, ISO country code, calling code, and remaining SMS units.
Use this endpoint before sending if your application needs to warn users before a campaign is queued. The actual send endpoints still perform the final wallet and provider-credit checks inside a database transaction.
Example
curl -X GET "https://api.swalasms.com/v1/balance" \ -H "Authorization: Bearer swl_live_YOUR_API_KEY"
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"mode": "live",
"contract_version": "2026-09-phase05.5",
"data": {
"balance": "value",
"currency": "SMS",
"countries": [
{
"wallet_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"country": "TZ",
"calling_code": "+255",
"balance": "value"
}
]
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
Send a single SMS
Also answers at
https://api.swalasms.com/v1/sms/quick-message with identical behaviour.
Queue one SMS for delivery through the active SwalaSMS routing engine, under the same rules a message gets when it is composed in the portal. Nothing is relaxed because the request arrived over the API.
The API key owner must have an active account, an approved sender ID assigned to the account, a verified SMS route for the destination country, enough customer SMS credit, and an available provider route with provider credit. The message body is checked by the spam policy before it is accepted.
Send recipient in E.164 format, for example +255712345678, and sender_id as one of your own approved sender ID names. GET /api/v1/sender-ids lists them. The placeholder in the example below belongs to no account, so pasting it unchanged is refused with 422.
Delivery-safety contract: errors include stable error_code and acceptance_state. DEFINITELY_NOT_ACCEPTED is emitted only by preacceptance guards. UNKNOWN preserves the original reference and requires reconciliation. Policy, spam, opt-out, validation and identity conflicts never authorize switching providers. Sandbox is synthetic and does not certify live idempotency. Prefer one Gateway account per Enterprise Organization; separate keys share account wallet/reference/lookup state.
Idempotency-Key must be unique per message. It exists so a retry cannot send twice: repeat a request with the same key and the same content, and you get the first message back with idempotent_replay: true, and nothing is sent again. Send different content under a key already used and the request is refused with 409 idempotency_key_reused rather than answered with the earlier message. Keys are remembered for the lifetime of the stored message. Generate a fresh one per send, or leave the header out.
Successful requests return 202 Accepted because the SMS is queued and delivered asynchronously. The response carries the message uid for your own records; delivery outcomes arrive at your configured webhook.
Send GSM-7 text. An SMS holds 160 GSM-7 characters per part, or only 70 once a single character falls outside that alphabet, so the same message can cost twice the credits or more. Emoji, curly quotes, long dashes, non-breaking spaces and zero-width characters routinely arrive in text copied from a CMS, a document or a chat window, and none of them are visible.
SwalaSMS converts what it safely can before charging: smart quotes become straight quotes, a long dash becomes a hyphen, invisible characters are dropped, and an accent the alphabet cannot carry is folded to its plain letter. The response reports this under encoding: requested, final, converted, segments and a changes list naming every character that was altered. Nothing is refused for encoding.
Text in a script GSM-7 has no room for, such as Arabic, Cyrillic or Chinese, is sent exactly as you wrote it, as Unicode, and charged at the Unicode rate. encoding.notice says so when it happens.
For one message to many recipients, or a personalised message to each, use POST /api/v2/sms/send-bulk. Repeating the same body and sender through this single-message endpoint is rate-limited.
Body
| Field | Type | Notes |
|---|---|---|
recipient
*
|
string | max 32 characters |
sender_id
*
|
string | max 32 characters |
body
*
|
string | max 1600 characters |
Example
curl -X POST "https://api.swalasms.com/v1/sms/messages" \
-H "Authorization: Bearer swl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: c34c7bb1-1e37-4fb3-99b7-2aa79bc372db" \
-d '{
"recipient": "+255712345678",
"sender_id": "YOURBRAND",
"body": "Your order is ready for pickup."
}'
Idempotency-Key must be unique per message. Reusing a key returns the
message the first request created. Nothing is sent the second time, and the response
says "idempotent_replay": true. Reusing one for a different
message is answered with 409 rather than sent. Keys are remembered for 24
hours. Generate one per send (a UUID), or omit the header entirely.
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"mode": "live",
"data": {
"uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"recipient": "+255712345678",
"sender_id": "SWALASMS",
"units": 1,
"channel": "api",
"status": "queued",
"queued_at": "2026-08-25T09:00:00+00:00",
"sent_at": null,
"delivered_at": null,
"failure_reason": null
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "The request body is not valid JSON (Syntax error), so no fields could be read from it and nothing was sent. Check for unterminated strings and missing commas."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"error": "idempotency_key_reused",
"message": "The Idempotency-Key \"3f1c…\" was already used for a different message on 2026-08-25T09:00:00+00:00. Send a new, unique key for each new message, or repeat the original request exactly to receive the earlier result.",
"data": {
"idempotency_key": "3f1c…",
"earlier_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"earlier_created_at": "2026-08-25T09:00:00+00:00"
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "The recipient field is required. (and 1 more error)",
"errors": {
"recipient": [
"The recipient field is required. Send one phone number in E.164 format, for example +255712345678."
],
"sender_id": [
"The sender_id field is required. Use an approved sender ID name from GET /api/v1/sender-ids."
]
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "\"YOURBRAND\" is not an approved sender ID on this account. Use one of the approved sender IDs the account owns; the API lists them at GET /api/v1/sender-ids."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
List messages and their delivery status
Every message this account has sent, newest first, with what became of it.
A send returns queued: the carrier decides delivery seconds or minutes later, so this is how an integration finds out. Poll it, or read one message directly with GET /sms/messages/{uid} using the uid the send returned.
Filter with status (queued, sent, delivered, failed), channel (api or web, to separate your integration's traffic from messages staff sent in the portal), and from / to as dates. per_page defaults to 50 and is capped at 100.
Each entry carries status, queued_at, sent_at, delivered_at, failure_reason and the billed units.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
status
|
query | string | Delivery status: queued, sent, delivered or failed. |
channel
|
query | string | Where the message came from: api or web. |
from
|
query | string | Only messages created on or after this date (YYYY-MM-DD), inclusive. |
to
|
query | string | Only messages created on or before this date (YYYY-MM-DD), inclusive. |
per_page
|
query | integer | Messages per page. Defaults to 50, capped at 100. |
page
|
query | integer | Page number, starting at 1. |
Example
curl -X GET "https://api.swalasms.com/v1/sms/messages?status=delivered&channel=api&from=2026-08-01&to=2026-08-31&per_page=50&page=1" \ -H "Authorization: Bearer swl_live_YOUR_API_KEY"
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"data": [
{
"uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"recipient": "+255712345678",
"sender_id": "SWALASMS",
"units": 1,
"channel": "api",
"status": "delivered",
"queued_at": "2026-08-25T09:00:00+00:00",
"sent_at": "2026-08-25T09:00:02+00:00",
"delivered_at": "2026-08-25T09:00:07+00:00",
"failure_reason": null
}
],
"meta": {
"current_page": 1,
"per_page": 50,
"total": 42,
"last_page": 3
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
Check one message
The delivery status of a single message, by the uid the send returned.
status moves queued → sent → delivered, or ends at failed with a failure_reason written for a person. delivered_at is set the moment the carrier confirms it. The response carries the queued, sent, delivered and failure timestamps when they are known; which provider carried the message is not exposed.
An invalid UID is answered with a validation error, and a valid one belonging to another account with 404. The two are told apart deliberately, so a wrong id is a mistake to fix rather than a hint about somebody else's traffic.
A sandbox key answers delivered immediately without looking anything up, so an integration can be built end to end before a real message is ever sent.
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
uid
* |
path | string |
Example
curl -X GET "https://api.swalasms.com/v1/sms/messages/{uid}" \
-H "Authorization: Bearer swl_live_YOUR_API_KEY"
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"data": {
"uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"recipient": "+255712345678",
"sender_id": "SWALASMS",
"units": 1,
"channel": "api",
"status": "delivered",
"queued_at": "2026-08-25T09:00:00+00:00",
"sent_at": "2026-08-25T09:00:02+00:00",
"delivered_at": "2026-08-25T09:00:07+00:00",
"failure_reason": null,
"campaign_uid": "4c3b2a19-5566-7788-99aa-bbccddeeff00",
"campaign_name": "August Promo",
"failure_context": null,
"delivery_report": {
"checked_at": "2026-08-25T09:05:00+00:00",
"status": "delivered",
"delivery_status": "DELIVRD"
}
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "SMS message was not found for this API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Message UID must be a valid UUID."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
Send bulk SMS
Send one campaign to up to 1000 recipients in one request: the same SMS to everyone, a personalised SMS for each person, or both in the same campaign.
Each entry in recipients is either a phone number, which receives message as written, or an object with to and the values for that person's {tags}:
{
"recipients": [
{ "to": "+255755000001", "name": "Amina", "amount": 45000 },
{ "to": "+255755000002", "name": "Juma", "amount": 12500 },
"+255755000003"
],
"message": "Habari {name}, deni lako ni TZS {amount}.",
"sender_id": "YOURBRAND"
}
Tags are filled exactly as in the portal: names are not case-sensitive, a tag with no value is removed along with the space it leaves, and arithmetic such as {= amount - paid } is worked out per recipient. A bare number has no values, so its tags are simply removed.
Every message is built and priced before anything is sent. Each recipient is charged for their own finished message, so a long name can cost one more part than a short one. If the account's balance cannot cover the whole campaign, nothing is sent and the request is answered 422 with INSUFFICIENT_BALANCE, the units needed and the units available.
You get every message's id at once. The whole campaign is saved and charged before the response is written -- about a second and a half for 1000 recipients -- and data.messages lists one entry per recipient with its message_uid, the same id GET /api/v1/sms/messages/{uid} and the delivery webhooks use. Sending starts straight after.
A number listed twice is sent once, to the first entry, and the second is reported under failures. A recipient whose message comes out empty or over 1600 characters is reported there too, and the rest of the campaign goes ahead.
Idempotency-Key must be unique per campaign. An identical repeat returns the same campaign and the same message ids with idempotent_replay: true and sends nothing again; a different campaign under a used key is refused with 409.
Send GSM-7 text. A part holds 160 GSM-7 characters, or 70 once one character falls outside that alphabet. Each message is converted where that is safe before it is priced, and data.messages[].units shows what each one costs.
This replaces POST /api/v1/sms/send-bulk, which keeps working for existing integrations.
Body
| Field | Type | Notes |
|---|---|---|
recipients
*
|
array | One to 1000 recipients. Each is a phone number, or an object with "to" and the values for that person's {tags}. · max 1000 items |
message
*
|
string | The SMS text. {tags} are filled from each recipient's values; a bare number gets it with its tags removed. · max 1600 characters |
sender_id
*
|
string | Your approved SwalaSMS sender ID name. · max 32 characters |
campaign_name
optional
|
string | A name for this campaign in reports. · max 120 characters |
scheduled_at
optional
|
string | Future ISO-8601 date/time, or omit/null to send now. |
Example
curl -X POST "https://api.swalasms.com/v2/sms/send-bulk" \
-H "Authorization: Bearer swl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5538a588-186d-4a3f-a156-70799b04339d" \
-d '{
"recipients": [
{
"to": "+255755000001",
"name": "Amina",
"amount": 45000
},
{
"to": "+255755000002",
"name": "Juma",
"amount": 12500
},
"+255755000003"
],
"message": "Habari {name}, deni lako ni TZS {amount}.",
"sender_id": "YOURBRAND",
"campaign_name": "June Reminders",
"scheduled_at": null
}'
Idempotency-Key must be unique per message. Reusing a key returns the
message the first request created. Nothing is sent the second time, and the response
says "idempotent_replay": true. Reusing one for a different
message is answered with 409 rather than sent. Keys are remembered for 24
hours. Generate one per send (a UUID), or omit the header entirely.
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"mode": "sandbox",
"data": {
"uid": "4c3b2a19-5566-7788-99aa-bbccddeeff00",
"name": "June Reminders",
"status": "queued",
"recipient_count": 3,
"queued_count": 3,
"failed_count": 0,
"total_units": 3,
"scheduled_at": null,
"idempotent_replay": false,
"queueing": false,
"messages": [
{
"to": "+255755000001",
"message_uid": "9f2c1e3a-4b7d-5e2b-9c3e-2f6a7d8b1c4e",
"status": "queued",
"units": 1
},
{
"to": "+255755000002",
"message_uid": "1a2b3c4d-5e6f-5a7b-8c9d-0e1f2a3b4c5d",
"status": "queued",
"units": 1
},
{
"to": "+255755000003",
"message_uid": "7c6b5a49-3827-5f1e-9d0c-b1a2c3d4e5f6",
"status": "queued",
"units": 1
}
],
"failures": []
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "The request body is not valid JSON (Syntax error), so no fields could be read from it and nothing was sent. Check for unterminated strings and missing commas."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"error": "idempotency_key_reused",
"message": "The Idempotency-Key \"3f1c…\" was already used for a different message on 2026-08-25T09:00:00+00:00. Send a new, unique key for each new message, or repeat the original request exactly to receive the earlier result.",
"data": {
"idempotency_key": "3f1c…",
"earlier_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"earlier_created_at": "2026-08-25T09:00:00+00:00"
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Each recipient object needs \"to\" set to a phone number, for example +255712345678.",
"errors": {
"recipients.1": [
"Each recipient object needs \"to\" set to a phone number, for example +255712345678."
]
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "\"YOURBRAND\" is not an approved sender ID on this account. Use one of the approved sender IDs the account owns; the API lists them at GET /api/v1/sender-ids."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
Sender IDs
Request a sender ID and track its approval. Mirrors the web flow exactly: start with the requirements call, listed first.
Get sender ID requirements
Also answers at
https://api.swalasms.com/v1/sms/sender-ids/requirements with identical behaviour.
Call this before requesting a sender ID. It returns everything that decides whether a request will be accepted, and those rules differ per platform and per account.
eligibility.can_request:falsemeans the account has not bought enough SMS credits yet.required_unitsandminimum_purchase_amountsay how much is needed. Do not attempt the request.countries[]: the only countries this account may request for. A country appears here once a phone number has been verified for it under SMS Routes, because a sender ID is registered per country.documents[]: every entry withis_required: truemust be attached when you request. Use itsuid.sender_id_rules: 3 to 11 characters of letters, numbers and single spaces between words, and a list of reserved words that are always refused. Case is yours to choose and is carried through to the handset.purposes: the allowed purpose values; send exactly one aspurpose.sample_message_rules: 50 to 500 characters.
Example
curl -X GET "https://api.swalasms.com/v1/sender-ids/requirements" \ -H "Authorization: Bearer swl_live_YOUR_API_KEY"
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"data": {
"eligibility": {
"can_request": true,
"purchased_units": 5000,
"required_units": 50,
"currency": "TZS",
"minimum_purchase_amount": "125000.00",
"price_per_unit": "25.00",
"reason": null
},
"countries": [
{
"uid": "ab98170f-3c7a-4cbe-8e3f-71a424b127e6",
"name": "Tanzania",
"iso2": "TZ",
"calling_code": "+255"
}
],
"documents": [],
"documents_required": false,
"name_reservation": {
"supported": true,
"required": false,
"request_field": "protection_requested",
"documents_field": "protection_documents_base64",
"max_documents": 3,
"accepted_formats": [
"pdf",
"jpg",
"jpeg",
"png"
],
"max_size_kb": 2048,
"description": "Optional. Set protection_requested to true and attach proof that you own the name to have it reserved for you."
},
"purposes": [
"Notification",
"Transactional",
"OTP / Verification",
"Promotional",
"Marketing",
"Alerts & Reminders",
"Other"
],
"sender_id_rules": {
"min_length": 3,
"max_length": 11,
"pattern": "^[A-Za-z0-9]+( [A-Za-z0-9]+)*$",
"description": "Letters, numbers, and single spaces between words.",
"reserved_names": [
"SWALASMS",
"SWALA"
]
},
"sample_message_rules": {
"min_length": 50,
"max_length": 500,
"description": "A real example of the messages you will send with this sender ID. Reviewers read it."
}
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
List sender IDs
Also answers at
https://api.swalasms.com/v1/sms/sender-ids with identical behaviour.
Every sender ID this account can send with, plus every request it has made, at any status.
Use this to poll a request submitted with POST /sender-ids. A sender ID may only be used for sending once is_usable is true (status is approved). When a request was turned down, rejection_reason explains why.
Statuses: pending, approved, rejected, suspended.
Example
curl -X GET "https://api.swalasms.com/v1/sender-ids" \ -H "Authorization: Bearer swl_live_YOUR_API_KEY"
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"data": [
{
"uid": "a1b2c3d4-1111-2222-3333-444455556666",
"sender_id": "SWALASMS",
"country_uid": "ab98170f-3c7a-4cbe-8e3f-71a424b127e6",
"country_code": "+255",
"country_name": "Tanzania",
"status": "approved",
"is_default": true,
"is_global": false,
"sample_message": "Your order has shipped and is on its way.",
"is_usable": true,
"submitted_document_uids": [
"c3d4e5f6-3333-4444-5555-666677778888"
],
"requested_at": "2026-08-01T10:00:00+00:00",
"updated_at": "2026-08-02T08:00:00+00:00"
}
]
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
Request a sender ID
Also answers at
https://api.swalasms.com/v1/sms/sender-ids with identical behaviour.
Submits a sender ID for manual review. Applies exactly the same gates as the web form, so anything refused here would have been refused there.
country_uid may be omitted: a request without one is registered for Tanzania, the only market currently open to sender ID requests. Send your own when GET /sender-ids/requirements lists more than one country.
Call GET /sender-ids/requirements first. It answers whether this account is eligible, which countries it may request for, and which documents, if any, are mandatory. Every refusal below is something that call would have told you in advance.
Attaching documents. Usually there are none: documents_required in the requirements response is false for most platforms, and a request with no documents is then a complete request, not an incomplete one. Send documents only for requirements returned with is_required: true, either way:
- JSON:
documents_base64: [{ "requirement_uid": "…", "filename": "certificate.pdf", "content_base64": "…" }] - Multipart: a file part named
documents[<requirement_uid>]
Claiming the name (optional). The web form offers a tick box that reserves the name so nobody else may register it; the API takes the same claim as protection_requested: true plus at least one supporting document in protection_documents_base64 ([{ "filename": "…", "content_base64": "…" }]), or as multipart protection_documents[]. The sender ID is granted either way, and the claim only decides whether the name is reserved. Ticking it with nothing attached is refused, the same as on the web.
Accepted formats are pdf, jpg, jpeg and png; the size ceiling is in max_size_kb on the requirements response, because it depends on what this server accepts.
Errors.
| Status | error |
Meaning |
|---|---|---|
| 422 | sender_id_not_eligible |
Not enough SMS credits purchased yet. |
| 422 | route_not_verified |
No verified SMS route for that country. |
| 422 | country_not_available |
That country is not open on this platform. |
| 409 | sender_id_already_requested |
This name already exists for this country. Poll GET /sender-ids instead; retrying never succeeds. |
Approval is manual. Poll GET /sender-ids until is_usable is true.
Body
| Field | Type | Notes |
|---|---|---|
sender_id
*
|
string | The name recipients will see. 3 to 11 characters, letters and numbers, single spaces between words. · max 11 characters |
country_uid
optional
|
string | The country this sender ID is registered for, from GET /sender-ids/requirements. Omit it and the request is registered for Tanzania (ab98170f-3c7a-4cbe-8e3f-71a424b127e6), the only market currently open to sender ID requests. |
sample_message
*
|
string | A real message you will send from this sender ID. · 50 to 500 characters |
purpose
*
|
string | One of: Notification, Transactional, OTP / Verification, Promotional, Marketing, Alerts & Reminders, Other. |
documents_base64
optional
|
array | No document is mandatory right now, so this may be omitted entirely. If GET /sender-ids/requirements ever returns a requirement with is_required true, send it here as {requirement_uid, filename, content_base64}, or as a multipart file named documents[<requirement_uid>]. · pdf, jpg, jpeg or png, max 2 MB each |
protection_requested
optional
|
boolean | Optional. True to also claim the name, so nobody else may register it. The sender ID itself is granted either way. |
protection_documents_base64
optional
|
array | Proof that you own the name, required only when protection_requested is true. Each entry is {filename, content_base64}; multipart callers send files as protection_documents[]. · pdf, jpg, jpeg or png, max 2 MB each |
Examples
No documents needed (the usual case)
curl -X POST "https://api.swalasms.com/v1/sender-ids" \
-H "Authorization: Bearer swl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_id": "YOURBRAND",
"country_uid": "ab98170f-3c7a-4cbe-8e3f-71a424b127e6",
"sample_message": "Your order has been confirmed and is being prepared. We will send you another message as soon as it is ready for pickup.",
"purpose": "Notification"
}'
With documents, and claiming the name. Send these only when GET /sender-ids/requirements asks for them
curl -X POST "https://api.swalasms.com/v1/sender-ids" \
-H "Authorization: Bearer swl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sender_id": "YOURBRAND",
"country_uid": "ab98170f-3c7a-4cbe-8e3f-71a424b127e6",
"sample_message": "Your order has been confirmed and is being prepared. We will send you another message as soon as it is ready for pickup.",
"purpose": "Notification",
"documents_base64": [
{
"requirement_uid": "c3d4e5f6-3333-4444-5555-666677778888",
"filename": "certificate.pdf",
"content_base64": "JVBERi0xLjQKJeLjz9MKMy..."
}
],
"protection_requested": true,
"protection_documents_base64": [
{
"filename": "brela-certificate.pdf",
"content_base64": "JVBERi0xLjQKJeLjz9MKMy..."
}
]
}'
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"message": "Sender ID request submitted for review.",
"data": {
"uid": "a1b2c3d4-1111-2222-3333-444455556666",
"sender_id": "SWALASMS",
"country_uid": "ab98170f-3c7a-4cbe-8e3f-71a424b127e6",
"country_code": "+255",
"country_name": "Tanzania",
"status": "approved",
"is_default": true,
"is_global": false,
"sample_message": "Your order has shipped and is on its way.",
"is_usable": true,
"submitted_document_uids": [
"c3d4e5f6-3333-4444-5555-666677778888"
],
"requested_at": "2026-08-01T10:00:00+00:00",
"updated_at": "2026-08-02T08:00:00+00:00"
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "The request body is not valid JSON (Syntax error), so no fields could be read from it and nothing was sent. Check for unterminated strings and missing commas."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"error": "sender_id_already_requested",
"message": "You already have a request or approved sender ID named \"YOURBRAND\" for Tanzania."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "The sample message field must be at least 50 characters. (and 1 more error)",
"errors": {
"sample_message": [
"The sample message field must be at least 50 characters."
],
"purpose": [
"The selected purpose is invalid."
]
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"error": "route_not_verified",
"message": "Verify a phone number for Tanzania under SMS Routes before requesting a sender ID there."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
Account
Who the key belongs to, which routes are verified, and what is left to spend.
Read the account profile
Returns the account, its verified SMS routes and the credit balance held for each country.
Example
curl -X GET "https://api.swalasms.com/v1/sms/profile" \ -H "Authorization: Bearer swl_live_YOUR_API_KEY"
Every response carries success, the request_uid for the
attempt, and, when something went wrong, a message written for a person.
{
"success": true,
"request_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"data": {
"uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+255712345678",
"phone_verified_at": "2026-08-25T09:00:00+00:00",
"country": {
"uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"iso2": "TZ",
"calling_code": "+255",
"name": "Jane Doe"
},
"balances": [
{
"wallet_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"country_uid": "9f2c1e3a-4b7d-4e2b-9c3e-2f6a7d8b1c4e",
"country": "TZ",
"calling_code": "+255",
"balance": 500,
"currency": "SMS"
}
],
"routes": "value"
}
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "Unauthorized API key."
}
{
"success": false,
"request_uid": "7b19fc2a-6d2b-47c7-95fc-d49ae6d03528",
"message": "API rate limit exceeded."
}
Text copied out of a CMS, a document, a chat window or an AI assistant carries characters that look ordinary and are not: curly quotes, long dashes, ellipses, non-breaking spaces, zero-width joiners and emoji. Any one of them changes the encoding of the whole message.
SwalaSMS does not refuse these. Before anything is charged, the body is converted wherever that can be done without changing what it says, and the response tells you exactly what was altered. Characters in a script GSM-7 cannot carry at all, such as Arabic, Cyrillic or Chinese, are left untouched and sent as Unicode at the Unicode rate.
| What you send | What is charged | What happens |
|---|---|---|
| Plain GSM-7 text | 160 characters per part | Sent unchanged |
’ “ — … | 160 characters per part | Converted to ' " - ... |
| Non-breaking & zero-width spaces | 160 characters per part | Removed, because they were invisible |
| Emoji | 160 characters per part | Removed, because GSM-7 cannot show it |
Accents such as č, ō | 160 characters per part | Folded to c, o |
| Arabic, Cyrillic, Chinese | 70 characters per part | Sent as written, charged as Unicode |
The encoding object
Every send response carries it. Log converted and changes during
integration and you will find the character in your own data that is costing you credits.
{
"encoding": {
"requested": "Unicode",
"final": "GSM-7",
"converted": true,
"segments": 2,
"characters": 207,
"characters_per_sms": 160,
"changes": [
{
"character": "U+2019",
"name": "Right single smart quote",
"occurrences": 2,
"replaced_with": "'"
},
{
"character": "U+1F600",
"name": "Emoji",
"occurrences": 1,
"replaced_with": ""
}
],
"notice": "Characters outside the GSM-7 alphabet were converted automatically, which brought this message down from 4 credits per recipient to 2."
}
}
| Field | Meaning |
|---|---|
requested | The encoding of the body exactly as you sent it |
final | The encoding that was charged and sent: GSM-7 or Unicode |
converted | true when characters were rewritten or removed |
segments | Credits charged per recipient |
characters_per_sms | 160 for GSM-7, 70 for Unicode |
changes | Every character altered, named, with how many there were |
notice | Present only when something needs saying, written for a person |
The safest integration strips these characters at the source, before the payload is built. What SwalaSMS converts, it converts consistently, but a message that arrives as GSM-7 is a message whose cost you already know.
Webhooks send delivery reports to your application as soon as a message reaches a final state. Create an API key first, then open SMS Webhooks to attach one or more webhook endpoints to that key. Only messages sent with that API key are posted to its webhooks.
Each endpoint gets a signing secret once, when it is created or regenerated. Store it securely.
SwalaSMS signs the exact JSON body with HMAC SHA-256 and sends the digest in
X-SwalaSMS-Signature.
Events
| Event | When it is sent |
|---|---|
sms.delivered | The message was delivered. |
sms.failed | The message reached a final failed state. |
Payload
{
"event": "sms.delivered",
"message_id": "9757991f-0000-4000-9000-000000000000",
"to": "0712345678",
"status": "delivered",
"sender_id": "MYBRAND",
"error": null,
"delivered_at": "2026-08-30T09:15:00+03:00",
"timestamp": 1788069700
}
Verify Signature
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $yourWebhookSecret);
$actual = $_SERVER['HTTP_X_SWALASMS_SIGNATURE'] ?? '';
if (! hash_equals($expected, $actual)) {
http_response_code(401);
exit;
}
$payload = json_decode($body, true);
Return any 2xx response to mark the webhook delivered. Redirects, timeouts and
non-2xx responses are treated as failed attempts.
Failed webhook attempts are retried up to four times with increasing delay. Webhook delivery runs on a separate queue from SMS sending, so a slow endpoint cannot delay new messages. If an endpoint fails 10 times in a row, SwalaSMS disables it and notifies the account by email and SMS. Update or re-enable it from SMS Webhooks.
Every failure answers with JSON carrying success: false, the
request_uid for the attempt, and a message written for a person.
Quote the request_uid when contacting support.
A malformed JSON body is reported as such, with 400, rather than as a
list of missing fields: if the payload cannot be parsed, none of its fields exist as
far as the server is concerned. Pasting a payload out of a document or a chat window
is the usual cause, because both replace straight quotes with curly ones.
| Status | Meaning | What to do |
|---|---|---|
| 400 | The body claims to be JSON but could not be parsed, so no fields were read and nothing was sent | Check the payload for curly quotes, unterminated strings and missing commas |
| 401 | Key missing, expired, revoked, calling from an address it is not allowed from, or without the permission the endpoint needs | Check the key and its scopes in application settings |
| 422 | Understood but refused: no credit, unapproved sender ID, unverified route, spam policy | Read message; it names the cause |
| 429 | Too many requests | Back off and retry; the limit is per key per minute |