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.

POST https://sozuri.net/api/v1/messaging/sendpremium
What you earn, and what it costs to start: shortcode setup and monthly fees, the volume-tiered revenue share, and a calculator that shows your net per message — see Premium SMS pricing. The same rates apply to both on-demand and subscription messages.

How the subscription flow works

  1. 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.
  2. Sozuri sends an activation webhook when the network confirms the subscription (and a consent webhook with their answer, when they were asked to confirm). From then on the number is an active subscriber.
  3. 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.
  4. Delivery reports and opt-outs arrive as webhooks.
Subscription premium SMS needs an approved premium service: a premium shortcode, plus the keywords and price agreed with the network. Request one in your project from Premium SMS › Subscription › Request Premium Service — our team handles the network paperwork. Premium SMS runs on Safaricom: subscribers are charged on their Safaricom airtime.

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
FieldRequiredTypeDescription
projectYesStringYour project’s name, exactly as it appears in your dashboard. It is half of the credential.
apiKeyIf no Bearer tokenStringYour project API key, as a body or query-string field. Prefer the Authorization: Bearer header.
numberYesStringThe 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.
messageYesStringThe premium content to send. The subscriber is charged per 160-character part.
keywordRecommendedStringThe 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.
fromIf no keywordStringYour premium shortcode. Used to find the service when keyword and premium_id are not sent.
premium_idNoNumberThe 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, typeNoStringAccepted 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:

FieldTypeDescription
messageData.messagesNumberAlways 1.
recipients[].messageIdStringSozuri’s id for this message. The delivery webhook reports the same id, so store it to join the two.
recipients[].toStringThe subscriber’s number, normalised to 254….
recipients[].statusStringaccepted: the network took the message. Not a delivery status — that arrives on the delivery webhook.
recipients[].statusCodeString11 — see status codes.
recipients[].messagePartNumberThe number of 160-character parts the subscriber is charged for.
recipients[].keywordStringThe service’s activation keyword.
recipients[].typeStringsubscription.
recipients[].linkIdnullAlways 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.

PurposeEndpointRequired 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
FieldRequiredTypeDescription
projectYesStringYour project’s name, as above.
apiKeyIf no Bearer tokenStringAs above.
numberYesStringThe subscriber’s number, normalised as above.
keywordYesStringThe 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.
nameNoStringSubscribe 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.

Networks require explicit consent before you subscribe somebody on their behalf. Keep proof of consent (form submission, voice call recording, etc.) — this is what networks audit when disputes arise.

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.

Nothing is sent until the service is approved and you switch the schedule on. Scheduled sending must also be switched on by Sozuri — if the panel says it is switched off, contact support. A missed slot is not caught up later: a reverse-billed “good morning” that arrives at 11:00 is a refund request. Message length is a price: a message is charged per 160-character part, so a 200-character tip costs your subscriber two parts.

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"
}

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.

FieldDescription
keywordYour service’s activation keyword — not necessarily the exact text the subscriber typed.
numberThe subscriber, as the network reports them (2547…).
networkAlways safaricom.
timestampDelivery 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 the X-Sozuri-Signature header 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.

descriptionEndpointWhat it means
Unknown, Expired, or Inactive Premium Servicesubscribe, unsubscribekeyword does not match an active premium service on your project.
Unknown or Inactive Premium servicesendpremiumNo active premium service on your project matches the premium_id, keyword or from you sent.
Unsupported Numberallnumber cannot be read as a Kenyan mobile number.
Subscriber already ExistssubscribeThe number is already an active subscriber of this service.
Unknown SubscriberunsubscribeThe number was never subscribed to this service.
Subscriber is already InactiveunsubscribeThe number is already inactive, or still pending confirmation.
This number is not an active subscriber of this servicesendpremiumOnly active subscribers can be sent to. Nothing is sent or charged.
A sentence ending (carrier code …)allThe 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.allA connection fault on the way to the network. Safe to retry.
Premium sending is not available right now. Nothing was sent; contact support.allSending is switched off on our side; retrying will not help.
ConditionHTTPResponse body
Any refusal in the table above. 200
{
    "status": "error",
    "description": "Subscriber already Exists"
}
A required field (number, keyword on subscribe/unsubscribe, message on send) is missing or empty. 422
{
    "error_code": "VALIDATION_FAILED",
    "message": "The submitted data failed validation.",
    "retryable": false,
    "errors": { "keyword": ["The keyword field is required."] }
}
The API key is missing or wrong, or project does not match it. 401
{
    "error_code": "AUTHENTICATION_FAILED",
    "message": "Authentication failed. Check your API key or bearer token.",
    "retryable": false
}

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.

๐Ÿ“–
20000
Daily Quote
“The best way out is always through.” โ€” Robert Frost. Send STOP to unsubscribe. 07:00

Health & wellness tips

Weekly maternal-health, HIV-adherence or chronic-care reminders — works on every phone, no data needed.

๐Ÿ’Š
20001
AfyaTips
AfyaTip: Week 24. Your baby is now about 30 cm long. Visit the clinic for your second trimester check-up this week. 08:00

Agronomy & market prices

Daily commodity prices for farmers, weather alerts, planting tips — high-value content for rural subscribers.

๐ŸŒพ
20002
ShambaPrices
Today’s Nairobi prices:
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&rsquo;ll set up the shortcode, register your keyword and handle billing reconciliation with the carrier.