API Reference

Integrate Paysasa checkout, invoices, Lengo, payment links, and webhooks into your application. Checkout, invoices, and reconciliation endpoints support API key authentication from your Businesses page.

Jump to section

Base URL

https://api.paysasa.com/api/v1

Production

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.

http
POST /checkout/sessions HTTP/1.1
      Host: api.paysasa.com
Content-Type: application/json
x-api-key: apk_live_a1b2c3d4.7f9e0a...
Security: Never expose your API key in client-side code or public repositories. Use it only in server-side code or secure environments. Keys can be revoked at any time from your Businesses page.

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.

GET/currencies

List enabled currencies (session required). Use this to populate checkout and wallet selectors.

javascript
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 }, ...]
Payment rails: M-Pesa checkout and withdrawals require 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
http
X-Idempotency-Key: 7f3c2b1a-4e5d-6f7a-8b9c-0d1e2f3a4b5c
POST/checkout/sessions

Create 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.

Verification required: By default, create/pay returns 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 Pay tenant domain).

Request Body

ParameterTypeDescription
amount *integerAmount in minor units (cents/fils). e.g. KES 100 = 10000
currency *stringISO 4217 currency code. e.g. KES, USD
business_id *stringYour business ID (from Businesses page)
descriptionstringHuman-readable description shown to the customer
success_urlstringRedirect URL after successful payment
cancel_urlstringRedirect URL if customer cancels
return_urlstringFallback return URL after checkout. Used when success_url is not set, or as a secondary hand-off target.
callback_urlstringOptional. 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_methodsstring[]Payment methods: wallet, m_pesa (KES only), paypal (USD sessions). Must match an active business wallet for the session currency.
metadataobjectArbitrary key-value data attached to the session (passed in webhook)
use_splitbooleanEnable 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_modestringpercentage (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_recipientsobject[]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.
How split checkout works: the customer pays the full amount into your primary business wallet (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.
javascript
// 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.
json
// 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.
GET/checkout/sessions/:id

Retrieve a checkout session by ID to check payment status. This read endpoint is currently public for hosted checkout pages.

javascript
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"
GET/checkout/sessions/:id

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.

javascript
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");
}
POST/checkout/requests

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.

javascript
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);
POST/checkout/discovery/paybill

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.

javascript
// 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);
POST/checkout/sessions/:id/pay

Initiate payment on an existing checkout session. Use this for cashier-triggered STK push shortcuts.

javascript
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);
POST/checkout/sessions/:id/cancel

Cancel 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.

javascript
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"
json
// 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.

Hosted payment page: Every invoice includes 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.
POST/invoices

Create an invoice. Requires business_id when using API key auth. Amounts are in minor units (KES 500 = 50000).

ParameterTypeDescription
business_id *stringYour business ID (required with API key)
line_items *array[{ description, quantity, unit_price }] — unit_price in minor units
to stringPayer email, phone, or pay handle (optional)
due_date stringISO datetime when payment is due
status stringdraft | pending (default pending)
metadata objectYour subscription/plan IDs — returned on GET
external_reference stringe.g. sub_12345 from your billing system
recurring objectSubscription template — see below
javascript
// 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
json
{
  "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..."
}
GET/invoices

List invoices for your business. With API key auth, pass business_id (required).

bash
curl "https://api.paysasa.com/api/v1/invoices?business_id=biz_xxxxxxxxxxxxxxxx&status=pending" \
  -H "x-api-key: apk_live_a1b2c3d4.7f9e0a..."
GET/invoices/:id

Retrieve invoice status (paid, pending, amount_remaining). Poll after customer pays on the hosted page.

POST/invoices/:id/send

Email or SMS the invoice link to the customer (includes payment_url).

javascript
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.",
  }),
});
GET/invoices/:id/public

No authentication — used by the hosted payment page. Returns amount, line items, and status only.

POST/invoices/:id/checkout

No 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.

javascript
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/customersCreate billing customer
POST /billing/productsCreate product
POST /billing/pricesRecurring price (amount minor units, interval day|week|month|year)
POST /subscriptionsCreate subscription; returns payment_url for first invoice
POST /subscriptions/checkoutCreate a subscription and return a checkout URL
POST /subscriptions/:id/cancel|pause|resumeCancel, pause, or resume a subscription
GET /subscriptions/portalCustomer portal (subscription_id + email or phone)
bash
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();
Related webhooks include 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]
Paysasa logo

Fast, secure digital payments for modern businesses — with escrow and APIs when you need more than a button.

Frequently asked questions

Does Paysasa work for personal and business use?

Yes. Individuals use Paysasa for transfers and wallets; businesses add checkout, invoicing, bulk payouts, and team controls.

What are Gold and Platinum badges?

They are trust tiers on top of verification. Gold enables immediate checkout settlement for known brands. Platinum adds fee discounts, higher withdrawal limits, and priority support.

Can we integrate Paysasa into our product?

Yes. REST APIs and webhooks cover checkout, wallet actions, escrow events, and reconciliation-friendly updates.

© 2026 Paysasa. All rights reserved.

Built for trusted business payments across Africa.