Skip to content
Home / API documentation

Textify Africa API

Bulk SMS, WhatsApp templates, OTP verification and delivery tracking, over a small set of JSON HTTP endpoints.

REST · JSON Bearer API key https://portal.textify.africa/api/v1

Textify Africa API

Textify Africa is a bulk SMS platform for businesses in Africa. The API lets you send transactional and marketing SMS at scale, verify users with one-time passcodes (OTP), and track delivery status in real time, all through a small set of JSON HTTP endpoints.

This site covers the developer-facing API

Send SMS or WhatsApp templates, request and manage sender names, buy credit, send and verify OTPs, and receive status webhooks, everything a normal integration needs. Admin-only actions (approving a sender name, managing bundle pricing, reseller payouts) stay in the Textify Africa portal.

What you can build

  • Bulk & single SMS: send one message or thousands in a single request, immediately or scheduled for a future date.
  • WhatsApp template messages: the same send endpoint can push approved WhatsApp templates instead of SMS, billed from a separate WhatsApp wallet.
  • Delivery tracking: every message moves through a clear status lifecycle, queryable by ID or pushed to your webhook URL.
  • OTP verification: generate and verify one-time passcodes for login, signup or transaction confirmation flows, delivered by SMS, WhatsApp, or optionally also email.
  • Custom sender names: request, manage and assign your own approved brand name instead of sending under a shared system sender.
  • Buy credit: top up your SMS balance programmatically, with the same pricing and payment flow as the portal's checkout.
  • Multiple API keys: create a separate, independently revocable key per environment or integration, each with its own optional webhook URL.

How the API is organized

Every request and response follows the same conventions, described in the next few pages:

From there, the reference is grouped by resource:

  • SMS API: send SMS or WhatsApp templates, list messages, fetch a single message.
  • OTP API: send and verify one-time passcodes, over SMS or WhatsApp.
  • Sender Names: request, list, update and assign your own sender names.
  • Credits: browse bundle pricing and buy SMS credit. Not yet mirrored on this page.
  • Webhooks: receive message, sender name, and purchase status updates on your own server. Not yet mirrored on this page.

Quickstart

Once you have an API key (see Authentication), sending an SMS is one request:

curl -X POST https://portal.textify.africa/api/v1/messages \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sender_name": "MYBRAND",
    "is_scheduled": false,
    "messages": [
      { "receiver": "0712345678", "content": "Hello Asha, your order is ready" }
    ]
  }'

Every response, success or failure, comes back in the same envelope shape, so you can write one response handler for the whole API. Continue to Authentication to get your API key.

Authentication

Every endpoint except a small set marked public requires an API key, sent as a standard Authorization header.

API Keys

Create non-expiring API keys from your account in the Textify Africa portal, not from the API itself: log in, open the API section, and click New API Key. Give it a name (defaults to "Default" if left blank) so you can tell keys apart, e.g. one per environment or integration. The raw key is shown once:

{
  "success": true,
  "status_code": 201,
  "status_text": "Created",
  "message": "api key has been created",
  "data": {
    "id": "1d9b7e3a-2f7e-4b3e-9d0a-8c1a6b2e4f5c",
    "name": "Production",
    "api_key": "txf_92c58d699610fc2233f871f05fcb5b78b5f07a483a449525b53c71732af0caf6"
  }
}
Shown once, save it immediately

The server only ever stores a SHA-256 hash of your key, never the raw value, so there is no way to retrieve it again later. If you lose it, create a new one.

Multiple, independently revocable keys

An account can hold any number of active keys at once, there's no single global key that generating a new one invalidates. Create one key per integration or environment, rename or change a key's webhook URL at any time, and delete a single key without affecting any of the account's other keys. Deleting a key takes effect immediately, the very next request authenticated with it fails.

Using an API key

Send it in the Authorization header, prefixed txf_, on any protected endpoint:

curl https://portal.textify.africa/api/v1/messages \
  -H "Authorization: Bearer txf_92c58d699610fc2233f871f05fcb5b78b5f07a483a449525b53c71732af0caf6"

API keys don't expire and aren't tied to a session, so there's no logout for them.

Per-key webhook URL

Each key can optionally carry its own webhook_url, set when you create it or changed later from the portal. Sender name and purchase status callbacks are routed to the webhook_url of whichever key was used to create that sender name request or purchase, falling back to your account's default webhook URL when that key has none set. Message delivery-status callbacks always use the account default, regardless of which key sent the message. See Webhooks for the full routing rules and payload shapes.

Authentication errors

A missing or invalid key, or a disabled/unverified account, returns 401 Unauthorized with a machine-readable error code:

{
  "success": false,
  "status_code": 401,
  "status_text": "Unauthorized",
  "message": "invalid api key",
  "error": "invalid_token"
}
StatuserrorMeaning
401 missing_token No Authorization header was sent.
401 invalid_token The API key doesn't match any account.
401 account_not_found The account tied to the key no longer exists.
401 account_disabled The account has been disabled by an administrator.
401 account_not_verified The account has not completed OTP verification yet.

Base URL

Every endpoint in this reference is relative to a single base URL:

https://portal.textify.africa/api/v1

For example, sending an SMS means making a request to https://portal.textify.africa/api/v1/messages. Throughout this reference, paths are written relative to that base, e.g. POST /messages.

All traffic is HTTPS

The API only accepts HTTPS requests. Plain HTTP requests are not supported in production.

Versioning

/api/v1 is the current and only stable version. Breaking changes will be introduced under a new version prefix rather than changing this one in place.

Request & Response Format

Requests

All request bodies are JSON. Send Content-Type: application/json on every POST, PUT, PATCH and DELETE request that includes a body, and authenticate with a bearer token as described in Authentication unless the endpoint is marked public.

curl -X POST https://portal.textify.africa/api/v1/messages \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "sender_name": "MYBRAND", "messages": [ { "receiver": "0712345678", "content": "Hi" } ] }'

Response envelope

Every endpoint, success or failure, responds with the same JSON envelope:

{
  "success": true,
  "status_code": 200,
  "status_text": "OK",
  "message": "message has been sent",
  "data": { }
}

success tells you which shape you got without inspecting the status code. On success, data holds the payload and error is omitted. On failure, error is a short machine-readable code you can branch on, and message is a human-readable description safe to log or, in most cases, show to a user.

Pagination

List endpoints (like GET /messages) accept these query parameters:

ParamDefaultDescription
page 1 Page number.
limit 10 Records per page (max 1000).
sort_by created_at Column to sort by (whitelisted per endpoint).
sort_dir desc asc or desc.
search None Required on /search endpoints.

The paginated result is nested inside the standard envelope's data field:

{
  "success": true,
  "status_code": 200,
  "status_text": "OK",
  "message": "messages retrieved",
  "data": {
    "data": [ { "id": "…", "status": "delivered" } ],
    "total": 42,
    "page": 1,
    "limit": 10,
    "total_pages": 5,
    "has_next_page": true,
    "has_previous_page": false,
    "next_page": 2,
    "previous_page": null
  }
}
Data scoping

List and search endpoints only return records the caller owns. Textify admin accounts see everything.

HTTP status codes

StatusStatus TextMeaning
200 OK Request succeeded.
201 Created A resource was created.
400 Bad Request Missing or invalid fields, see error and message.
401 Unauthorized Missing/invalid API key or disabled/unverified account.
403 Forbidden The account type doesn't have permission for this action.
404 Not Found The route or the requested record doesn't exist.
405 Method Not Allowed The HTTP method isn't supported on this path.
500 Internal Server Error Unexpected server-side failure.

SMS API

Send single or bulk SMS (or WhatsApp template messages), list your message history, and fetch a single message by id. One SMS unit is 160 characters, longer content is automatically billed as multiple parts.

Send SMS

POST /messages

Creates and queues one or many messages atomically. The sender name (if provided) must be approved, enabled, and owned by, assigned to, or a shared default for the caller. Unknown recipients are auto-created as contacts. Balance (or postpaid limit) is checked and deducted in the same transaction. Admins send for free.

FieldTypeRequiredDescription
sender_name string optional An approved sender name you own or are assigned to. Omit to send without an attached sender identity. Ignored when channel is whatsapp.
messages array required One or more { receiver, content } objects. Non-empty.
messages[].receiver string required Recipient phone number. Normalized to 255… international format.
messages[].content string required Message text. 160 characters = 1 billed SMS unit; longer content is billed as ceil(length / 160) parts.
is_scheduled boolean optional Defaults to false. When true, scheduled_date is required.
scheduled_date string (ISO 8601) optional Required when is_scheduled is true. When it's due, the message moves from scheduled to sending.
channel string optional sms (default) or whatsapp. See Send via WhatsApp below for the fields that apply then.
curl -X POST https://portal.textify.africa/api/v1/messages \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sender_name": "MYBRAND",
    "is_scheduled": false,
    "messages": [
      { "receiver": "0712345678", "content": "Hello Asha, your order is ready" }
    ]
  }'

Bulk messages: send to many receivers in one call by adding more entries to messages. Scheduled send: set is_scheduled and a future scheduled_date; the message is stored with status scheduled and picked up by the background sender once due.

Response

On success the endpoint returns 201 Created with no payload. The message is queued, not returned inline. Look it up afterwards with Get Message by ID or List Messages, or subscribe to status webhooks.

{
  "success": true,
  "status_code": 201,
  "status_text": "Created",
  "message": "messages have been created",
  "data": null
}
Message lifecycle

Non-scheduled messages start as sending; scheduled ones stay scheduled until due. From there: sending → processing → sent → delivered | undelivered | failed. Delivery status is polled from the provider roughly every 30 seconds and pushed to your webhook_url when it changes.

Errors

StatuserrorMeaning
400 validation_error No messages provided, missing scheduled_date, empty content, or an invalid receiver.
400 creation_error Sender name isn't approved/enabled, isn't owned or assigned to the caller, or the balance/postpaid limit is insufficient.
401 unauthorized / invalid_token Missing or invalid API key.

Send via WhatsApp

The same POST /messages endpoint sends WhatsApp messages when channel is "whatsapp". Every WhatsApp send is a template send, freeform content isn't supported on this channel, use template_params per recipient instead.

FieldTypeRequiredDescription
channel string required Must be "whatsapp".
template_id uuid required An approved WhatsApp template, created and approved in the portal's WhatsApp section. Every WhatsApp send is a template send, there's no freeform text.
whatsapp_source string optional default (Textify's shared WhatsApp number) or own (your own registered number). Defaults to default.
messages[].receiver string required Recipient phone number. Normalized to 255… international format.
messages[].template_params array of strings optional Ordered body variable values for this recipient, matching the template's placeholders.
{
  "channel": "whatsapp",
  "template_id": "7c1a2e2a-4b0e-4e9a-9a2e-1a2b3c4d5e6f",
  "whatsapp_source": "default",
  "messages": [
    { "receiver": "0712345678", "template_params": ["Asha", "TXF-4821"] }
  ]
}
Billed from a separate WhatsApp wallet

WhatsApp sends are charged against your account's USD WhatsApp wallet balance at a live rate based on the template's category, not your regular SMS balance. Top up the wallet from the portal's WhatsApp section.

Errors

StatuserrorMeaning
400 validation_error No messages provided, or a receiver is missing/invalid.
400 creation_error template_id is missing, isn't approved, WhatsApp sending isn't configured for the account, whatsapp_source is "own" without an active registered number, or the WhatsApp wallet balance is insufficient.

List Messages

GET /messages

Paginated list of messages the caller owns (or all messages, for admins).

FieldTypeRequiredDescription
page int optional Page number. Default 1.
limit int optional Records per page. Default 10, max 1000.
sort_by string optional One of status, count, is_scheduled, scheduled_date, created_at, updated_at. Default created_at.
sort_dir string optional asc or desc. Default desc.
status string optional Filter by exact status, e.g. delivered, sent, failed.
scheduled boolean optional true returns only scheduled messages.
curl "https://portal.textify.africa/api/v1/messages?page=1&limit=20&status=delivered&sort_dir=desc" \
  -H "Authorization: Bearer $API_KEY"

There's also GET /messages/search?search=…, which accepts the same pagination parameters and matches against message content and recipient.

Get Message by ID

GET /messages/{id}

Fetch a single message, including its joined sender name, recipient and contact name.

curl https://portal.textify.africa/api/v1/messages/6b2f6e2a-4b0e-4e9a-9a2e-1a2b3c4d5e6f \
  -H "Authorization: Bearer $API_KEY"

Message object

FieldTypeRequiredDescription
id uuid required Message id.
status string required scheduled, sending, processing, processing_status, sent, delivered, undelivered, or failed.
content string required Message body as sent.
count int required Billed SMS units, ceil(length / 160).
sender_name string | null optional Joined sender name, when one was attached.
phone_number string | null optional Joined recipient number (255… format).
contact_name string | null optional Joined contact name, when known.
is_scheduled boolean required Whether this message was created as a scheduled send.
scheduled_date string | null optional ISO 8601 timestamp the message is/was due.
message_id string | null optional Provider-assigned message id, once sent.
channel string required sms or whatsapp.
template_id / template_name uuid | string | null optional The WhatsApp template used, when channel is whatsapp.
whatsapp_phone_number_id string | null optional The Meta phone number id the WhatsApp message was sent from, when channel is whatsapp.
failure_reason string | null optional Set when status is failed.
created_at string required ISO 8601 creation timestamp.

Errors

StatuserrorMeaning
400 invalid_parameter The id path segment isn't a valid UUID.
404 not_found No message exists with that id.

OTP API

Send one-time passcodes for login, signup or transaction confirmation, and verify them server-side. OTPs are single-use and expire after 30 minutes.

Send OTP

POST /otps

Sends a 6-digit code by SMS (and by email, if email_address is provided). Without sender_name, the code is delivered for free via the system sender. With a custom, approved sender_name, delivery is billed like a normal message from the caller's balance. This is the flow resellers integrate with to send OTPs under their own brand.

FieldTypeRequiredDescription
phone_number string required Recipient phone number. Normalized to 255… international format.
email_address string optional When set, the OTP is also emailed to this address.
sender_name string optional An approved sender name you own. Omit to send free from the system sender ("Textify"). Ignored when channel is whatsapp.
brand_name string optional Brand name interpolated into the OTP message. Defaults to "Textify Africa".
channel string optional sms (default) or whatsapp.
whatsapp_source string optional default (Textify's shared WhatsApp number) or own (your own registered number). Only used when channel is whatsapp.
curl -X POST https://portal.textify.africa/api/v1/otps \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "0712345678",
    "email_address": "customer@example.com",
    "sender_name": "MYBRAND",
    "brand_name": "My Brand"
  }'
Branded OTPs require authentication

POST /otps sits behind the same bearer-token auth as every other endpoint. If you pass a custom sender_name, the message is charged to the authenticated caller's balance. There's no way to send a billed, branded OTP anonymously.

Sending over WhatsApp

Set channel to "whatsapp" and whatsapp_source to pick which registered number sends it. sender_name is ignored on this channel:

curl -X POST https://portal.textify.africa/api/v1/otps \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "0712345678",
    "brand_name": "My Brand",
    "channel": "whatsapp",
    "whatsapp_source": "default"
  }'
WhatsApp OTPs are always billed, authenticated sends

There's no free system number for WhatsApp, every WhatsApp OTP is charged from the authenticated caller's WhatsApp wallet balance. Account-verification OTPs (registration, login, forgot-password) always stay on the SMS channel.

Errors

StatuserrorMeaning
400 validation_error phone_number was missing or blank.
400 creation_error sender_name isn't approved/owned, channel is whatsapp without an authenticated caller, or WhatsApp sending isn't available (misconfigured account/number).
401 unauthorized / invalid_token Missing or invalid API key.

Verify OTP

POST /otps/verify Public

Consumes a code, each OTP can only be verified once. This endpoint is public, so it can be called directly from a client app during signup or login flows, without an API key.

FieldTypeRequiredDescription
code number required The 6-digit OTP code, as a JSON number (not a string).
phone_number string optional Must match the number the OTP was sent to. Provide phone_number and/or email_address.
email_address string optional Alternative match target if the OTP was also emailed.
curl -X POST https://portal.textify.africa/api/v1/otps/verify \
  -H "Content-Type: application/json" \
  -d '{ "code": 484895, "phone_number": "0712345678" }'

Errors

StatuserrorMeaning
400 validation_error code was missing or 0.
400 verification_error No matching, unexpired OTP was found for that code and phone/email.

Sender Names

Request your own branded sender name instead of sending under the shared system sender, manage who on your account can use it, and track its review status.

Requests go through admin review

A new sender name is created with status requested. An admin approves or declines it, you can't approve your own. Poll GET /sender-names/{id} or subscribe to the sender name status webhook to know when a decision is made.

Request a Sender Name

POST /sender-names
FieldTypeRequiredDescription
name string required The sender name to request. Max 11 characters.
reason string required Why you're requesting it, shown to the reviewing admin.
is_default boolean optional Request this as your account's default sender, used when a Send SMS call omits sender_name.
curl -X POST https://portal.textify.africa/api/v1/sender-names \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "MYBRAND",
    "reason": "Marketing campaigns for my store"
  }'
You don't pick the sending gateway

Which SMS provider actually delivers messages under this name is decided by Textify when the request is reviewed, not by you at request time — there's no provider field to set, and it isn't included in the API response.

Errors

StatuserrorMeaning
400 validation_error name or reason was missing, or name is longer than 11 characters.
400 creation_error That name is already taken by another sender name.
401 unauthorized / invalid_token Missing or invalid API key.

List Sender Names

GET /sender-names

Returns sender names you created, are assigned to, or that are the shared default, plus your customers' if you're a reseller.

FieldTypeRequiredDescription
status string optional requested, approved, or declined.
page / limit / sort_by / sort_dir — optional Standard pagination; see Request & Response Format.
curl "https://portal.textify.africa/api/v1/sender-names?status=approved" \
  -H "Authorization: Bearer $API_KEY"

GET /sender-names/search?search=… accepts the same filters plus a search term matched against name and status. GET /sender-names/{id} fetches one by id.

Sender name object

Every response includes these fields, regardless of whose sender name it is:

FieldTypeRequiredDescription
id uuid required Sender name id.
name string required The sender name text, as it appears to recipients.
status string required requested, approved, or declined.
is_default boolean required Whether this is the account's default sender.
is_disabled boolean required Set by an admin independently of status, an approved name can still be disabled.
created_at string required ISO 8601 creation timestamp.
Extra fields on your own sender names

A sender name you don't own — visible only because it's a shared default — comes back with just the fields above. One you created, or one belonging to your own reseller customers, also includes the fields below.

FieldTypeRequiredDescription
created_by / created_by_name uuid / string optional Who requested it.
updated_by / updated_by_name uuid | null / string | null optional The admin who last approved/declined/updated it, if any.
updated_at string | null optional ISO 8601 timestamp of the last update.
reseller_id uuid | null optional Set when it belongs to one of your customers, if you're a reseller.
users array of uuids optional Which of your account's users are allowed to send with it.
api_key_id uuid | null optional The API key that created it, if it was created via the API rather than the portal.

Update a Sender Name

PUT /sender-names/{id}

You can update a sender name you own; a reseller can also update their customers'.

FieldTypeRequiredDescription
name string required Max 11 characters.
is_default boolean optional Whether this should be the account's default sender.
curl -X PUT https://portal.textify.africa/api/v1/sender-names/9a1c1e2a-4b0e-4e9a-9a2e-1a2b3c4d5e6f \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "MYBRAND", "is_default": true }'

Assign Users

PATCH /sender-names/{id}/assign

Replaces the full list of user ids allowed to send with this sender name, send the complete list each time, not just additions.

curl -X PATCH https://portal.textify.africa/api/v1/sender-names/9a1c1e2a-4b0e-4e9a-9a2e-1a2b3c4d5e6f/assign \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "users": ["c62f7e2a-...", "d81a9b3c-..."] }'

Delete a Sender Name

DELETE /sender-names/{id}
curl -X DELETE https://portal.textify.africa/api/v1/sender-names/9a1c1e2a-4b0e-4e9a-9a2e-1a2b3c4d5e6f \
  -H "Authorization: Bearer $API_KEY"

DELETE /sender-names/bulk takes { "ids": ["…", "…"] } to delete several at once.

Support

Stuck on an integration, or waiting on a sender name review? Our engineers answer directly.