On-demand premium SMS
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.
- How the flow works
- Request parameters
- Response parameters
- Sample request
- Request webhook (the
linkId) - Delivery status webhook
- How webhooks are sent and verified
- Error responses
How the flow works
- The subscriber texts your keyword (e.g.
QUIZ) to your premium shortcode. - Sozuri sends a request webhook (
"type": "linkNotification") to your premium service’s callback URL, carrying alinkIdfor this request. - You reply through the API, quoting that
linkId— it is what authorises the charge. - Sozuri sends a delivery webhook (
"type": "premiumDelivery") when the network reports whether your reply reached the subscriber.
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
| 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 — 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. |
| message | Yes | String | The content you are charging for. The subscriber is charged per 160-character part. |
| linkId | Yes | String | The 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. |
| keyword | Recommended | String | Your 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 (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_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.
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:
| 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 | ondemand. |
| recipients[].linkId | String | The 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"
}
| 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…). Send it back as number. |
| linkId, messageId | The same value: the id of this request. Send it back as linkId. |
| message, status | Both carry the text the subscriber sent. |
| network | Always safaricom. |
| timestamp | Unix time, in seconds, when Sozuri sent the webhook (a number). |
| authKey | Your project’s Auth Key — see verifying webhooks. |
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 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 send answers HTTP 200 with { "status": "error", "description": "…" },
so check status, not the HTTP code. Nothing is sent or charged.
| description | What it means |
|---|---|
Unknown or Inactive Premium service | No active premium service on your project matches the premium_id, keyword or from you sent. |
Unsupported Number | number cannot be read as a Kenyan mobile number. |
linkId is required for an on-demand premium message | The service is on-demand and no linkId was sent. |
Unknown link id | No 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. |
| Condition | HTTP | Response body |
|---|---|---|
| Any refusal in the table above. | 200 | |
number or message is missing or empty. |
422 | |
The API key is missing or wrong, or project does not match it. |
401 | |
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.
News & horoscopes
Daily snippets of paid content on demand — the foundation of the original mobile content businesses.
Quizzes & trivia
Pay-per-question trivia campaigns on radio or TV — the keyword becomes the call-to-action.
A - Nigeria
B - Cote d’Ivoire
C - South Africa 08:42
Monetise your content via SMS.
Apply for a premium shortcode and we’ll handle billing, settlement and carrier paperwork end-to-end.