Skip to main content

TypeScript SDK

npm install @palpluss/sdk
Requires Node.js 18+ or any modern runtime with the Fetch API (Bun, Deno, Edge runtimes).

Client initialisation

import { PalPluss } from '@palpluss/sdk';

const client = new PalPluss({
  apiKey: process.env.PALPLUSS_API_KEY,   // or pass directly
  timeout: 30_000,
  autoRetryOnRateLimit: true,
  maxRetries: 3,
});

Framework examples

Store the key in .env.local:
PALPLUSS_API_KEY=pk_live_xxxxxxxxxxxxxxxxxxxx
Create a shared client singleton so the HTTP connection pool is reused across requests:
// lib/palpluss.ts
import { PalPluss } from '@palpluss/sdk';

export const palpluss = new PalPluss({
  apiKey: process.env.PALPLUSS_API_KEY,
});
STK Push — Route Handler
// app/api/pay/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { palpluss } from '@/lib/palpluss';
import { PalPlussApiError } from '@palpluss/sdk';

export async function POST(req: NextRequest) {
  const { amount, phone, reference } = await req.json();

  try {
    const tx = await palpluss.stkPush({
      amount,
      phone,
      accountReference: reference,
      callbackUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/palpluss`,
    });

    return NextResponse.json({ transactionId: tx.transactionId });
  } catch (err) {
    if (err instanceof PalPlussApiError) {
      return NextResponse.json(
        { error: err.message, code: err.code },
        { status: err.httpStatus },
      );
    }
    throw err;
  }
}
Webhook handler
// app/api/webhooks/palpluss/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { parseWebhookPayload } from '@palpluss/sdk';

export async function POST(req: NextRequest) {
  // Acknowledge immediately — process in background
  const body = await req.text();

  let payload;
  try {
    payload = parseWebhookPayload(body);
  } catch {
    return NextResponse.json({ error: 'Invalid payload' }, { status: 400 });
  }

  // Fire and forget — do NOT await before responding
  void processWebhook(payload);

  return NextResponse.json({ received: true });
}

async function processWebhook(payload: ReturnType<typeof parseWebhookPayload>) {
  const { event_type, transaction } = payload;

  if (event_type === 'transaction.success') {
    // fulfil order, send confirmation SMS, etc.
    console.log(`Payment confirmed — receipt: ${transaction.mpesa_receipt}`);
  } else if (event_type === 'transaction.failed') {
    console.log(`Payment failed: ${transaction.result_desc}`);
  }
}

STK Push

const tx = await client.stkPush({
  amount: 500,
  phone: '254712345678',
  accountReference: 'ORDER-001',
  transactionDesc: 'Payment for order #001',
  callbackUrl: 'https://yourapp.com/webhooks/palpluss',
});

console.log(tx.transactionId);  // save this for polling / reconciliation
console.log(tx.status);         // "PENDING"

B2C Payout

Always supply an idempotencyKey so you can safely retry without double-paying:
const payout = await client.b2cPayout({
  amount: 1000,
  phone: '254712345678',
  reference: 'SALARY-JAN-2025-EMP001',
  description: 'January salary',
  idempotencyKey: 'salary-jan-2025-emp001',
});

console.log(payout.transactionId);
console.log(payout.status);  // "PENDING"

Service wallet

// Check balance
const balance = await client.getServiceBalance();
console.log(balance.availableBalance);  // e.g. 45000

// Top up via STK Push
const topup = await client.serviceTopup({
  amount: 10000,
  phone: '254712345678',
  idempotencyKey: 'topup-jan-2025-001',
});

Transactions

// Fetch one
const tx = await client.getTransaction('tx_01j8abc123');
console.log(tx.status);  // "SUCCESS"

// List with pagination
let cursor: string | null = null;

do {
  const page = await client.listTransactions({
    limit: 100,
    status: 'SUCCESS',
    type: 'STK',
    cursor,
  });

  for (const item of page.items) {
    console.log(item.transaction_id, item.status);
  }

  cursor = page.next_cursor ?? null;
} while (cursor !== null);

Payment wallet channels

// Create
const channel = await client.createChannel({
  type: 'PAYBILL',
  shortcode: '400200',
  name: 'Main Paybill',
  isDefault: true,
});

// Update
await client.updateChannel(channel.id, { name: 'Primary Paybill' });

// Delete
await client.deleteChannel(channel.id);

Error handling

import { PalPluss, PalPlussApiError, RateLimitError } from '@palpluss/sdk';

try {
  await client.stkPush({ amount: 500, phone: '254712345678' });
} catch (err) {
  if (err instanceof RateLimitError) {
    // err.retryAfter — seconds until the rate limit resets
    console.log(`Retry after ${err.retryAfter}s`);
  } else if (err instanceof PalPlussApiError) {
    // err.code       — machine-readable code  e.g. "INVALID_PHONE"
    // err.httpStatus — HTTP status code        e.g. 400
    // err.message    — human-readable message
    // err.requestId  — include in support requests
    console.log(`[${err.code}] ${err.message} (requestId: ${err.requestId})`);
  }
}

Common error codes

CodeHTTPMeaning
INVALID_PHONE400Phone number format not recognised
INSUFFICIENT_SERVICE_BALANCE402Not enough service tokens to cover the fee
INSUFFICIENT_FUNDS409B2C wallet balance below the requested amount
KYC_NOT_VERIFIED403Tenant KYC not approved — required for B2C
INVALID_API_KEY401API key is missing, invalid, or revoked
RATE_LIMIT_EXCEEDED42960 requests per minute limit exceeded