Guide · 08 / 10

SDKs

Quickstarts for Web, iOS, and Android.

WasaaPay ships three client SDKs. Their signatures below are taken directly from the SDK sources.

SDKPackageModel
Web / Node@wasaapay/api-clientTyped server-side client for the full REST API (you sign requests).
iOSWasaaPaySDK (SPM)Drop-in checkout UI; you pass a publishable key + amount.
Androidcom.wasaapay:checkout-sdkDrop-in Compose checkout screen; you pass a publishable key + amount.

Two different auth models — read this first.

  • The Web client authenticates like the raw API: your full merchant API key plus an HMAC signer you inject. It runs server-side (the secret must never reach a browser).
  • The mobile SDKs authenticate with a publishable key (pk_…) and build the collection themselves from the amount/currency/reference you pass — they do not consume a checkout-session client_token, and they never carry your secret. Publishable-key auth is the mobile SDKs' documented design; if you are wiring the mobile SDKs against your own backend, confirm publishable-key acceptance end-to-end before going live (the server gateway documented in this guide authenticates with the full API key + HMAC).
  • None of the SDKs expose the checkout-session endpoints yet. Drive those over raw HTTP — see Collections → Hosted checkout.

Web / Node — @wasaapay/api-client

A fully typed client covering every REST operation (collections, refunds, transactions, sub-merchants, wallets, split rules, payouts, webhooks, FX). It returns a Result union and never throws on API errors.

Availability. This package is currently an internal workspace package ("private": true, version 0.1.0) — it is not published to the public npm registry yet. Inside the WasaaPay monorepo you depend on it as "@wasaapay/api-client": "workspace:*". Treat the npm add line below as the intended public form once it's published.

// package.json (monorepo today)
"dependencies": { "@wasaapay/api-client": "workspace:*" }

Initialize with an API key + signer

The client does not compute HMAC itself — you inject a signer callback, so the secret stays in your code, not in the bundle. The signer computes the same `${timestamp}.${body}` HMAC described in Authentication.

import { createHmac } from 'node:crypto';
import {
  WasaaPayClient, SANDBOX_BASE_URL,
  type RequestSigner, type SignatureInput,
} from '@wasaapay/api-client';

// The signing secret is the part of your key after the first "."
const createSigner = (secret: string): RequestSigner =>
  ({ timestamp, body }: SignatureInput) =>
    createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');

const client = new WasaaPayClient({
  baseUrl: SANDBOX_BASE_URL,             // or PRODUCTION_BASE_URL, or your own
  auth: {
    mode: 'api-key',
    apiKey: process.env.WASAAPAY_API_KEY!,      // full "wp_<prefix>.<secret>"
    signer: createSigner(process.env.WASAAPAY_API_SECRET!), // the <secret> part
  },
});

The client automatically sets X-WasaaPay-Key, X-WasaaPay-Timestamp, and X-WasaaPay-Signature on every request, and attaches an auto-generated Idempotency-Key to mutating calls (override via options.idempotencyKey).

Create a collection

const result = await client.createCollection({
  amount: 150000,            // minor units — KES 1,500.00
  currency: 'KES',
  rail: 'mobile_money',
  payer_phone: '254700000001',
  reference: 'order_10293',
});

if (result.ok) {
  console.log(result.data.id, result.data.status);   // Transaction
} else {
  console.error(result.error.code, result.error.httpStatus, result.error.requestId);
  // Retry the SAME logical attempt with the returned idempotency key:
  // await client.createCollection(body, { idempotencyKey: result.idempotencyKey });
}

Other common calls

await client.getCollection(id);
await client.createRefund(collectionId, { amount: 50000, reason: 'partial' });
await client.createPayout({ wallet_id, destination_type: 'mobile_money', destination_ref, amount: 500000, currency: 'KES' });
await client.listTransactions({ status: 'success', limit: 50 });
await client.createWebhookEndpoint({ url, subscribed_events: ['transaction.success'] });

Pagination helpers make cursor loops trivial:

import { createPager, collectAllPages } from '@wasaapay/api-client';

const pager = createPager((cursor) => client.listTransactions({ cursor }));
const first = await pager.next();
if (pager.hasMore) { const second = await pager.next(); }

const all = await collectAllPages((cursor) => client.listPayouts({ cursor }));

Checkout sessions: @wasaapay/api-client has no createCheckoutSession method yet. Use raw HTTP for the hosted-checkout flow.


iOS — WasaaPaySDK

A drop-in checkout that presents WasaaPay's payment UI and returns a typed result. Requires iOS 15+.

Install (Swift Package Manager)

Add the WasaaPaySDK package and link the WasaaPaySDK product.

// In Package.swift
dependencies: [
    .package(url: "https://github.com/web-masters-ke/wasaapay-ios-sdk.git", from: "1.0.0"),
],
targets: [
    .target(name: "YourApp", dependencies: [
        .product(name: "WasaaPaySDK", package: "wasaapay-ios-sdk"),
    ]),
]

Publishing caveat. The SDK is not yet published with a tagged SPM release, and its Package.swift currently lives in the repo's WasaaPaySDK/ subdirectory (SPM expects a root manifest). The sample app consumes it by local path. Confirm the exact package URL / version with WasaaPay before wiring the line above; until then, add it via File → Add Packages… → Add Local… pointing at a checkout of the SDK.

Present the checkout (UIKit)

import WasaaPaySDK

let checkout = WasaaPayCheckout(publishableKey: "pk_live_…")   // environment: .production by default

checkout.present(
    from: viewController,
    amount: 150_000,          // Int64, integer minor units — KES 1,500.00
    currency: "KES",
    reference: "order_10293"
) { result in
    switch result {
    case .success(let transaction):
        print("Paid:", transaction.id, transaction.providerReference ?? "")
    case .failure(let error):
        print("Failed:", error.errorCode ?? "-", error.requestID ?? "-")
    case .cancelled:
        print("Shopper dismissed checkout")
    }
}

The full initializer is WasaaPayCheckout(publishableKey:environment:configuration:). The result type is WasaaPayResult with three cases: .success(Transaction), .failure(WasaaPayError), and .cancelled. WasaaPayError exposes errorCode (the backend catalog code) and requestID for support.

SwiftUI

WasaaPayCheckoutView(
    checkout: checkout,
    amount: 150_000,
    currency: "KES",
    reference: "order_10293"
) { result in
    // same WasaaPayResult switch as above
}

Environment toggle

WasaaPayCheckout(publishableKey: "pk_test_…", environment: .sandbox)
// .production → https://api.wasaapay.com/v1
// .sandbox    → https://sandbox.api.wasaapay.com/v1
// .custom(baseURL:) → your own gateway; the URL must include /v1

For a plain-HTTP local gateway, also relax pinning: WasaaPayCheckout(publishableKey:, environment: .custom(baseURL:), configuration: CheckoutConfiguration(pinning: .systemTrustOnly)).


Android — com.wasaapay:checkout-sdk

A Compose checkout screen (WasaaPayCheckoutScreen) plus a WasaaPayCheckout config object. Requires minSdk 24.

Install (Gradle)

Resolve from Maven Central + Google:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories { google(); mavenCentral() }
}
// app/build.gradle.kts
dependencies {
    implementation("com.wasaapay:checkout-sdk:1.0.0")
}

Present the checkout (Compose)

WasaaPayCheckout is a configuration holder; presentation is done through the WasaaPayCheckoutScreen composable, which you embed in a bottom sheet, dialog, or destination.

import com.wasaapay.checkout.WasaaPayCheckout
import com.wasaapay.checkout.WasaaPayCheckoutScreen
import com.wasaapay.checkout.CheckoutResult

val checkout = WasaaPayCheckout(publishableKey = "pk_live_…")  // environment defaults to Production

WasaaPayCheckoutScreen(
    checkout = checkout,
    amountMinorUnits = 150_000,   // Long, integer minor units — KES 1,500.00
    currency = "KES",
    reference = "order_10293",
) { result ->
    when (result) {
        is CheckoutResult.Success -> println("Paid: ${result.transaction.id}")
        is CheckoutResult.Failure -> println("Failed: ${result.error.errorCode} ${result.error.requestId}")
        CheckoutResult.Cancelled  -> println("Shopper dismissed checkout")
    }
}

CheckoutResult is a sealed interface with Success(transaction), Failure(error), and the Cancelled object. error (a WasaaPayException) exposes errorCode and requestId.

Environment toggle

import com.wasaapay.checkout.core.WasaaPayEnvironment
import com.wasaapay.checkout.security.CertificatePinningConfiguration

// Hosted sandbox:
WasaaPayCheckout(publishableKey = "pk_test_…", environment = WasaaPayEnvironment.Sandbox)

// Local gateway from the emulator (10.0.2.2 = host localhost), plain HTTP:
WasaaPayCheckout(
    publishableKey = "pk_test_…",
    environment = WasaaPayEnvironment.Custom("http://10.0.2.2:3000/v1"),
    configuration = CheckoutConfiguration(pinning = CertificatePinningConfiguration.SYSTEM_TRUST_ONLY),
)

WasaaPayEnvironment is Production (https://api.wasaapay.com/v1), Sandbox (https://sandbox.api.wasaapay.com/v1), or Custom(baseUrl) (must include /v1).

Production card tokenization. On both mobile SDKs the default card tokenizer is a sandbox placeholder. A production build must inject the certified card processor's tokenizer via CheckoutConfiguration(cardTokenizer = …) — never let raw card data reach your app.