> ## Documentation Index
> Fetch the complete documentation index at: https://docs.palpluss.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript SDK

> Use the PalPluss TypeScript SDK with Next.js, Node.js, NestJS, Encore.ts, and any other TypeScript or JavaScript framework.

# TypeScript SDK

<CodeGroup>
  ```bash npm theme={null}
  npm install @palpluss/sdk
  ```

  ```bash yarn theme={null}
  yarn add @palpluss/sdk
  ```

  ```bash pnpm theme={null}
  pnpm add @palpluss/sdk
  ```
</CodeGroup>

Requires Node.js 18+ or any modern runtime with the Fetch API (Bun, Deno, Edge runtimes).

***

## Client initialisation

```typescript theme={null}
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

<Tabs>
  <Tab title="Next.js (App Router)">
    Store the key in `.env.local`:

    ```bash theme={null}
    PALPLUSS_API_KEY=pk_live_xxxxxxxxxxxxxxxxxxxx
    ```

    Create a shared client singleton so the HTTP connection pool is reused across requests:

    ```typescript theme={null}
    // lib/palpluss.ts
    import { PalPluss } from '@palpluss/sdk';

    export const palpluss = new PalPluss({
      apiKey: process.env.PALPLUSS_API_KEY,
    });
    ```

    **STK Push — Route Handler**

    ```typescript theme={null}
    // 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`,
          channelId: 'your-payment-channel-id'
        });

        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**

    ```typescript theme={null}
    // 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}`);
      }
    }
    ```
  </Tab>

  <Tab title="NestJS">
    Install as a provider and inject via the module system:

    ```typescript theme={null}
    // 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 {}
    ```

    **Payments service**

    ```typescript theme={null}
    // 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',
            channelId: 'your-payment-channel-id'
          });
        } catch (err) {
          if (err instanceof PalPlussApiError) {
            throw new BadRequestException({
              message: err.message,
              code: err.code,
            });
          }
          throw err;
        }
      }
    }
    ```

    **Webhook controller**

    ```typescript theme={null}
    // 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 };
      }
    }
    ```
  </Tab>

  <Tab title="Encore.ts">
    Declare the PalPluss API key as a secret and create a service:

    ```typescript theme={null}
    // 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;
    }
    ```

    **Payments API endpoint**

    ```typescript theme={null}
    // 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',
          channelId: 'your-payment-channel-id'
        });

        return { transactionId: tx.transactionId, status: tx.status };
      },
    );
    ```

    **Webhook receiver**

    ```typescript theme={null}
    // 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 }));
      },
    );
    ```
  </Tab>

  <Tab title="Node.js (Express)">
    ```typescript theme={null}
    // 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`,
          channelId: 'your-payment-channel-id'
        });

        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);
    ```
  </Tab>

  <Tab title="Node.js (Fastify)">
    ```typescript theme={null}
    // 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`,
            channelId: 'your-payment-channel-id'
          });

          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 });
    ```
  </Tab>
</Tabs>

***

## STK Push

```typescript theme={null}
const tx = await client.stkPush({
  amount: 500,
  phone: '254712345678',
  accountReference: 'ORDER-001',
  transactionDesc: 'Payment for order #001',
  callbackUrl: 'https://yourapp.com/webhooks/palpluss',
  channelId: 'your-payment-channel-id'
});

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:

```typescript theme={null}
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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
// 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

```typescript theme={null}
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         |
