API v2.1 · Live & Test modes

Payza Payment API

Accept M-Pesa, Airtel, Telkom, Equitel, card, bank, and crypto payments across Africa with one REST endpoint.

Your servercalls POST /api/v1/pay
Payzatriggers STK push or hosted checkout
Customerenters PIN or picks a network
Your callback_urlreceives the result: fulfil the order
stk_push.sh
# fires an M-Pesa prompt in ~2s
curl -X POST payzaapi.co.ke/api/v1/pay \
  -H "X-Public-Key: pk_live_•••" \
  -H "X-Secret-Key: sk_live_•••" \
  -d '{"amount":500,"currency":"KES",
      "customer":{"phone":"2547•••••••"}}'

Recent changes

  • USD card payments (Paystack): payment_url now offers card, Apple Pay, mobile money, and Visa QR for USD, alongside the existing crypto option.
  • stk_push: false: suppresses the M-Pesa prompt and returns a hosted checkout page instead. Enables Airtel, Telkom, and Equitel on KES without auto-prompting Safaricom.
  • redirect_url & cancel_url: documented below. Required for the hosted checkout flow.
  • KES phone with stk_push:false: accepts all Kenyan network formats, not just Safaricom.
  • payment_url always returned: even for KES STK push. Redirecting to it shows a network-selection checkout page.

Payzaapi is a SaaS payment platform built for Africa. Integrate one API to collect payments: Payzaapi handles the M-Pesa STK push, hosted checkout, card processing, webhooks, and wallet crediting.

How it works

  1. Your server calls POST /api/v1/pay with the customer's details and amount.
  2. Payza triggers the payment (M-Pesa prompt, or returns a payment_url for hosted checkout).
  3. When payment completes, Payza POSTs to your callback_url with the result.
  4. Optionally verify the webhook signature and fulfil the order.

Base URL

https://payzaapi.co.ke

Quickstart

Step 1: Register at payzaapi.co.ke/auth/register.php and verify your email.

Step 2: Generate API keys from your dashboard under API Keys. You get a public key (pk_test_…) and a secret key (sk_test_…).

Step 3: Make a test payment using the keys in test mode (no real money moves).

Minimal example: M-Pesa STK push

# KES 1000 STK push: fires M-Pesa prompt to phone
curl -X POST https://payzaapi.co.ke/api/v1/pay \
  -H "X-Public-Key: pk_test_YOUR_KEY" \
  -H "X-Secret-Key: sk_test_YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1000,
    "currency": "KES",
    "reference": "ORDER_001",
    "customer": {
      "email": "[email protected]",
      "name": "Jane Wanjiku",
      "phone": "254712345678"
    },
    "callback_url": "https://yoursite.com/webhook/payza"
  }'

Minimal example: Hosted checkout (all networks)

# KES hosted checkout: no STK push, customer picks network on Payza page
curl -X POST https://payzaapi.co.ke/api/v1/pay \
  -H "X-Public-Key: pk_live_YOUR_KEY" \
  -H "X-Secret-Key: sk_live_YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1000,
    "currency": "KES",
    "stk_push": false,
    "reference": "ORDER_002",
    "customer": {
      "email": "[email protected]",
      "name": "Jane Wanjiku",
      "phone": "254733123456"
    },
    "callback_url": "https://yoursite.com/webhook/payza",
    "redirect_url": "https://yoursite.com/payment/confirm?ref=ORDER_002",
    "cancel_url":   "https://yoursite.com/addfunds"
  }'

Then redirect the customer to data.payment_url from the response.

Authentication

Every API request requires two headers:

HeaderDescription
X-Public-KeyYour public key (pk_test_… or pk_live_…)
X-Secret-KeyYour secret key (sk_test_… or sk_live_…). Keep this private: never expose it in frontend code.

Never put your secret key in client-side JavaScript or anywhere publicly visible. All API calls must be made from your server.

Environments

EnvironmentKey prefixBehaviour
Test pk_test_… / sk_test_… No real money moves. Payments auto-succeed instantly: full webhook lifecycle works exactly as live.
Live pk_live_… / sk_live_… Real M-Pesa STK pushes and card charges.

The same endpoint (/api/v1/pay) is used for both environments. The key prefix determines the mode.

POST /api/v1/pay: Initialize Payment

POSThttps://payzaapi.co.ke/api/v1/pay

Creates a new payment transaction. Behaviour depends on the currency and stk_push parameters: see table below.

Request headers

HeaderRequiredDescription
X-Public-KeyYour public key
X-Secret-KeyYour secret key
Content-Typeapplication/json

Request body parameters

ParameterTypeRequiredDescription
amountfloatPayment amount in the specified currency.
currencystringCurrency code. Default: KES. See Currencies.
referencestringYour unique order ID (max 64 chars). Auto-generated if omitted. Duplicate returns HTTP 409.
stk_pushbooleanNEW Default: true. Set to false to suppress the automatic M-Pesa STK push and receive a payment_url hosted checkout instead. With stk_push: false, all Kenyan network formats are accepted in customer.phone (not just Safaricom). See KES All Networks.
customer.emailstringCustomer email address.
customer.namestringCustomer full name.
customer.phonestring✅ for KESPhone number. Format: 0712345678 or 254712345678. With stk_push: true (default): Safaricom lines only. With stk_push: false: any Kenyan network.
callback_urlstringYour URL that Payza will POST the payment result to. Must be publicly reachable.
redirect_urlstringNEW URL to redirect the customer to after completing payment on the hosted checkout page. Include your reference as a query parameter so you can verify on return.
cancel_urlstringNEW URL to redirect the customer to if they cancel on the hosted checkout page.
descriptionstringPayment description shown on the checkout page (max 100 chars).
metadataobjectAny key-value pairs you want passed back unchanged in the webhook.

Example: KES STK push (Safaricom)

{
  "amount":    500,
  "currency":  "KES",
  "reference": "ORDER_001",
  "customer": {
    "email": "[email protected]",
    "name":  "Jane Wanjiku",
    "phone": "254712345678"
  },
  "callback_url": "https://yoursite.com/payment/payza"
}

Example: KES hosted checkout, all networks

{
  "amount":     500,
  "currency":   "KES",
  "stk_push":   false,
  "reference":  "ORDER_002",
  "customer": {
    "email": "[email protected]",
    "name":  "Jane Wanjiku",
    "phone": "254733123456"
  },
  "callback_url": "https://yoursite.com/payment/payza_ke_checkout",
  "redirect_url": "https://yoursite.com/payment/payza_ke_checkout?reference=ORDER_002",
  "cancel_url":   "https://yoursite.com/addfunds"
}

Example: NGN card / bank transfer

{
  "amount":     5000,
  "currency":   "NGN",
  "reference":  "ORDER_003",
  "customer": {
    "email": "[email protected]",
    "name":  "Emeka Obi"
  },
  "callback_url": "https://yoursite.com/payment/payza_ngn",
  "redirect_url": "https://yoursite.com/payment/payza_ngn?reference=ORDER_003",
  "cancel_url":   "https://yoursite.com/addfunds"
}

Example: Multi-currency (PawaPay mobile money) NEW

Every currency other than KES and NGN: Ghanaian Cedi, Tanzanian Shilling, West African CFA, Ugandan Shilling, Rwandan Franc, Zambian Kwacha, and more: is handled the same way as NGN above: call /api/v1/pay, then redirect your customer to payment_url. They enter their mobile money number there and PawaPay sends them a payment prompt directly on their phone. See Supported Currencies for the full, current list.

{
  "amount":     50000,
  "currency":   "UGX",
  "reference":  "ORDER_004",
  "customer": {
    "email": "[email protected]",
    "name":  "Amara Nakato"
  },
  "callback_url": "https://yoursite.com/payment/payza_ugx",
  "redirect_url": "https://yoursite.com/payment/payza_ugx?reference=ORDER_004",
  "cancel_url":   "https://yoursite.com/addfunds"
}

Requesting a currency that isn't live yet (see Supported Currencies) returns 400 with a message telling you it's coming soon: check before charging in it.

Response

200 Success 4xx / 5xx Errors
{
  "success": true,
  "message": "M-Pesa STK push sent to 254712345678.",
  "data": {
    "reference":          "ORDER_001",
    "payment_url":        "https://payzaapi.co.ke/payment/checkout.php?ref=ORDER_001",
    "crypto_payment_url": "https://payzaapi.co.ke/payment/cryptomus-checkout.php?type=api&reference=ORDER_001",
    "amount":      500,
    "currency":    "KES",
    "gateway":     "payzaapi",
    "actual_gateway": "tuma", // NEW: the real processor: tuma, korapay, test, or pending
    "status":      "pending",
    "stk_sent":    true,
    "accepted_methods": ["mpesa", "airtel", "bank_transfer", "card", "crypto"]
  }
}

payment_url is always returned, even for KES STK push. Redirecting to it shows Payza's hosted checkout page with all network options. With stk_push: false, stk_sent will be false and no prompt fires.

gateway is always "payzaapi". Use actual_gateway to see the real processor behind a transaction: "tuma" (M-Pesa STK push), "korapay" (multi-currency), "test" (test-mode key), or "pending" (KES hosted checkout: no network chosen yet). This field is the same for both /pay and /verify responses on a given transaction.

HTTP CodeCondition
400Missing or invalid field
401Missing or invalid API credentials
409Duplicate reference
429Rate limit exceeded
502Upstream gateway rejected the request

KES: Accepting All Networks NEW

By default, KES payments fire an M-Pesa STK push. To show Payza's hosted checkout page instead: where the customer selects their own network: set "stk_push": false in your request.

Safaricom · 07xx / 01xx Airtel · 073x / 074x Telkom · 077x Equitel · 076x

With stk_push: false the phone field accepts any Kenyan network number. Payza's checkout page detects the network and shows the correct payment flow.

How the hosted checkout flow works

  1. You call POST /api/v1/pay with stk_push: false, redirect_url, and cancel_url.
  2. Payza returns payment_url: no prompt fires on the customer's phone.
  3. You redirect the customer to payment_url.
  4. Customer sees a network selector: Safaricom / Airtel / Telkom / Equitel. They pick their network, enter their number, and tap Pay.
  5. Payment completes → Payza fires your callback_url webhook → Payza redirects the customer to your redirect_url.
  6. On your redirect_url page, call GET /api/v1/verify/{reference} to confirm the status (webhook may have arrived already).

Full example

<?php
$reference = 'PZKEC-' . time() . '-' . bin2hex(random_bytes(5));

$payload = [
    'amount'       => 500,
    'currency'     => 'KES',
    'stk_push'     => false,
    'reference'    => $reference,
    'customer'     => [
        'email' => '[email protected]',
        'name'  => 'John Doe',
        'phone' => '254733123456',  // any Kenyan network
    ],
    'callback_url' => 'https://yoursite.com/payment/payza_ke_checkout',
    'redirect_url' => 'https://yoursite.com/payment/payza_ke_checkout?reference=' . $reference,
    'cancel_url'   => 'https://yoursite.com/addfunds',
    'description'  => 'Add Funds',
];

$ch = curl_init('https://payzaapi.co.ke/api/v1/pay');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode($payload),
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'X-Public-Key: ' . $publicKey,
        'X-Secret-Key: ' . $secretKey,
    ],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($result['success']) {
    // Redirect customer to hosted checkout: they pick their network there
    header('Location: ' . $result['data']['payment_url']);
    exit;
}

Important: With stk_push: true (default), Safaricom numbers will still receive an STK push even if you redirect to payment_url. Use stk_push: false whenever you want the customer to initiate payment themselves on Payza's checkout page.

GET /api/v1/verify/{reference}: Verify Payment

GEThttps://payzaapi.co.ke/api/v1/verify/{reference}

Returns the current status of a transaction. For live pending transactions, Payza polls the upstream gateway before responding: the status is always up to date.

Use /verify as a fallback when a user returns to your site after the hosted checkout redirect. The webhook may have already arrived and credited the balance: check your database first before calling this endpoint.

curl https://payzaapi.co.ke/api/v1/verify/ORDER_001 \
  -H "X-Public-Key: pk_live_YOUR_KEY" \
  -H "X-Secret-Key: sk_live_YOUR_SECRET"
{
  "success": true,
  "data": {
    "reference": "ORDER_001",
    "status":    "success",  // pending | success | failed | cancelled
    "amount":    500,
    "currency":  "KES",
    "gateway":   "payzaapi",
    "actual_gateway": "tuma", // same value /pay returned for this reference
    "paid_at":   "2024-01-01T12:05:12+03:00"
  }
}
StatusMeaning
pendingAwaiting customer action
successPayment confirmed
failedWrong PIN, insufficient funds, or timeout
cancelledCustomer cancelled

₿ Accepting Crypto Payments

Every transaction created via POST /api/v1/pay automatically includes a crypto_payment_url in the response. No extra integration step required.

For USD, payment_url (the standard checkout redirect used by every currency) now also offers card, Apple Pay, mobile money, and Visa QR via Paystack: crypto isn't the only USD option anymore. Use crypto_payment_url specifically when you want to send a customer straight to the crypto flow, skipping the card option. See Supported Currencies.

How it works

  1. Call POST /api/v1/pay in any currency (USD recommended for crypto).
  2. Response includes data.crypto_payment_url.
  3. Redirect to crypto_payment_url: customer picks coin and network on Cryptomus-hosted page.
  4. On-chain confirmation → Payza fires webhook with "gateway": "cryptomus".
CoinNetworks
USDTTRON (TRC-20), Ethereum (ERC-20), BSC (BEP-20), Polygon
USDCEthereum, Polygon, BSC
BTCBitcoin
ETHEthereum
BNBBSC
SOLSolana
TRXTRON
LTCLitecoin
DOGEDogecoin
TONTON

Crypto payments use the same verify endpoint and webhook system: no separate flow.

Webhooks

When a payment completes, Payza POSTs to your callback_url. Respond with HTTP 200 within 10 seconds.

{
  "event":     "payment.success",
  "reference": "ORDER_001",
  "status":    "success",
  "amount":    500,
  "currency":  "KES",
  "gateway":   "tuma",  // tuma | korapay | pawapay | paystack | cryptomus
  "customer": { "email": "[email protected]", "name": "Jane Wanjiku" },
  "metadata": { "user_id": "123" },
  "paid_at":   "2024-01-01T12:05:12+03:00"
}

No retry policy currently: if your endpoint misses a webhook, use GET /api/v1/verify/{reference} to poll for status. Also check your database before crediting: Payza may POST the same event twice in some edge cases.

Signature verification (optional)

Every account has its own Webhook Signing Secret, unique to you — find it in Dashboard → API Keys. If you use it, Payza includes an X-Payza-Signature header on every webhook: HMAC-SHA256 of the raw body using that secret.

<?php
$rawBody  = file_get_contents('php://input');
$received = $_SERVER['HTTP_X_PAYZA_SIGNATURE'] ?? '';

// Only verify if you have an encryption key configured
if (!empty($received) && !empty($encryptionKey)) {
    $expected = hash_hmac('sha256', $rawBody, $encryptionKey);
    if (!hash_equals($expected, $received)) {
        http_response_code(401);
        exit('Invalid signature');
    }
}

$data = json_decode($rawBody, true);
if ($data['event'] === 'payment.success') {
    // Check your DB first to avoid double-crediting
    fulfillOrder($data['reference']);
}
http_response_code(200);
echo 'OK';

Supported Currencies

This table is generated live from our current PawaPay/Paystack configuration: not a hand-maintained list: so it reflects exactly what's payable right now.

CurrencyCodePayment methodMinimumNotes
Kenyan Shilling KES M-Pesa STK push or hosted checkout (all networks) KES 1 Use stk_push: false for Airtel/Telkom/Equitel
Nigerian Naira NGN Card, bank transfer, OPay NGN 100 Returns payment_url: redirect customer
Ghanaian Cedi GHS Mobile money (PawaPay) GHS 1
Tanzanian Shilling TZS Mobile money (PawaPay) TZS 500
West African CFA Franc XOF Mobile money (PawaPay) XOF 100 Ivory Coast (Orange, MTN, Wave) and Senegal (Orange, Free)
US Dollar USD Card, Apple Pay, Visa QR, mobile money (Paystack) or crypto (BTC, ETH, USDT + more) USD 0.50 See Crypto Payments for the crypto rail. Which Paystack channels actually appear (Apple Pay, Visa QR, mobile money) depends on the customer's card/device: card is always available.
Rwandan Franc RWF Mobile money (PawaPay) Set by PawaPay* MTN, Airtel
Ugandan Shilling UGX Mobile money (PawaPay) Set by PawaPay* MTN, Airtel
Zambian Kwacha ZMW Mobile money (PawaPay) Set by PawaPay* Airtel, MTN, Zamtel
Malawian Kwacha MWK Mobile money (PawaPay) Set by PawaPay* Airtel, TNM
Sierra Leonean Leone SLL Mobile money (PawaPay) Set by PawaPay* Orange
Congolese Franc CDF Mobile money (PawaPay) Set by PawaPay* DRC: Airtel, MTN, Orange
Mozambican Metical MZN Mobile money (PawaPay) Set by PawaPay* ⚠️ Recently enabled: if you hit unexpected errors on MZN specifically, contact [email protected] before assuming it's your integration
Central African CFA Franc XAF Mobile money (PawaPay) Set by PawaPay* Cameroon: MTN, Orange

* Payza doesn't enforce its own minimum for currencies marked "Set by PawaPay": an amount that's too small is rejected directly by PawaPay's API with an error you can surface to the customer. If you need an exact floor to validate client-side before submitting, check PawaPay's current published limits or ask support rather than relying on a guessed figure.

Coming soon: accepted as a currency value in some contexts but not live yet on our account; requesting these returns 400 with a "coming soon" message rather than silently failing: ZAR (South African Rand), GNF (Guinean Franc), BIF (Burundian Franc), GMD (Gambian Dalasi), SZL (Eswatini Lilangeni), LRD (Liberian Dollar).

Rate Limits

100 requests per 60 seconds per API key pair. Exceeding returns HTTP 429 with a Retry-After header.

HeaderDescription
X-RateLimit-Limit100
X-RateLimit-RemainingRequests left in current window
X-RateLimit-ResetUnix timestamp when window resets
Retry-AfterSeconds to wait (only on 429)

Error Handling

{ "success": false, "message": "A valid customer.email is required." }
HTTP CodeMeaning
200Success
400Bad request: missing or invalid parameters
401Unauthorised: invalid or missing API keys
403Forbidden: account suspended
404Transaction not found
409Duplicate reference
429Rate limit exceeded
502Upstream gateway rejected the request
500Internal server error

Code Examples

PHP: KES STK push

<?php
$payload = [
    'amount'       => 500,
    'currency'     => 'KES',
    'reference'    => 'ORD_' . uniqid(),
    'customer'     => ['email' => '[email protected]', 'name' => 'Jane', 'phone' => '254712345678'],
    'callback_url' => 'https://yoursite.com/payment/payza',
];
$ch = curl_init('https://payzaapi.co.ke/api/v1/pay');
curl_setopt_array($ch, [
    CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-Public-Key: ' . $publicKey,
        'X-Secret-Key: ' . $secretKey,
    ],
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
// STK push sent: store reference, wait for webhook

PHP: KES hosted checkout (all networks)

<?php
$ref = 'PZKEC-' . time() . '-' . bin2hex(random_bytes(5));
$payload = [
    'amount'       => 500,
    'currency'     => 'KES',
    'stk_push'     => false,          // no auto STK push
    'reference'    => $ref,
    'customer'     => ['email' => '[email protected]', 'name' => 'User', 'phone' => '254733123456'],
    'callback_url' => 'https://yoursite.com/payment/payza_ke_checkout',
    'redirect_url' => 'https://yoursite.com/payment/payza_ke_checkout?reference=' . $ref,
    'cancel_url'   => 'https://yoursite.com/addfunds',
];
// ... same curl call ...
// On success:
header('Location: ' . $result['data']['payment_url']);
exit;

PHP: NGN card / bank transfer

<?php
$ref = 'PZNG-' . time() . '-' . bin2hex(random_bytes(5));
$payload = [
    'amount'       => 5000,
    'currency'     => 'NGN',
    'reference'    => $ref,
    'customer'     => ['email' => '[email protected]', 'name' => 'Emeka'],
    'callback_url' => 'https://yoursite.com/payment/payza_ngn',
    'redirect_url' => 'https://yoursite.com/payment/payza_ngn?reference=' . $ref,
    'cancel_url'   => 'https://yoursite.com/addfunds',
];
// ... same curl call ...
header('Location: ' . $result['data']['payment_url']);
exit;

Security Best Practices

Testing Your Integration

Test mode uses pk_test_… / sk_test_… keys. Payments auto-succeed with no real phone prompt. The full webhook lifecycle works identically to live.

Test checklist

  1. Initialize a KES STK push: confirm reference stored and webhook fires.
  2. Initialize a KES hosted checkout (stk_push: false): confirm redirect to payment_url works.
  3. Initialize an NGN payment: confirm redirect to payment_url and webhook fires.
  4. Call GET /api/v1/verify/{reference}: confirm status: success.
  5. Test duplicate reference: expect HTTP 409.
  6. Test missing field: expect HTTP 400 with descriptive message.

Once all steps pass in test mode, switch to live keys.

FAQ

How do I accept Airtel, Telkom, or Equitel payments in KES?

Set "stk_push": false in your request. This suppresses the automatic M-Pesa prompt and returns a payment_url: redirect the customer there. Payza's checkout page shows a network selector (Safaricom, Airtel, Telkom, Equitel) and the customer initiates payment themselves. See KES All Networks.

When should I use stk_push: true vs false?

true (default): best for Safaricom-only flows where you want a seamless background STK push without redirecting the customer. false: use when you want to support all Kenyan networks, or when you don't want to fire a prompt automatically.

Does payment_url work for Safaricom too?

Yes. Every successful /api/v1/pay response includes a payment_url regardless of currency or stk_push setting. Redirecting a Safaricom customer to payment_url shows them Payza's checkout with all network options.

Do I need a Multi-Currency subscription for NGN?

NGN worked in our production integration without activating any separate subscription. If you encounter issues, contact [email protected].

How long before a payment expires?

M-Pesa STK push: 5 minutes. Hosted checkout sessions: 30 minutes. After expiry, create a new payment.

Can I re-use a reference?

No. Each reference must be globally unique. Use a combination of your order ID and a timestamp or random bytes.

My webhook isn't arriving: what should I check?

  1. Is your callback_url publicly reachable from the internet?
  2. Does your server return HTTP 200 within 10 seconds?
  3. Use GET /api/v1/verify/{reference} as a fallback to check status.

What Kenyan numbers are accepted?

With stk_push: true (default): Safaricom only: 07xx or 01xx. Format: 0712345678 or 254712345678.

With stk_push: false: Any Kenyan network: Safaricom, Airtel (073x/074x), Telkom (077x), Equitel (076x).