Subscription premium SMS
Subscription premium SMS lets you send paid content to people who have opted in to your service — daily tips, devotionals, market prices — on a daily, weekly or monthly cadence. Each message is charged to the subscriber’s airtime when it is delivered, until they opt out.
- How the subscription flow works
- Send a premium message: request
- Send a premium message: response
- Sample send-message request
- Add or remove a subscriber
- Let Sozuri send on a schedule
- Subscriber webhooks (activation, deactivation, consent, delivery)
- Error responses
How the subscription flow works
- A subscriber opts in by texting your activation keyword (e.g.
JOIN) to your shortcode — or you ask for them with the subscribe endpoint, and the network asks them to confirm on their phone. - Sozuri sends an
activationwebhook when the network confirms the subscription (and aconsentwebhook with their answer, when they were asked to confirm). From then on the number is an active subscriber. - You send premium content through the API, or let Sozuri send it on a schedule. Only active subscribers can be sent to, and each message is charged.
- Delivery reports and opt-outs arrive as webhooks.
Send a premium message: request parameters
Authentication and headers
Send your project API key as a Bearer token — or as an apiKey field in the body or
query string — and your project’s name in the project field. The key alone
does not authenticate, and an apiKey header is not read. Every endpoint on this page accepts
POST (JSON or form body) and GET (query string).
POST /api/v1/messaging/sendpremium HTTP/1.1
Host: sozuri.net
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: application/json
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | Your project’s name, exactly as it appears in your dashboard. It is half of the credential. |
| apiKey | If no Bearer token | String | Your project API key, as a body or query-string field. Prefer the Authorization: Bearer header. |
| number | Yes | String | The subscriber’s number. It must be an active subscriber of this service. 0700 000 001, +254700000001 and 700000001 all become 254700000001; a number that cannot be read as a Kenyan mobile number is refused with Unsupported Number. |
| message | Yes | String | The premium content to send. The subscriber is charged per 160-character part. |
| keyword | Recommended | String | The service’s activation keyword, as shown on the Premium Services page. It picks the service, which matters when one shortcode carries more than one. If it matches no active service, from is tried. |
| from | If no keyword | String | Your premium shortcode. Used to find the service when keyword and premium_id are not sent. |
| premium_id | No | Number | The service’s number, shown as #n on its card on the Premium Services page. When sent, it is used instead of keyword and from. |
| channel, type | No | String | Accepted and ignored. Kept for older integrations; the product type comes from the service itself. |
Send at least one of premium_id, keyword or from; they are tried in that
order. Do not send a linkId on a subscription message — on a shortcode that also carries an
on-demand service, a linkId selects the on-demand one. One request sends one message to one subscriber.
Send a premium message: response
The request is answered once the network has accepted or refused the message, so allow a few seconds. An accepted message returns HTTP 200 with:
| Field | Type | Description |
|---|---|---|
| messageData.messages | Number | Always 1. |
| recipients[].messageId | String | Sozuri’s id for this message. The delivery webhook reports the same id, so store it to join the two. |
| recipients[].to | String | The subscriber’s number, normalised to 254…. |
| recipients[].status | String | accepted: the network took the message. Not a delivery status — that arrives on the delivery webhook. |
| recipients[].statusCode | String | 11 — see status codes. |
| recipients[].messagePart | Number | The number of 160-character parts the subscriber is charged for. |
| recipients[].keyword | String | The service’s activation keyword. |
| recipients[].type | String | subscription. |
| recipients[].linkId | null | Always null for a subscription message. |
Sample request
POST /api/v1/messaging/sendpremium HTTP/1.1
Host: sozuri.net
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Accept: application/json
{
"project": "my project",
"number": "254700000001",
"message": "Today's quote: The best way out is always through. - Robert Frost.",
"keyword": "JOIN",
"from": "20000"
}
Sample JSON response
{
"messageData": { "messages": 1 },
"recipients": [
{
"messageId": "SOZPREM0123456789ABC",
"to": "254700000001",
"status": "accepted",
"statusCode": "11",
"messagePart": 1,
"keyword": "JOIN",
"type": "subscription",
"linkId": null
}
]
}
Add or remove a subscriber
Subscribers normally opt in themselves by texting your activation keyword to your shortcode. You can also
request a subscription yourself — for example when somebody opts in on a web form — using the
subscribe endpoint. These are separate endpoints from sending, not a
type on the send call.
| Purpose | Endpoint | Required fields |
|---|---|---|
| Opt a number in | POST /api/v1/messaging/subscribe | project, number, keyword |
| Opt a number out | POST /api/v1/messaging/unsubscribe | project, number, keyword |
| Send to a subscriber | POST /api/v1/messaging/sendpremium | project, number, message, and keyword, from or premium_id |
Subscribe and unsubscribe fields
| Field | Required | Type | Description |
|---|---|---|---|
| project | Yes | String | Your project’s name, as above. |
| apiKey | If no Bearer token | String | As above. |
| number | Yes | String | The subscriber’s number, normalised as above. |
| keyword | Yes | String | The service’s activation keyword, exactly as shown on the Premium Services page. It is the only way these two endpoints choose the service; from is not read. |
| name | No | String | Subscribe only. Stored with the subscriber (up to 120 characters) and used by the {name} placeholder in scheduled messages. |
Subscribe
curl -X POST https://sozuri.net/api/v1/messaging/subscribe \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{
"project": "my project",
"keyword": "JOIN",
"number": "0700000001",
"name": "Jane Doe"
}'
{
"project": "my project",
"shortcode": "20000",
"keyword": "JOIN",
"number": "254700000001",
"network": "safaricom",
"type": "subscribe",
"status": "pending",
"statusCode": "112",
"description": "Your request for the service is under process."
}
statusCode and description are the network’s own reference and wording for the
request. Keep them for your records; act on the webhooks, not on these.
status is pending, not active, and that is not a delay in our
system. The network owns consent: it asks the subscriber to confirm on their phone, and the
subscription only exists once they accept. Their answer reaches you as a consent
webhook, and the subscription going live as an activation webhook
— those are the events to act on. Until then the number shows as Pending on the
Premium Services page, and a send to it is refused with
This number is not an active subscriber of this service. Calling subscribe again for a pending
number asks the network again; for an active one it is refused with Subscriber already Exists.
Unsubscribe
curl -X POST https://sozuri.net/api/v1/messaging/unsubscribe \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{
"project": "my project",
"keyword": "JOIN",
"number": "0700000001"
}'
{
"project": "my project",
"shortcode": "20000",
"keyword": "JOIN",
"number": "254700000001",
"network": "safaricom",
"type": "unsubscribe",
"status": "inactive",
"statusCode": "1",
"description": "Subscription cancelled"
}
As soon as the network accepts the cancellation, the number is inactive in Sozuri and can no longer be sent to.
If the network refuses it, the number stays active and you get the refusal back. A number that is still
Pending is not active yet, so unsubscribing it answers Subscriber is already Inactive.
A subscriber can also opt out by texting your opt-out keyword (usually STOP) to your shortcode, and
the network can deactivate them — for example when an offer is withdrawn. Both produce a
deactivation webhook.
Let Sozuri send on a schedule
You do not need your own cron job or content system. On the Premium Services page, a subscription service has a Sending schedule panel where you set:
- A cadence — every day at 07:00, weekdays, Mondays, the 1st of the month, or any five-field cron expression, in the timezone you choose (Nairobi by default).
- What gets sent — either one fixed message, or a list with one message per line. A list is sent in order, one line per run, starting again from the top after the last line. The message box holds up to 480 characters in total, list included.
- The price per 160-character part, charged to the subscriber.
Placeholders {name}, {msisdn}, {date}, {time},
{offer} and {shortcode} are filled in per recipient; {name} becomes
“there” when we have no name for the subscriber. Every message sent appears in the
Message log on the same page, grouped by a run id, and each raises a delivery webhook exactly as an
API send does.
Subscriber webhooks
You receive four kinds of webhook, all POSTed to the callback URL on your premium service. That URL is shown as Callback on the service’s card on the Premium Services page. We set it when the service is provisioned — send it to support to set or change it. It is not one of the URLs under Manage API › Callback URLs, which are for ordinary SMS.
1. Activation
Sent when the network reports the subscription is live — whether the subscriber texted your keyword or confirmed after a subscribe call.
{
"project": "my project",
"shortcode": "20000",
"keyword": "JOIN",
"number": "254700000001",
"network": "safaricom",
"type": "activation",
"status": "active",
"authKey": "szk_YOUR_AUTH_KEY"
}
2. Deactivation
Sent when the network reports an opt-out — the subscriber texted your opt-out keyword, or the network deactivated them.
{
"project": "my project",
"shortcode": "20000",
"keyword": "JOIN",
"number": "254700000001",
"network": "safaricom",
"type": "deactivation",
"status": "inactive",
"authKey": "szk_YOUR_AUTH_KEY"
}
3. Consent
Sent when the subscriber answers the network’s confirmation prompt — typically after you called subscribe. status is active if they accepted and inactive if they declined.
{
"project": "my project",
"shortcode": "20000",
"keyword": "JOIN",
"number": "254700000001",
"network": "safaricom",
"type": "consent",
"status": "inactive",
"authKey": "szk_YOUR_AUTH_KEY"
}
4. Delivery status
Sent once for each delivery report the network sends for one of your premium messages.
{
"project": "my project",
"shortcode": "20000",
"keyword": "JOIN",
"number": "254700000001",
"messageId": "SOZPREM0123456789ABC",
"network": "safaricom",
"type": "premiumDelivery",
"status": "DeliveryToTerminal",
"timestamp": 1762560000,
"authKey": "szk_YOUR_AUTH_KEY"
}
messageId is the recipients[].messageId the send returned. status is the
network’s own delivery description, passed through as written: DeliveryToTerminal means
delivered; a description that mentions an absent subscriber, expiry or delivery being impossible means it was not.
timestamp is the Unix time, in seconds, when Sozuri sent the webhook.
| Field | Description |
|---|---|
| keyword | Your service’s activation keyword — not necessarily the exact text the subscriber typed. |
| number | The subscriber, as the network reports them (2547…). |
| network | Always safaricom. |
| timestamp | Delivery webhooks only. Activation, deactivation and consent webhooks carry no timestamp. |
How webhooks are sent and verified
POST https://your-app.example/sozuri/premium
Content-Type: application/json
Accept: application/json
X-Sozuri-Signature: t=1762560000,v1=<hex hmac_sha256(signing_secret, "1762560000." + raw_body)>
- Authentication. The body ends with your project’s
authKey, and theX-Sozuri-Signatureheader signs the exact raw body. Both come from Manage API › Callback URLs › Webhook security, which appears once your project has a callback configuration. Without one, premium webhooks are sent with neither. See Verifying authenticity. - Several URLs. The callback may be a comma-separated list; each URL gets its own copy.
- Enforce HTTPS. If it is ticked on your callback configuration, a plain
http://callback is not called at all. - One attempt. Each webhook is sent once. We wait up to 8 seconds, do not follow redirects and do not retry, so answer quickly with a
2xx.
Error responses
A refused request answers HTTP 200 with { "status": "error", "description": "…" },
so check status, not the HTTP code.
| description | Endpoint | What it means |
|---|---|---|
Unknown, Expired, or Inactive Premium Service | subscribe, unsubscribe | keyword does not match an active premium service on your project. |
Unknown or Inactive Premium service | sendpremium | No active premium service on your project matches the premium_id, keyword or from you sent. |
Unsupported Number | all | number cannot be read as a Kenyan mobile number. |
Subscriber already Exists | subscribe | The number is already an active subscriber of this service. |
Unknown Subscriber | unsubscribe | The number was never subscribed to this service. |
Subscriber is already Inactive | unsubscribe | The number is already inactive, or still pending confirmation. |
This number is not an active subscriber of this service | sendpremium | Only active subscribers can be sent to. Nothing is sent or charged. |
A sentence ending (carrier code …) | all | The network refused the request; the text explains why (for example The subscriber does not have enough airtime to be charged. (carrier code …) or This number is already subscribed. (carrier code …)). |
The network could not be reached. Nothing was sent; try again shortly. | all | A connection fault on the way to the network. Safe to retry. |
Premium sending is not available right now. Nothing was sent; contact support. | all | Sending is switched off on our side; retrying will not help. |
| Condition | HTTP | Response body |
|---|---|---|
| Any refusal in the table above. | 200 | |
A required field (number, keyword on subscribe/unsubscribe, message on send) is missing or empty. |
422 | |
The API key is missing or wrong, or project does not match it. |
401 | |
Use cases
The recurring SMS services that thrive on premium subscriptions across Africa.
Daily content (quotes, devotionals, jokes)
Subscriber pays a daily fee to receive one short message a day — the original mobile content business model, still huge.
Health & wellness tips
Weekly maternal-health, HIV-adherence or chronic-care reminders — works on every phone, no data needed.
Agronomy & market prices
Daily commodity prices for farmers, weather alerts, planting tips — high-value content for rural subscribers.
Maize KES 4,200 / 90kg bag
Beans KES 9,800 / 90kg
Milk KES 55 / litre
Rain expected in Eldoret tomorrow. 06:00
Sports & betting tips
Tipsters and sports networks monetise daily picks via opt-in subscriptions.
Launch a recurring SMS service.
We’ll set up the shortcode, register your keyword and handle billing reconciliation with the carrier.