Checkout Sessions
The core way to take a payment. Create a session, send your customer to the hosted page, then confirm the result by redirect, polling or webhook. Includes split payouts, POS requests and paybill helpers.
https://api.paysasa.com/api/v1Authx-api-keyFormatJSONCreate 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.Redirect fields: 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 Paysasa 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, Paysasa 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;Retry guidance: only failed checkout sessions are retryable. Expired sessions must create a new checkout session. Do not auto-replay completed payments.
// 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"
}
}Checkout lifecycle: sessions move from 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.
Retrieve 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"Poll 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");
}POS-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);Business 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);Initiate 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);Cancel an active checkout session when the customer exits payment or requests cancellation. Cancellable states are typically pending, processing, and failed. 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"
}