On-demand premium SMS lets you charge a subscriber for content they ask for — quizzes, sports scores, news, horoscopes. The subscriber texts your keyword to your premium shortcode, you reply with the content, and the charge is taken from their airtime when your reply is delivered.

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 flow works

  1. The subscriber texts your keyword (e.g. QUIZ) to your premium shortcode.
  2. Sozuri sends a request webhook ("type": "linkNotification") to your premium service’s callback URL, carrying a linkId for this request.
  3. You reply through the API, quoting that linkId — it is what authorises the charge.
  4. Sozuri sends a delivery webhook ("type": "premiumDelivery") when the network reports whether your reply reached the subscriber.
On-demand premium SMS needs an approved premium service: a premium shortcode, plus the keyword and price agreed with the network. Request one in your project from Premium SMS › On-demand inbox › Request premium service; we handle the network approval. Premium SMS runs on Safaricom: the subscriber is charged on their Safaricom airtime.
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. An apiKey header is not read. The endpoint 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

Request parameters

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 — normally the number from the request webhook. 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 content you are charging for. The subscriber is charged per 160-character part.
linkIdYesStringThe linkId from the request webhook. It must belong to a request we received for this service on your project. Without it the reply is refused and nothing is sent.
keywordRecommendedStringYour 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 (e.g. 20000). Used to find the service when keyword and premium_id are not sent. When the shortcode carries several services, a request that includes a linkId goes to its on-demand service.
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. One request sends one message to one subscriber.

Response parameters

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[].typeStringondemand.
recipients[].linkIdStringThe linkId this reply was sent against.

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": "Your premium content.",
    "linkId": "00000000000000000000000001",
    "keyword": "QUIZ",
    "from": "20000"
}
<?php

$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL => "https://sozuri.net/api/v1/messaging/sendpremium",
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_POSTFIELDS => json_encode([
        "project" => "my project",
        "number"  => "254700000001",
        "message" => "Your premium content.",
        "linkId"  => "00000000000000000000000001",
        "keyword" => "QUIZ",
        "from"    => "20000",
    ]),
    CURLOPT_HTTPHEADER => [
        "Accept: application/json",
        "Authorization: Bearer YOUR_API_KEY",
        "Content-Type: application/json",
    ],
]);
echo curl_exec($curl);
curl_close($curl);
const response = await fetch("https://sozuri.net/api/v1/messaging/sendpremium", {
    method: "POST",
    headers: {
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
        "Accept": "application/json"
    },
    body: JSON.stringify({
        project: "my project",
        number: "254700000001",
        message: "Your premium content.",
        linkId: "00000000000000000000000001",
        keyword: "QUIZ",
        from: "20000"
    })
});
console.log(await response.json());
require 'uri'
require 'net/http'
require 'json'

uri  = URI("https://sozuri.net/api/v1/messaging/sendpremium")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Content-Type"]  = "application/json"
request["Accept"]        = "application/json"
request.body = {
    project: "my project", number: "254700000001", message: "Your premium content.",
    linkId: "00000000000000000000000001", keyword: "QUIZ", from: "20000"
}.to_json

puts http.request(request).body
import requests

response = requests.post(
    "https://sozuri.net/api/v1/messaging/sendpremium",
    headers={
        "Authorization": "Bearer YOUR_API_KEY",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    json={
        "project": "my project", "number": "254700000001", "message": "Your premium content.",
        "linkId": "00000000000000000000000001", "keyword": "QUIZ", "from": "20000",
    },
)
print(response.json())
HttpResponse<String> response = Unirest.post("https://sozuri.net/api/v1/messaging/sendpremium")
    .header("Authorization", "Bearer YOUR_API_KEY")
    .header("Content-Type",  "application/json")
    .header("Accept",        "application/json")
    .body("{\"project\":\"my project\",\"number\":\"254700000001\","
        + "\"message\":\"Your premium content.\",\"linkId\":\"00000000000000000000000001\","
        + "\"keyword\":\"QUIZ\",\"from\":\"20000\"}")
    .asString();
var client  = new RestClient("https://sozuri.net/api/v1/messaging/sendpremium");
var request = new RestRequest(Method.POST);
request.AddHeader("Accept",        "application/json");
request.AddHeader("Content-Type",  "application/json");
request.AddHeader("Authorization", "Bearer YOUR_API_KEY");
request.AddParameter("application/json",
    "{\"project\":\"my project\",\"number\":\"254700000001\","
  + "\"message\":\"Your premium content.\",\"linkId\":\"00000000000000000000000001\","
  + "\"keyword\":\"QUIZ\",\"from\":\"20000\"}",
    ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
curl -X POST https://sozuri.net/api/v1/messaging/sendpremium \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'Authorization: Bearer YOUR_API_KEY' \
    -d '{
        "project": "my project",
        "number": "254700000001",
        "message": "Your premium content.",
        "linkId": "00000000000000000000000001",
        "keyword": "QUIZ",
        "from": "20000"
    }'
Sample JSON response
{
    "messageData": { "messages": 1 },
    "recipients": [
        {
            "messageId": "SOZPREM0123456789ABC",
            "to": "254700000001",
            "status": "accepted",
            "statusCode": "11",
            "messagePart": 1,
            "keyword": "QUIZ",
            "type": "ondemand",
            "linkId": "00000000000000000000000001"
        }
    ]
}

Request webhook (the linkId)

When a subscriber texts your keyword to your premium shortcode, Sozuri POSTs this to the callback URL on your premium service. That URL is shown as Callback on the service’s card on the Premium Services page (Premium SMS › Subscription in your project menu). 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. Capture the linkId: your reply must quote it, and it is the only thing that authorises the charge.

{
    "project": "my project",
    "shortcode": "20000",
    "keyword": "QUIZ",
    "number": "254700000001",
    "messageId": "00000000000000000000000001",
    "network": "safaricom",
    "type": "linkNotification",
    "linkId": "00000000000000000000000001",
    "message": "QUIZ",
    "status": "QUIZ",
    "timestamp": 1762560000,
    "authKey": "szk_YOUR_AUTH_KEY"
}
FieldDescription
keywordYour service’s activation keyword — not necessarily the exact text the subscriber typed.
numberThe subscriber, as the network reports them (2547…). Send it back as number.
linkId, messageIdThe same value: the id of this request. Send it back as linkId.
message, statusBoth carry the text the subscriber sent.
networkAlways safaricom.
timestampUnix time, in seconds, when Sozuri sent the webhook (a number).
authKeyYour project’s Auth Key — see verifying webhooks.
A linkId is short-lived. Reply while the subscriber is still expecting the content. A stale one is refused by the network with The link id has expired or does not exist. Ask the subscriber to text the keyword again. (carrier code …) and nothing is charged. Sozuri does not stop you replying to the same linkId twice; every reply the network accepts is a separate charged message. Every request, its reply, and the requests still unanswered are on Premium SMS › On-demand inbox in your dashboard.

If your premium shortcode is also set up for 2-way SMS on your project, the subscriber’s text also reaches your inbound-messages callback. That copy has no linkId, so reply using the request webhook above.

Delivery status webhook

Sent to the same callback URL, once for each delivery report the network sends for one of your replies:

{
    "project": "my project",
    "shortcode": "20000",
    "keyword": "QUIZ",
    "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, so the two join directly. status is the network’s own delivery description, passed through as written: DeliveryToTerminal means the reply was delivered; a description that mentions an absent subscriber, expiry or delivery being impossible means it was not. Your dashboard shows each reply as Accepted, Sent, Delivered, Undelivered or Failed; a report we do not recognise leaves it at Sent rather than counting it as delivered.

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 send answers HTTP 200 with { "status": "error", "description": "…" }, so check status, not the HTTP code. Nothing is sent or charged.

descriptionWhat it means
Unknown or Inactive Premium serviceNo active premium service on your project matches the premium_id, keyword or from you sent.
Unsupported Numbernumber cannot be read as a Kenyan mobile number.
linkId is required for an on-demand premium messageThe service is on-demand and no linkId was sent.
Unknown link idNo request with that linkId was received for this service on your project.
The link id has expired or does not exist. Ask the subscriber to text the keyword again. (carrier code …)The network refused the reply because the request is no longer open.
The subscriber does not have enough airtime to be charged. (carrier code …)Nothing was charged and nothing was delivered.
This shortcode is blacklisted by the carrier. (carrier code …)Contact support — no message on this shortcode will be accepted.
Any other text, ending (carrier code …)The network refused the message; the text is the network’s own explanation.
The network could not be reached. Nothing was sent; try again shortly.A connection fault on the way to the network. Safe to retry.
Premium sending is not available right now. Nothing was sent; contact support.Sending is switched off on our side; retrying will not help.
ConditionHTTPResponse body
Any refusal in the table above. 200
{
    "status": "error",
    "description": "Unknown link id"
}
number or message is missing or empty. 422
{
    "error_code": "VALIDATION_FAILED",
    "message": "The submitted data failed validation.",
    "retryable": false,
    "errors": { "number": ["The number 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

Where on-demand premium SMS earns its keep.

Sports scores & alerts

Subscriber texts GOAL to get the latest score on demand — charge per request, no app required.

⚽
20000
SportsZone
GOAL 20:14
Man City 2-1 Arsenal (78'). Haaland scored from a penalty. Send GOAL for next update. 20:14

News & horoscopes

Daily snippets of paid content on demand — the foundation of the original mobile content businesses.

✨
20001
Daily Horoscope
LEO 07:01
Leo: Today brings unexpected good fortune in work matters. Trust your instincts. 07:01

Quizzes & trivia

Pay-per-question trivia campaigns on radio or TV — the keyword becomes the call-to-action.

🎯
20002
Drive-Time Trivia
Q3: Which country won the 2024 AFCON?
A - Nigeria
B - Cote d’Ivoire
C - South Africa 08:42
B 08:43
Correct! You’ve banked KES 200 in airtime. Next question at 09:00. Keep listening! 08:43

Monetise your content via SMS.

Apply for a premium shortcode and we&rsquo;ll handle billing, settlement and carrier paperwork end-to-end.