TypeScript SDK
Copy
Ask AI
npm install @palpluss/sdk
Client initialisation
Copy
Ask AI
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
- Next.js (App Router)
- NestJS
- Encore.ts
- Node.js (Express)
- Node.js (Fastify)
Store the key in Create a shared client singleton so the HTTP connection pool is reused across requests:STK Push — Route HandlerWebhook handler
.env.local:Copy
Ask AI
PALPLUSS_API_KEY=pk_live_xxxxxxxxxxxxxxxxxxxx
Copy
Ask AI
// lib/palpluss.ts
import { PalPluss } from '@palpluss/sdk';
export const palpluss = new PalPluss({
apiKey: process.env.PALPLUSS_API_KEY,
});
Copy
Ask AI
// 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;
}
}
Copy
Ask AI
// 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}`);
}
}
Install as a provider and inject via the module system:Payments serviceWebhook controller
Copy
Ask AI
// palpluss/palpluss.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PalPluss } from '@palpluss/sdk';
export const PALPLUSS_CLIENT = 'PALPLUSS_CLIENT';
@Module({
providers: [
{
provide: PALPLUSS_CLIENT,
inject: [ConfigService],
useFactory: (config: ConfigService) =>
new PalPluss({ apiKey: config.get<string>('PALPLUSS_API_KEY') }),
},
],
exports: [PALPLUSS_CLIENT],
})
export class PalPlussModule {}
Copy
Ask AI
// payments/payments.service.ts
import { Inject, Injectable, BadRequestException } from '@nestjs/common';
import { PalPluss, PalPlussApiError } from '@palpluss/sdk';
import { PALPLUSS_CLIENT } from '../palpluss/palpluss.module';
@Injectable()
export class PaymentsService {
constructor(
@Inject(PALPLUSS_CLIENT) private readonly palpluss: PalPluss,
) {}
async initiateStk(amount: number, phone: string, reference: string) {
try {
return await this.palpluss.stkPush({
amount,
phone,
accountReference: reference,
callbackUrl: process.env.WEBHOOK_BASE_URL + '/webhooks/palpluss',
});
} catch (err) {
if (err instanceof PalPlussApiError) {
throw new BadRequestException({
message: err.message,
code: err.code,
});
}
throw err;
}
}
}
Copy
Ask AI
// webhooks/webhooks.controller.ts
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
import { parseWebhookPayload } from '@palpluss/sdk';
@Controller('webhooks')
export class WebhooksController {
@Post('palpluss')
@HttpCode(200)
async handlePalPluss(@Body() body: unknown) {
const payload = parseWebhookPayload(JSON.stringify(body));
const { event_type, transaction } = payload;
if (event_type === 'transaction.success') {
// fulfil order
}
return { received: true };
}
}
Declare the PalPluss API key as a secret and create a service:Payments API endpointWebhook receiver
Copy
Ask AI
// payments/palpluss.ts
import { PalPluss } from '@palpluss/sdk';
import { secret } from 'encore.dev/config';
const palplussApiKey = secret('PalPlussApiKey');
// Lazily initialised so the secret is available at runtime
let _client: PalPluss | null = null;
export function getPalPlussClient(): PalPluss {
if (!_client) {
_client = new PalPluss({ apiKey: palplussApiKey() });
}
return _client;
}
Copy
Ask AI
// payments/payments.ts
import { api } from 'encore.dev/api';
import { getPalPlussClient } from './palpluss';
interface InitiatePaymentRequest {
amount: number;
phone: string;
reference: string;
}
interface InitiatePaymentResponse {
transactionId: string;
status: string;
}
export const initiatePayment = api(
{ expose: true, method: 'POST', path: '/payments/initiate' },
async (req: InitiatePaymentRequest): Promise<InitiatePaymentResponse> => {
const client = getPalPlussClient();
const tx = await client.stkPush({
amount: req.amount,
phone: req.phone,
accountReference: req.reference,
callbackUrl: 'https://your-app.encr.app/webhooks/palpluss',
});
return { transactionId: tx.transactionId, status: tx.status };
},
);
Copy
Ask AI
// webhooks/webhooks.ts
import { api, APIError } from 'encore.dev/api';
import { parseWebhookPayload } from '@palpluss/sdk';
export const palplussWebhook = api.raw(
{ expose: true, method: 'POST', path: '/webhooks/palpluss' },
async (req, resp) => {
const chunks: Buffer[] = [];
for await (const chunk of req) chunks.push(chunk);
const body = Buffer.concat(chunks).toString();
let payload;
try {
payload = parseWebhookPayload(body);
} catch {
resp.writeHead(400).end(JSON.stringify({ error: 'Invalid payload' }));
return;
}
if (payload.event_type === 'transaction.success') {
// fulfil order
}
resp.writeHead(200).end(JSON.stringify({ received: true }));
},
);
Copy
Ask AI
// src/index.ts
import express from 'express';
import { PalPluss, parseWebhookPayload, PalPlussApiError } from '@palpluss/sdk';
const app = express();
app.use(express.json());
const palpluss = new PalPluss({ apiKey: process.env.PALPLUSS_API_KEY });
// Initiate STK Push
app.post('/api/pay', async (req, res) => {
const { amount, phone, reference } = req.body;
try {
const tx = await palpluss.stkPush({
amount,
phone,
accountReference: reference,
callbackUrl: `${process.env.APP_URL}/webhooks/palpluss`,
});
res.json({ transactionId: tx.transactionId });
} catch (err) {
if (err instanceof PalPlussApiError) {
res.status(err.httpStatus).json({ error: err.message, code: err.code });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
// Webhook handler
app.post('/webhooks/palpluss', (req, res) => {
// Acknowledge before processing
res.status(200).json({ received: true });
try {
const payload = parseWebhookPayload(JSON.stringify(req.body));
if (payload.event_type === 'transaction.success') {
console.log(`Payment confirmed — receipt: ${payload.transaction.mpesa_receipt}`);
} else if (payload.event_type === 'transaction.failed') {
console.log(`Payment failed: ${payload.transaction.result_desc}`);
}
} catch (err) {
console.error('Invalid webhook payload', err);
}
});
app.listen(3000);
Copy
Ask AI
// src/server.ts
import Fastify from 'fastify';
import { PalPluss, parseWebhookPayload, PalPlussApiError } from '@palpluss/sdk';
const app = Fastify();
const palpluss = new PalPluss({ apiKey: process.env.PALPLUSS_API_KEY });
app.post<{ Body: { amount: number; phone: string; reference: string } }>(
'/api/pay',
async (request, reply) => {
const { amount, phone, reference } = request.body;
try {
const tx = await palpluss.stkPush({
amount,
phone,
accountReference: reference,
callbackUrl: `${process.env.APP_URL}/webhooks/palpluss`,
});
return { transactionId: tx.transactionId };
} catch (err) {
if (err instanceof PalPlussApiError) {
reply.status(err.httpStatus).send({ error: err.message, code: err.code });
}
throw err;
}
},
);
app.post('/webhooks/palpluss', async (request, reply) => {
reply.status(200).send({ received: true });
try {
const payload = parseWebhookPayload(JSON.stringify(request.body));
if (payload.event_type === 'transaction.success') {
// fulfil order
}
} catch (err) {
request.log.error({ err }, 'Invalid webhook payload');
}
});
app.listen({ port: 3000 });
STK Push
Copy
Ask AI
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 anidempotencyKey so you can safely retry without double-paying:
Copy
Ask AI
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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
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
| Code | HTTP | Meaning |
|---|---|---|
INVALID_PHONE | 400 | Phone number format not recognised |
INSUFFICIENT_SERVICE_BALANCE | 402 | Not enough service tokens to cover the fee |
INSUFFICIENT_FUNDS | 409 | B2C wallet balance below the requested amount |
KYC_NOT_VERIFIED | 403 | Tenant KYC not approved — required for B2C |
INVALID_API_KEY | 401 | API key is missing, invalid, or revoked |
RATE_LIMIT_EXCEEDED | 429 | 60 requests per minute limit exceeded |