POST https://sozuri.net/api/v1/zuka/push
DISCLAIMER: Sozuri does not aggregate or collect your payments on your behalf. We provide you a plug-and-play solution for your paybill payment notifications so that you don’t have to build one. All M-Pesa funds belong to you and are deposited to the Paybill owner, not Sozuri.

Sozuri Payments lets a Paybill or Till holder collect M-Pesa payments by STK Push, using their own Daraja credentials. Plug the credentials in once, and Sozuri:

  • Sends an M-Pesa payment prompt to your customer’s phone — from your own system through the API, or from a hosted payment link (with a QR code) that anyone can open.
  • For every confirmed payment, sends an SMS from your Sender ID to the customer who paid and to every number in Staff to Notify. Each SMS ends with a shared five-digit Zuka OTP — the cashier can ask the customer for it to confirm they really paid, which stops fake-screenshot fraud. These are ordinary SMS sent from your project.
  • Forwards every confirmed payment to your webhook URL.

Setup — Bring Your Own Shortcode

Register your M-Pesa credentials in your project’s Payments settings. You’ll need a Daraja API application from developer.safaricom.co.ke and an M-Pesa admin account at org.ke.m-pesa.com. To set these up, follow the steps below:

  1. Apply for a Till/Paybill number by visiting a Safaricom shop or via m-pesaforbusiness.co.ke. (For assistance, the M-PESA Business Team contacts are: 0722 002 222, e-mail: [email protected].)
  2. Once approved, you’ll receive:
    • Your Till/Paybill number.
    • Access to the M-PESA Business admin portal (org.ke.m-pesa.com) — if this is missing, contact the team above.
  3. Create an assistant role user in the Business admin portal for Daraja verification.
  4. Create a developer account at the Daraja portal: developer.safaricom.co.ke.
  5. Create a sandbox app and select the “Lipa Na M-Pesa Sandbox and Paybill Sandbox” API.
  6. Get your Consumer Key and Consumer Secret.
  7. Click “Go Live” and select verification by shortcode.
  8. Enter the assistant username (created above) and select your desired API products.
  9. You’ll receive production credentials and a passkey on the Daraja portal or via email.

Now you have your Paybill/Till No., Consumer Key, Consumer Secret, and Passkey. Open your project’s Payments menu, click Configure in the top right, and fill in these fields:

FieldRequiredDescription
Business NameYesShown at the top of your payment link page.
TypeYesPaybill or Till.
Shortcode / Till No.YesYour M-Pesa paybill or till number.
Consumer KeyFor STK PushFrom your Daraja API application.
Consumer SecretFor STK PushFrom your Daraja API application.
PasskeyFor STK PushFrom your Daraja STK Push setup. Without the key, secret and passkey, neither the API nor the payment link can send a prompt.
Transaction TypeNoPaybill (C2B) (the default) or Buy Goods (Till) — the M-Pesa transaction type used for every prompt.
Sender IDFor SMSThe sender ID the payment SMS are sent from. The SMS go out as promotional messages, so this must be one of your project’s promotional sender IDs, registered on the network of each number it goes to.
Staff to NotifyNoComma-separated phone numbers (cashier, manager) that receive the payment SMS.
Message TemplateNoThe text of the payment SMS. The placeholders you can use are listed in the dialog.
Webhook URLNoWhere Sozuri posts each confirmed payment. Separate several URLs with commas and no spaces; each receives the webhook.

Once saved, the dialog also shows a Confirmation URL. Register it with M-Pesa as your paybill’s confirmation URL if you want payments that customers make straight to your paybill (not through a prompt) to produce the same SMS and webhook. You do not need to register the STK callback URL: Sozuri sends it with every prompt.

1. Initiate STK Push

This sends an M-Pesa payment prompt to your customer’s phone. When they enter their M-Pesa PIN and the payment succeeds, Sozuri records the transaction, sends the payment SMS and posts the confirmation to your webhook. A successful response here only means the prompt was sent — the outcome arrives later, through the webhook or the status check.

Authenticate with your project’s API key as a Bearer token (or an apiKey body field — an apiKey header is not read) and send your project’s exact name in project. See Authentication.

Request parameters
FieldRequiredTypeDescription
projectYesStringYour project’s exact name.
phoneYesStringThe customer’s Kenyan M-Pesa number, sent as a JSON string. Sozuri keeps the last nine digits and puts 254 in front, so 07xx, 2547xx and +254 7xx all work.
amountYesIntegerWhole Kenya shillings, minimum 1. Decimals are refused.
account_referenceNoStringUp to 12 characters — an invoice number, student ID, anything you want the customer to see on the prompt. It comes back as BillRefNumber in the webhook, so make it unique per payment. If you leave it out, Sozuri uses your project’s code (the last part of your payment link), which is longer than 12 characters — send your own.
Sample request
POST /api/v1/zuka/push HTTP/1.1
Host: sozuri.net
Authorization: Bearer YOUR_PROJECT_API_KEY
Content-Type: application/json
Accept: application/json

{
    "project": "Your_Project_Name",
    "phone": "0700000000",
    "amount": 500,
    "account_reference": "INV-001"
}
<?php

$curl = curl_init();
curl_setopt_array($curl, [
    CURLOPT_URL            => 'https://sozuri.net/api/v1/zuka/push',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode([
        'project'           => 'Your_Project_Name',
        'phone'             => '0700000000',
        'amount'            => 500,
        'account_reference' => 'INV-001',
    ]),
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer YOUR_PROJECT_API_KEY',
        'Content-Type: application/json',
        'Accept: application/json',
    ],
]);

$response = curl_exec($curl);
curl_close($curl);

$result = json_decode($response, true);
echo $result['checkout_request_id'];
const response = await fetch('https://sozuri.net/api/v1/zuka/push', {
    method: 'POST',
    headers: {
        'Authorization': 'Bearer YOUR_PROJECT_API_KEY',
        'Content-Type': 'application/json',
        'Accept': 'application/json',
    },
    body: JSON.stringify({
        project: 'Your_Project_Name',
        phone: '0700000000',
        amount: 500,
        account_reference: 'INV-001',
    }),
});

const data = await response.json();
console.log(data.checkout_request_id);
import requests

response = requests.post(
    "https://sozuri.net/api/v1/zuka/push",
    headers={
        "Authorization": "Bearer YOUR_PROJECT_API_KEY",
        "Content-Type": "application/json",
        "Accept": "application/json",
    },
    json={
        "project": "Your_Project_Name",
        "phone": "0700000000",
        "amount": 500,
        "account_reference": "INV-001",
    },
)
print(response.json())
curl --request POST 'https://sozuri.net/api/v1/zuka/push' \
    --header 'Authorization: Bearer YOUR_PROJECT_API_KEY' \
    --header 'Content-Type: application/json' \
    --header 'Accept: application/json' \
    --data '{
        "project": "Your_Project_Name",
        "phone": "0700000000",
        "amount": 500,
        "account_reference": "INV-001"
    }'
Success response (200)
{
    "message": "STK push sent. Prompt your customer to check their phone.",
    "checkout_request_id": "ws_CO_08112026101530123456789",
    "merchant_request_id": "12345-67890123-1"
}

Keep checkout_request_id: it is what you pass to the status check. The webhook does not carry it — match the webhook to your payment by BillRefNumber.

Errors
ConditionHTTPResponse body
Missing or wrong API key or project name. 401
{
    "error_code": "AUTHENTICATION_FAILED",
    "message": "Authentication failed. Check your API key or bearer token.",
    "retryable": false
}
A field is missing or invalid (for example phone sent as a number, amount below 1 or with decimals, account_reference over 12 characters). 422
{
    "error_code": "VALIDATION_FAILED",
    "message": "The submitted data failed validation.",
    "retryable": false,
    "errors": {
        "phone": ["The phone must be a string."],
        "amount": ["The amount must be at least 1."]
    }
}
The project’s payment settings have no Consumer Key, Consumer Secret or Passkey. 422
{
    "error": "No active paybill with STK credentials found for this project."
}
M-Pesa did not accept your Consumer Key and Consumer Secret. 422
{
    "error": "Could not authenticate with M-Pesa. Check paybill credentials."
}
M-Pesa could not be reached. 422
{
    "error": "Network error initiating payment."
}
M-Pesa refused the prompt (wrong passkey, invalid number, and so on). error carries M-Pesa’s own message. 422
{
    "error": "<M-Pesa's error message>"
}

Payment webhook callback

For every confirmed payment, Sozuri POSTs a JSON body to each URL in your Webhook URL setting. That covers prompts sent through the API or your payment link, and — if you registered the Confirmation URL with M-Pesa — payments made straight to your paybill. A payment produces one webhook per URL, even when M-Pesa reports it to Sozuri twice.

  • Only successful payments are sent. A prompt the customer cancels, ignores, or cannot pay sends nothing — use the status check to see a failure.
  • One attempt, no retries. Your response is not read, so any status code is fine, but a URL that is down when the payment arrives misses it. If a URL cannot be connected to at all (unknown host, connection refused), the URLs listed after it miss that payment too, so list your most important URL first.
  • Not signed. The request carries no signature or key. Before releasing high-value goods, confirm the TransID and amount in your M-Pesa statement.
Alongside the webhook, Sozuri sends the payment SMS to the customer who paid and to every number in Staff to Notify, from your Sender ID. Each SMS ends with the same five-digit Zuka OTP — the cashier can ask the customer for it to verify they really paid, preventing fake-screenshot fraud.
Request headers
POST {your webhook URL}
Content-Type: application/json
Accept: application/json
Webhook body — payment from a prompt
{
    "TransactionType": "Pay Bill",
    "TransID": "TKA0000000",
    "TransTime": "20261108101530",
    "TransAmount": "500",
    "BusinessShortCode": "123456",
    "BillRefNumber": "INV-001",
    "InvoiceNumber": "",
    "OrgAccountBalance": "",
    "ThirdPartyTransID": "",
    "MSISDN": "254700000000",
    "FirstName": "",
    "MiddleName": "",
    "LastName": ""
}
FieldTypeDescription
TransactionTypeStringAlways Pay Bill for a prompt payment.
TransIDStringThe M-Pesa receipt number.
TransTimeStringWhen M-Pesa completed the payment, as YYYYMMDDHHmmss (Kenya time).
TransAmountStringThe amount paid, in shillings, as M-Pesa reported it (for example "500").
BusinessShortCodeStringYour paybill or till number from your settings.
BillRefNumberStringThe account_reference of the prompt — your own value, your project’s code if you sent none, or what the customer typed on the payment link (your paybill number if they typed nothing). Use it to match the payment.
OrgAccountBalanceStringYour paybill balance when M-Pesa includes it; usually empty.
MSISDNStringThe number that paid, as 2547XXXXXXXX.
InvoiceNumber, ThirdPartyTransID, FirstName, MiddleName, LastNameStringAlways empty for a prompt payment. Present so the body has the same shape as a direct paybill payment.
Webhook body — payment straight to your paybill

For a payment M-Pesa reports to your Confirmation URL, Sozuri forwards M-Pesa’s own confirmation unchanged. It has the same fields as above, filled with M-Pesa’s values: TransactionType as M-Pesa names it, TransAmount with decimals (for example "500.00"), BillRefNumber as the account number the customer entered, and the payer’s number and names as M-Pesa supplies them. If a prompt payment is also reported this way, you receive whichever of the two reports reaches Sozuri first.

Check payment status

After sending a prompt, you can ask for its outcome with the checkout_request_id from the push response. This is the check your payment link page runs; it needs no API key.

Check every 5 seconds and stop after 2 minutes (24 checks) or when the status is confirmed or fail. Treat the webhook as the record of payment: if your paybill’s own confirmation of a prompt payment reaches Sozuri before the prompt’s result, the payment is recorded and the webhook is sent, but this status stays pending.
Request
GET/zuka/pay/{code}/status/{checkout_request_id}

{code} is your payment-link code — the last part of your payment link.

curl https://sozuri.net/zuka/pay/YOUR_PAYMENT_LINK_CODE/status/ws_CO_08112026101530123456789
StatusMeaning
pendingThe prompt was sent and M-Pesa has not reported a result yet.
confirmedPayment successful — trans_id is the M-Pesa receipt number.
failThe customer cancelled, the prompt timed out, the PIN was wrong or the wallet had insufficient funds.
Responses (always HTTP 200)
// Pending
{ "status": "pending", "trans_id": "", "amount": "500" }

// Confirmed
{ "status": "confirmed", "trans_id": "TKA0000000", "amount": "500" }

// Failed
{ "status": "fail", "trans_id": "", "amount": "500" }

// Unknown checkout_request_id
{ "status": "pending" }

amount is a string of whole shillings. An id Sozuri has no record of also answers pending, with no other fields.

Every project with saved payment settings gets a hosted payment page anyone can open and pay from — no code required. Find it, with a QR code and Copy and Preview buttons, on your project’s Payments page.

https://sozuri.net/zuka/pay/{your-payment-link-code}

The page shows your Business Name and paybill number. The customer enters their M-Pesa number, the amount, and optionally a reference of up to 12 characters (your paybill number is used when they leave it blank), then receives an STK Push. The page checks for the result every 5 seconds for up to 2 minutes and shows confirmed, failed or timed out. A confirmed payment produces the same SMS and webhook as a prompt sent through the API. Share the link over WhatsApp, print the QR code on a receipt, or link to it from your portal.

The page needs your Consumer Key, Consumer Secret and Passkey: without them, sending a prompt from it fails with Payment link is not active.

Use cases

Where teams plug Sozuri Zuka into their day-to-day collections.

E-commerce checkout

Trigger an STK prompt with the order number as account_reference, listen for the webhook, and ship as soon as a payment with that BillRefNumber arrives.

M
MPESA
Payment confirmation
RJ12X9PQRS Confirmed. Ksh 1,500.00 sent to GALAXION STORES on 18/5/26 at 4:21 PM. New M-PESA balance is Ksh 8,420.00. 16:21

WhatsApp / SMS payment links

Share your payment link in a conversation — the customer enters the amount and reference and pays from their browser, with no technical effort on your side.

G
Galaxion Stores
business account
Hi Wanjiru, your invoice #4821 is ready. Pay KES 2,400 here, using reference 4821:
https://sozuri.net/zuka/pay/YOUR_CODE 11:02
Paid ๐Ÿ‘ 11:05 โœ“โœ“
Payment received. Asante! Your order is now being prepared. 11:05

QR codes on receipts & tables

Print your payment link’s QR code on receipts, restaurant tables, donation boxes — customers scan, pay, leave.

School fees & event tickets

Use account_reference for the student ID or ticket number (up to 12 characters) — it comes back as BillRefNumber, so reconciliation becomes a one-line database join.

Collect M-Pesa in under an hour.

Bring your Daraja credentials, plug them into a Sozuri project, and send your first STK Push from your payment link or the API today.