Quickstart: take your first payment
Go from nothing to a paid checkout session in six steps. You need a Paysasa business, an API key and a server that can make an HTTPS request.
https://api.paysasa.com/api/v1Authx-api-keyFormatJSONGet your business verified#
Money movement is switched on per business. Create your business in the dashboard, complete KYC and wait for verification. Until then, create and pay calls answer 403 with business_unverified or owner_kyc_unverified. Details are on Compliance & trust.
Create an API key#
In the dashboard open Businesses → API Keys and generate a key. It looks like apk_live_xxx.secret (live) or apk_test_xxx.secret (test). Store it in an environment variable on your server, never in a browser bundle or a public repository. See Authentication.
Create a checkout session#
Post the amount in minor units (KES 150.00 is 15000), a currency, your business ID and the URLs customers return to. The response contains a hosted_url.
curl -X POST "https://api.paysasa.com/api/v1/checkout/sessions" \
-H "x-api-key: apk_test_xxxxxxxx.your_secret" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"currency": "KES",
"business_id": "biz_xxxxxxxxxxxxxxxx",
"description": "Order #1042",
"success_url": "https://yourstore.com/order/success?id=1042",
"cancel_url": "https://yourstore.com/order/cancel?id=1042"
}'// 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;A successful response contains the new session:
// 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"
}
}Send the customer to pay#
Redirect the customer to session.hosted_url. Paysasa shows the payment methods you allowed (M-Pesa, wallet, card where enabled), then sends them back to your success_url or cancel_url. Do not trust the redirect alone: confirm the result from your server by polling GET /checkout/sessions/:id or, better, from a webhook.
Listen for webhooks#
Add a webhook URL in your business settings and handle checkout.paid to fulfil orders. Every delivery is signed with HMAC-SHA256 in the x-paysasa-signature header, so verify the raw body before trusting it:
import { createHmac } from "crypto";
function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
const expected = createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Use timingSafeEqual to prevent timing attacks
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature.replace("sha256=", ""), "hex");
if (a.length !== b.length) return false;
return require("crypto").timingSafeEqual(a, b);
}
// In your webhook handler (e.g. Next.js Route Handler):
export async function POST(req: Request) {
const rawBody = await req.text();
const sig = req.headers.get("x-paysasa-signature") ?? "";
if (!verifyWebhook(rawBody, sig, process.env.WEBHOOK_SECRET!)) {
return Response.json({ error: "Invalid signature" }, { status: 401 });
}
const event = JSON.parse(rawBody);
// Process event.event ...
return Response.json({ received: true });
}The full event list and payload shape are on Webhooks.
Harden and go live#
- Send an idempotency key on payments so a retry never charges twice.
- Branch on the HTTP status and, for 403s, the machine
code. See Error codes. - Switch from
apk_test_toapk_live_keys when your business is verified. - Browse every endpoint for payouts, invoices, subscriptions, Lengo, WaaS and more.