Skip to main content
REST API v1

Cyzora API documentation

Collect M-Pesa payments with an STK prompt or a hosted checkout page, pay out to phones, tills, paybills and bank accounts, and hear about every result through signed webhooks.

1. Quickstart

Send your first test payment in a few minutes, without moving real money.

1. Create a test key

In the dashboard, open API keys and create a Test key. It starts with sk_test_.

2. Send a charge

Post an amount and a phone. 254700000001 always succeeds in test mode.

3. Get the result

Add a webhook URL and we post the signed result to your server.

Your first test charge
curl -X POST "https://api.cyzora.co.ke/api/v1/charges" \
  -H "X-API-Key: sk_test_..." \
  -H "Idempotency-Key: order_1042" \
  -H "Content-Type: application/json" \
  -d '{"amount": 150000, "phone": "254700000001", "account_ref": "Order 1042"}'

2. Authentication

Each key you create in the dashboard is a pair: a secret key and a public key.

KeyStarts withUse it for
Secret keysk_live_ / sk_test_Every server call: charges, checkout sessions, payouts, exports. Send it in the X-API-Key header or as Authorization: Bearer.
Public keypk_live_ / pk_test_Safe to put in a web page. It only identifies your business to the hosted checkout and cannot read data or move money.
Header
X-API-Key: sk_live_yourSecretKey

Keep secret keys on your server. Never put one in a website, a mobile app or a public repository. If a key leaks, revoke it under API keys and create a new one.

3. Test mode

Test keys use the same endpoints, never send a real prompt, and never touch your balance or reports.

Pay with one of these numbers to choose the outcome. The result arrives after a few seconds.

Phone numberOutcome
254700000001Success, with a TEST receipt
254700000002Failed: balance too low
254700000003Failed: the customer cancelled
254700000004Timed out: no PIN entered

Test webhooks are signed like real ones and carry "livemode": false. Test keys cannot create payouts.

4. Collect with STK push

Send an M-Pesa prompt straight to the customer’s phone. You build the screen; we handle M-Pesa.

POST/api/v1/charges
FieldRequiredDescription
amountYesAmount in cents. KES 1,500 is 150000.
phoneYesThe customer’s M-Pesa number, like 254712345678 or 0712345678.
account_refOptionalYour order or invoice number. It shows in your dashboard and in webhooks.
Send an Idempotency-Key
Add an Idempotency-Key header, such as your order number. If your request is retried, we return the first result instead of prompting the customer twice.
Response: the prompt is on its way
{
  "id": "7f0c0f43-5d1b-4a43-9a47-2f6f1d6c0b11",
  "reference": "chg_9f8a2c1b4d3e5a6b7c8d",
  "amount": 150000,
  "currency": "KES",
  "payer_phone": "254712345678",
  "account_ref": "Order 1042",
  "status": "pending",
  "mpesa_receipt": null
}

The charge starts as pending and becomes succeeded, failed or timeout. Wait for the webhook, or check it yourself:

Check a charge
curl "https://api.cyzora.co.ke/api/v1/charges/7f0c0f43-5d1b-4a43-9a47-2f6f1d6c0b11" \
  -H "X-API-Key: sk_live_..."

5. Hosted checkout

Start a payment on your server and send the customer to our page. It asks for their number, sends the prompt and shows the result.

  1. Your server calls POST /api/v1/transactions/initialize.
  2. Redirect the customer to the checkout_url you get back.
  3. They pay with M-Pesa and we send them to your callback_url.
  4. Confirm the payment with the webhook or the verify call before you hand over anything.
Start a session
curl -X POST "https://api.cyzora.co.ke/api/v1/transactions/initialize" \
  -H "X-API-Key: sk_live_..." \
  -H "Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 250000,
    "account_ref": "Order 1042",
    "description": "2 x Margherita pizza",
    "callback_url": "https://shop.example.com/orders/1042/thanks",
    "cancel_url": "https://shop.example.com/cart",
    "customer_phone": "0712345678",
    "metadata": { "order_id": "1042" },
    "expires_in": 1800
  }'
Response
{
  "reference": "chg_9f8a2c1b4d3e5a6b7c8d",
  "checkout_url": "https://checkout.cyzora.co.ke/pay/chg_9f8a2c1b4d3e5a6b7c8d",
  "amount": 250000,
  "currency": "KES",
  "status": "awaiting_payment",
  "expires_at": "2026-10-07T12:30:00Z",
  "livemode": true
}
FieldRequiredDescription
amountYesAmount in cents. The customer can’t change it.
account_refOptionalYour order number. Returned in webhooks.
callback_urlOptionalWhere the customer lands after paying.
cancel_urlOptionalWhere “Back to the store” goes if the payment fails or the link expires.
descriptionOptionalShown to the customer, up to 200 characters.
customer_phoneOptionalPrefills the M-Pesa number.
customer_emailOptionalStored with the session and returned in webhooks.
metadataOptionalUp to 20 text key and value pairs, returned in webhooks and the verify call.
expires_inOptionalSeconds the link stays payable, 300 to 86400. Default 1800.
Redirect rules

callback_url and cancel_url must use https (plain http only works for localhost). If you list allowed redirect domains on the API keys page, the URLs must be on one of them.

The customer returns with ?reference=…&status=success, but anyone can type that address. Only mark an order paid after the charge.succeeded webhook or the verify call says succeeded.

Verify a payment on your server
curl "https://api.cyzora.co.ke/api/v1/transactions/verify/chg_9f8a2c1b4d3e5a6b7c8d" \
  -H "X-API-Key: sk_live_..."

A session can be retried after a failed attempt until it expires, and only one payment can succeed per session. When nobody pays in time you get charge.expired and no money was taken.

7. Payouts

Send money from your Cyzora balance to an M-Pesa number, a till, a paybill or a bank account. Over the API you can pay any destination directly, or pay a saved payout channel by its ID. Withdrawals made by hand in the dashboard always go to a saved, verified channel.

Payout channels

TypeYou give usReady
mobileAn M-Pesa numberAfter you type the 6-digit code we text to that number
tillA Buy Goods till numberStraight away
paybillA paybill number and account numberStraight away
bankThe bank, account number and account nameStraight away. We pay through the bank’s M-Pesa paybill with your account number as the reference.

Channels are saved on the Payouts page of the dashboard. Each one gets an ID like ch_1a2b3c4d5e6f7a8b. Saving destinations you pay often is optional for the API.

List your channels
curl "https://api.cyzora.co.ke/api/v1/payout-channels" -H "X-API-Key: sk_live_..."

{
  "channels": [
    {
      "id": "5c1e...",
      "channel_id": "ch_1a2b3c4d5e6f7a8b",
      "type": "bank",
      "label": "Main account",
      "status": "verified",
      "display": "Equity Bank ••••6789"
    }
  ]
}

Send a payout

POST/api/v1/payouts
FieldRequiredDescription
amountYesAmount in cents that the destination receives in full. The minimum is KES 30 (3000). Cyzora’s 1.5% is taken from your remaining balance, not from this amount.
channel_idOr a destinationA saved, verified channel on your account.
destination_typeOr channel_idmobile, till, paybill or bank.
phonemobileThe M-Pesa number, e.g. 0712345678.
short_codetill, paybillThe till or paybill number (5 to 7 digits).
account_refpaybillThe account number on that paybill.
bank_code, account_number, account_namebankBank codes come from GET /api/v1/payout-banks. We pay through the bank’s M-Pesa paybill with the account number as the reference.
Pay out to a channel
curl -X POST "https://api.cyzora.co.ke/api/v1/payouts" \
  -H "X-API-Key: sk_live_..." \
  -H "Idempotency-Key: payout_2026_10_001" \
  -H "Content-Type: application/json" \
  -d '{"amount": 250000, "channel_id": "ch_1a2b3c4d5e6f7a8b"}'
Or pay any destination directly
# An M-Pesa number
curl -X POST "https://api.cyzora.co.ke/api/v1/payouts" -H "X-API-Key: sk_live_..." -H "Idempotency-Key: payout_2026_10_002" \
  -H "Content-Type: application/json" \
  -d '{"amount": 150000, "destination_type": "mobile", "phone": "0712345678"}'

# A bank account (paid through the bank's paybill)
curl -X POST "https://api.cyzora.co.ke/api/v1/payouts" -H "X-API-Key: sk_live_..." -H "Idempotency-Key: payout_2026_10_003" \
  -H "Content-Type: application/json" \
  -d '{"amount": 500000, "destination_type": "bank", "bank_code": "68", "account_number": "0123456789", "account_name": "Jane Doe"}'
Response
{
  "id": "9d4f...",
  "amount": 250000,
  "fee": 3750,
  "channel_id": "ch_1a2b3c4d5e6f7a8b",
  "destination_type": "paybill",
  "status": "requested",
  "created_at": "2026-10-07T12:00:00Z"
}

Use a live secret key and an Idempotency-Key so a retry never pays twice. Your business must be verified before payouts run. A payout goes requested, then processing, then paid or failed, and you get payout.paid or payout.failed. List payouts with GET /api/v1/payouts and read one with GET /api/v1/payouts/{id}.

See the fee before you pay
curl "https://api.cyzora.co.ke/api/v1/payouts/fee-preview?amount=250000" -H "X-API-Key: sk_live_..."

{ "amount": 250000, "fee": 3750 }

8. Webhooks

Add your server’s URL on the Webhooks page. That’s all you enter: we create the signing secret for you.

  • The secret starts with whsec_ and is shown once when you add the URL. You can view it again later with your password.
  • Each request carries X-Cyzora-Event and X-Cyzora-Signature: an HMAC-SHA256 of the raw request body with your secret, in hex.
  • Reply with any 2xx status to confirm. Otherwise we retry after 10 seconds, 30 seconds, 2 minutes and 10 minutes, then mark the delivery failed and email the address on your account.
  • Use “Send test” on the Webhooks page to get a webhook.test event.
EventWhen
charge.succeededM-Pesa confirmed a payment.
charge.failedA payment failed, timed out or was cancelled. For checkout sessions, retryable: true means the customer can still try again.
charge.expiredA checkout session ended unpaid. No money was taken.
payout.paidA payout reached its destination. Includes channel_id.
payout.failedA payout failed. The amount is back in your balance. Includes channel_id.
charge.succeeded
{
  "event": "charge.succeeded",
  "charge_id": "7f0c0f43-5d1b-4a43-9a47-2f6f1d6c0b11",
  "reference": "chg_9f8a2c1b4d3e5a6b7c8d",
  "account_ref": "Order 1042",
  "amount": 250000,
  "currency": "KES",
  "status": "succeeded",
  "mpesa_receipt": "TBR89X1024",
  "metadata": { "order_id": "1042" },
  "livemode": true
}
payout.paid
{
  "event": "payout.paid",
  "payout_id": "9d4f...",
  "channel_id": "ch_1a2b3c4d5e6f7a8b",
  "amount": 250000,
  "fee": 3750,
  "status": "paid",
  "transaction_id": "TJ81AB2C3D"
}
Verify the signature (Node.js)
import crypto from "node:crypto";
import express from "express";

const app = express();

// Use the raw body: re-serialised JSON will not match the signature.
app.post("/cyzora-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const expected = crypto
    .createHmac("sha256", process.env.CYZORA_WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");
  const received = String(req.headers["x-cyzora-signature"] || "");

  const ok = expected.length === received.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  if (event.event === "charge.succeeded") {
    // mark the order paid, using event.reference or event.metadata
  }
  res.sendStatus(200);
});
Verify the signature (PHP)
<?php
$raw = file_get_contents('php://input');
$secret = getenv('CYZORA_WEBHOOK_SECRET');
$expected = hash_hmac('sha256', $raw, $secret);
$received = $_SERVER['HTTP_X_CYZORA_SIGNATURE'] ?? '';

if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
if ($event['event'] === 'charge.succeeded') {
    // mark the order paid
}
http_response_code(200);

9. CSV export

Download your transactions for accounting, from the Transactions page or the API.

Export a date range
curl "https://api.cyzora.co.ke/api/v1/charges/export?from=2026-10-01&to=2026-10-07&status=succeeded" \
  -H "X-API-Key: sk_live_..." -o transactions.csv

from and to are dates and to is included. status is optional. Columns: date, customer phone, reference, M-Pesa receipt, status, amount in KES, payment link, and the customer’s answers.

10. WooCommerce

Take M-Pesa at checkout on your WordPress store with the official plugin.

Download the plugin (v1.2.0)
  1. In WordPress, go to Plugins, Add New, Upload Plugin, choose the zip, then Install and Activate. WooCommerce must already be active.
  2. Make sure your store uses https, and add its domain under Redirect domains on the API keys page.
  3. Open WooCommerce, Settings, Payments, Cyzora Pay, paste your secret key and save. Use a test key first.
  4. Add https://yourstore.com/wc-api/cyzora_webhook on the Webhooks page so orders update even if the customer closes the tab.
  5. Place a test order with 254700000001, then switch to your live key.

The plugin works with the classic and block checkouts and with WooCommerce’s order storage. It shows only for stores priced in KES, and it confirms every payment with Cyzora before marking an order paid.

11. Errors

Errors come back as JSON with a plain message you can show or log.

Error body
{ "error": "channel ch_1a2b... is not verified yet" }
StatusMeaning
400Something in the request is missing or wrong, such as the amount, the phone, or a balance too low for the payout.
401No key, or the key is wrong or revoked.
403Not allowed: a test key creating a payout, your business isn’t verified yet, or the account is suspended.
404Not found, including a channel that belongs to another account.
409That payout channel is already on your account.
410The payment link has expired.
422The payout channel isn’t verified yet, or was blocked.
429Too many requests. Wait a moment and try again.

Questions? Email support@cyzora.co.ke.