API Reference
Jump to section
Base URL
https://api.paysasa.com/api/v1
Include your composite API key in x-api-key. Format: apk_live_xxx.secret or apk_test_xxx.secret. Generate keys from Businesses → API Keys.
POST /checkout/sessions HTTP/1.1
Host: api.paysasa.com
Content-Type: application/json
x-api-key: apk_live_a1b2c3d4.7f9e0a...All money-moving APIs use minor units (integer amounts). For KES and USD this is cents (100 minor = 1.00). UGX and similar zero-decimal currencies use 1 minor = 1 unit. Only currencies enabled for your account are accepted; disabled codes return 400.
/currenciesList enabled currencies (session required). Use this to populate checkout and wallet selectors.
const res = await fetch("https://api.paysasa.com/api/v1/currencies", { credentials: "include" });
const { currencies } = await res.json();
// [{ code: "KES", name: "Kenyan Shilling", decimal_places: 2, is_base: 0 }, ...]KES. PayPal top-up settles to USD. Other pairs use wallet balance and conversion.Send X-Idempotency-Key on money-moving requests. Replays return the original result with idempotent_replay: true when applicable.
- Required:
/p2p/transfers,/checkout/sessions/:id/pay - Recommended:
/payouts(withdrawals),/wallets/convert,/invoices/:id/pay - Business-scoped: checkout payments key on business + idempotency key
X-Idempotency-Key: 7f3c2b1a-4e5d-6f7a-8b9c-0d1e2f3a4b5c
/checkout/sessionsCreate a new checkout session. Share the hosted_url with your customer to complete payment. Paysasa supports post-payment redirects via success_url, cancel_url, return_url, and optional callback_url for external apps.
403 with code business_unverified (or owner_kyc_unverified) until the business is verified. See Compliance & Trust below.success_url is the primary post-payment destination. return_url is a fallback when success_url is omitted. callback_url is optional — use the same absolute URL as success_url for cross-domain apps and POS. Using the same URL for all three fields is fine. External absolute URLs on another host are preserved (not rewritten to the Pay tenant domain).Request Body
| Parameter | Type | Description |
|---|---|---|
| amount * | integer | Amount in minor units (cents/fils). e.g. KES 100 = 10000 |
| currency * | string | ISO 4217 currency code. e.g. KES, USD |
| business_id * | string | Your business ID (from Businesses page) |
| description | string | Human-readable description shown to the customer |
| success_url | string | Redirect URL after successful payment |
| cancel_url | string | Redirect URL if customer cancels |
| return_url | string | Fallback return URL after checkout. Used when success_url is not set, or as a secondary hand-off target. |
| callback_url | string | Optional. Explicit merchant-app URL for cross-domain integrations. Checked first in the post-payment redirect chain. Usually the same as success_url; omit if success_url already points to your external app. |
| allow_methods | string[] | Payment methods: wallet, m_pesa (KES only), paypal (USD sessions). Must match an active business wallet for the session currency. |
| metadata | object | Arbitrary key-value data attached to the session (passed in webhook) |
| use_split | boolean | Enable post-payment split payout. When omitted, saved business default splits apply automatically if configured in Business Settings. Pass false to disable defaults for one session. |
| split_mode | string | percentage (default) or fixed. Secondary wallets receive up to 100% (or up to the session total for fixed mode); any remainder stays in the primary checkout wallet automatically. Business default splits are percentage-based. |
| split_recipients | object[] | Required when use_split is true. Percentage mode: { wallet_id, percentage } (sum ≤ 100; remainder to primary). Fixed mode: { wallet_id, amount } where amount is in minor units (sum ≤ session total; remainder to primary). Recipients must be secondary business wallets for the same business_id and currency — create additional same-currency wallets with a display_name via Business Settings. Cross-business or personal wallets are not supported. |
seller_wallet_id). After payment succeeds, Pay automatically moves each split portion from that wallet to recipient wallets in the same business. Use percentage splits for proportional sharing, or fixed amounts when each recipient should receive an exact value. Configure reusable percentage defaults under Business Settings → Default Checkout Split, or pass use_split and split_mode per session.// Create a checkout session (Node.js)
const res = await fetch("https://api.paysasa.com/api/v1/checkout/sessions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "apk_live_a1b2c3d4.7f9e0a...",
},
body: JSON.stringify({
amount: 15000, // KES 150.00
currency: "KES",
business_id: "biz_xxxxxxxxxxxxxxxx",
description: "Order #1042 — 2x T-Shirts",
success_url: "https://yourstore.com/order/success?id=1042",
cancel_url: "https://yourstore.com/order/cancel?id=1042",
return_url: "https://yourstore.com/checkout/return?id=1042",
allow_methods: ["wallet", "m_pesa"],
metadata: { orderId: "1042", customerId: "cus_abc" },
// Optional: percentage split across two same-business wallets
use_split: true,
split_mode: "percentage",
split_recipients: [
{ wallet_id: "wal_fulfillment_01", percentage: 60 },
{ wallet_id: "wal_marketing_01", percentage: 40 },
],
// Or fixed amounts (minor units) that sum to amount:
// use_split: true,
// split_mode: "fixed",
// split_recipients: [
// { wallet_id: "wal_ops_01", amount: 10000 },
// { wallet_id: "wal_reserve_01", amount: 5000 },
// ],
}),
});
const { session } = await res.json();
// Redirect customer:
window.location.href = session.hosted_url;// Response 201 Created
{
"session": {
"id": "ses_01hx...",
"reference": "CHK-20240815-0042",
"status": "pending",
"amount": 15000,
"currency": "KES",
"hosted_url": "https://app.paysasa.com/checkout/ses_01hx...",
"expires_at": "2024-08-15T15:00:00.000Z",
"metadata": { "orderId": "1042" },
"return_url": "https://yourstore.com/checkout/return?id=1042",
"success_url": "https://yourstore.com/order/success?id=1042",
"cancel_url": "https://yourstore.com/order/cancel?id=1042"
}
}pending to processing when payment starts, then to completed on success or failed/expired/cancelled when payment does not complete. Retry is supported only for failed sessions. Expired and cancelled sessions require a new checkout session. Completed sessions are final and must not be auto-retried./checkout/sessions/:idRetrieve a checkout session by ID to check payment status. This read endpoint is currently public for hosted checkout pages.
const res = await fetch(
"https://api.paysasa.com/api/v1/checkout/sessions/ses_01hx..."
);
const { session } = await res.json();
console.log(session.status); // "pending" | "paid" | "expired" | "cancelled"/checkout/sessions/:idPoll the checkout session status from your backend or hosted checkout page until the session moves out of pending. Use the same read endpoint you use for the initial session lookup.
async function waitForPayment(sessionId) {
const deadline = Date.now() + 2 * 60 * 1000;
while (Date.now() < deadline) {
const res = await fetch("https://api.paysasa.com/api/v1/checkout/sessions/" + sessionId);
const { session } = await res.json();
if (session.status !== "pending") {
return session;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
throw new Error("Timed out waiting for checkout session status");
}/checkout/requestsPOS-focused helper endpoint. It creates a checkout session and optional linked payment request for a recipient identifier (phone/wallet ID). Supports the same redirect fields (success_url, cancel_url, return_url, callback_url) and use_split / split_recipients as /checkout/sessions.
const res = await fetch("https://api.paysasa.com/api/v1/checkout/requests", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "apk_live_a1b2c3d4.7f9e0a...",
},
body: JSON.stringify({
business_id: "biz_xxxxxxxxxxxxxxxx",
amount: 12500,
currency: "KES",
to: "254712345678",
description: "POS checkout request",
return_url: "https://yourstore.com/checkout/return?id=1042",
success_url: "https://yourstore.com/order/success?id=1042",
cancel_url: "https://yourstore.com/order/cancel?id=1042",
use_split: true,
split_recipients: [
{ wallet_id: "wal_ops_01", percentage: 70 },
{ wallet_id: "wal_reserve_01", percentage: 30 },
],
}),
});
const data = await res.json();
console.log(data.session_id, data.hosted_url, data.checkout_qr_url);/checkout/discovery/paybillBusiness paypoint helper: create a custom-amount hosted checkout. Set the account reference to attribute funds to a WaaS virtual wallet. Use identifier, paybill_reference, serial_account_number, or external_id (e.g. WAAS-POLL-1, 22457777, or pollsoko_user_992). For server-to-server integrations, prefer /waas/deposits/checkout with API key auth.
// Paypoint / kiosk flow — links checkout to WaaS virtual account when reference matches
const res = await fetch("https://api.paysasa.com/api/v1/checkout/discovery/paybill", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
business_id: "biz_xxxxxxxxxxxxxxxx",
// Any of these can identify the virtual account (explicit fields take precedence):
paybill_reference: "WAAS-POLL-1",
// external_id: "pollsoko_user_992",
// virtual_account_id: "va_xxxxxxxxxxxxxxxx",
amount: 50000,
currency: "KES",
description: "Wallet top-up",
}),
});
const data = await res.json();
// data.waas is set when a virtual account was matched
console.log(data.hosted_url, data.waas);/checkout/sessions/:id/payInitiate payment on an existing checkout session. Use this for cashier-triggered STK push shortcuts.
const res = await fetch("https://api.paysasa.com/api/v1/checkout/sessions/ses_01hx.../pay", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "apk_live_a1b2c3d4.7f9e0a...",
"x-idempotency-key": crypto.randomUUID(),
},
body: JSON.stringify({
payment_method: "m_pesa",
phone: "254712345678",
customer_name: "Jane Customer",
}),
});
const result = await res.json();
console.log(result.status, result.checkout_request_id);/checkout/sessions/:id/cancelCancel an active checkout session when the customer exits payment or requests cancellation. Cancellable states are typically pending,processing, andfailed. Completed and expired sessions are not cancellable.
const res = await fetch("https://api.paysasa.com/api/v1/checkout/sessions/ses_01hx.../cancel", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "apk_live_a1b2c3d4.7f9e0a...",
},
body: JSON.stringify({
reason: "Customer cancelled checkout",
}),
});
const data = await res.json();
console.log(data.status, data.message); // "cancelled", "Checkout cancelled"// Response 200
{
"ok": true,
"status": "cancelled",
"message": "Checkout cancelled"
}Issue itemized invoices and share a hosted payment page. For recurring plans with customers, products, and lifecycle events, use the Subscriptions API. You can also enable recurring on a template invoice to generate a new invoice each billing cycle.
payment_url (e.g. https://app.paysasa.com/pay/invoice/inv_…). Customers can pay with M-PESA without signing in, or with wallet after sign-in./invoicesCreate an invoice. Requires business_id when using API key auth. Amounts are in minor units (KES 500 = 50000).
| Parameter | Type | Description |
|---|---|---|
| business_id * | string | Your business ID (required with API key) |
| line_items * | array | [{ description, quantity, unit_price }] — unit_price in minor units |
| to | string | Payer email, phone, or pay handle (optional) |
| due_date | string | ISO datetime when payment is due |
| status | string | draft | pending (default pending) |
| metadata | object | Your subscription/plan IDs — returned on GET |
| external_reference | string | e.g. sub_12345 from your billing system |
| recurring | object | Subscription template — see below |
// Monthly recurring invoice
const res = await fetch("https://api.paysasa.com/api/v1/invoices", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "apk_live_a1b2c3d4.7f9e0a...",
},
body: JSON.stringify({
business_id: "biz_xxxxxxxxxxxxxxxx",
to: "[email protected]",
currency: "KES",
status: "pending",
due_date: new Date(Date.now() + 7 * 86400000).toISOString(),
external_reference: "sub_plan_pro_001",
metadata: { subscription_id: "sub_plan_pro_001", plan: "pro" },
line_items: [
{
description: "Pro plan — monthly",
quantity: 1,
unit_price: 299900, // KES 2,999.00
},
],
recurring: {
enabled: true,
interval: "monthly",
every: 1,
end_at: null, // or ISO date to stop renewals
},
business_notes: "Thanks for subscribing to Pro.",
}),
});
const { invoice, payment_url } = await res.json();
// Share payment_url with your customer or store it in your app{
"invoice": {
"id": "inv_01hx...",
"invoice_number": "INV-ABC123-0001",
"amount": 299900,
"amount_paid": 0,
"amount_remaining": 299900,
"currency": "KES",
"status": "pending",
"payment_url": "https://app.paysasa.com/pay/invoice/inv_01hx...",
"recurring": { "enabled": true, "interval": "monthly", "every": 1, "next_run": "..." },
"metadata": { "subscription_id": "sub_plan_pro_001", "plan": "pro" }
},
"payment_url": "https://app.paysasa.com/pay/invoice/inv_01hx..."
}/invoicesList invoices for your business. With API key auth, pass business_id (required).
curl "https://api.paysasa.com/api/v1/invoices?business_id=biz_xxxxxxxxxxxxxxxx&status=pending" \ -H "x-api-key: apk_live_a1b2c3d4.7f9e0a..."
/invoices/:idRetrieve invoice status (paid, pending, amount_remaining). Poll after customer pays on the hosted page.
/invoices/:id/sendEmail or SMS the invoice link to the customer (includes payment_url).
await fetch("https://api.paysasa.com/api/v1/invoices/inv_01hx.../send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "apk_live_a1b2c3d4.7f9e0a...",
},
body: JSON.stringify({
email: "[email protected]",
message: "Your subscription renewal is ready.",
}),
});/invoices/:id/publicNo authentication — used by the hosted payment page. Returns amount, line items, and status only.
/invoices/:id/checkoutNo authentication — start M-PESA payment from your own UI (returns hosted_url). The hosted page at /pay/invoice/:id uses this for the payer checkout experience.
const res = await fetch("https://api.paysasa.com/api/v1/invoices/inv_01hx.../checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
amount: 299900,
customer_phone: "254712345678",
}),
});
const { hosted_url } = await res.json();
window.location.href = hosted_url;Recurring invoices: Set recurring.enabled: true on a template invoice to generate a new invoice each cycle. For plan-based billing, use the Subscriptions API.
Create billing customers, products, and prices, then start subscriptions for your business. Each billing period issues an invoice customers can pay with M-Pesa or wallet. Authenticate with a business API key, or a signed-in session plus business_id.
| POST /billing/customers | Create billing customer |
| POST /billing/products | Create product |
| POST /billing/prices | Recurring price (amount minor units, interval day|week|month|year) |
| POST /subscriptions | Create subscription; returns payment_url for first invoice |
| POST /subscriptions/checkout | Create a subscription and return a checkout URL |
| POST /subscriptions/:id/cancel|pause|resume | Cancel, pause, or resume a subscription |
| GET /subscriptions/portal | Customer portal (subscription_id + email or phone) |
const res = await fetch("https://pay.airsoko.com/api/v1/subscriptions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "apk_live_…",
},
body: JSON.stringify({
business_id: "biz_…",
customer_id: "cust_…",
items: [{ price: "price_…", quantity: 1 }],
collection_method: "send_invoice",
trial_days: 0,
}),
});
const { subscription, payment_url } = await res.json();subscription.created, subscription.updated, subscription.canceled, customer.created, invoice.created, and invoice.paid. Configure endpoints under your business webhook settings.Rate Limits & Notes
- • API keys are scoped to a single business — use one key per business.
- • Amounts are in minor units per currency (e.g. KES/USD: 100 = 1.00; UGX: 1 = 1).
- • Disabled currencies are rejected with HTTP 400 — enable them in admin before use.
- • Rate limits: default 120 read / 60 write requests per minute per IP or API key (see
X-RateLimit-*headers). - • Checkout sessions expire after 30 minutes by default.
- • Webhook retries: 3 attempts with exponential back-off (1m, 5m, 30m).
- • Need help? Email [email protected]