npm package: @pok-pay/server-sdk
Description: POK Pay official Node.js/TypeScript server SDK
Latest version: 0.1.0

# @pok-pay/server-sdk

Official POK Pay Node.js/TypeScript server SDK — full integration kit with API client, webhook handler, NestJS module, and Prisma/TypeORM schemas.

## Installation

```bash
npm install @pok-pay/server-sdk
```

Requires Node.js 18+.

## Environment Variables

```env
POKPAY_ENV=staging           # 'staging', 'production', or 'live' (alias for production)
POKPAY_KEY_ID=your_key_id
POKPAY_KEY_SECRET=your_key_secret
POKPAY_MERCHANT_ID=your_merchant_id_uuid
```

| Env | API URL | App URL |
|---|---|---|
| `staging` | `https://api-staging.pokpay.io` | `https://app-staging.pokpay.io` |
| `production` | `https://api.pokpay.io` | `https://app.pokpay.io` |
| `live` | `https://api.pokpay.io` | `https://app.pokpay.io` |

## Quick Start — Express

```typescript
import express from 'express';
import { PokPayClient, handlePokWebhook } from '@pok-pay/server-sdk';

const pokConfig = {
  env: process.env.POKPAY_ENV as 'staging' | 'production',
  keyId: process.env.POKPAY_KEY_ID!,
  keySecret: process.env.POKPAY_KEY_SECRET!,
  merchantId: process.env.POKPAY_MERCHANT_ID!,
};

const client = new PokPayClient(pokConfig);
const app = express();
app.use(express.json());

// 1. Create a payment order (requires your app's auth)
app.post('/payments/create-order', authMiddleware, async (req, res) => {
  const { amount, currencyCode, orderId } = req.body;
  const sdkOrderId = await client.createOrder({
    amount,
    currencyCode,
    webhookUrl: `${process.env.API_URL}/webhooks/pok?orderId=${orderId}`,
    redirectUrl: `${process.env.APP_URL}/payment/success`,
  });
  // Save sdkOrderId to your DB order record here
  res.json({ sdkOrderId });
});

// 2. Setup tokenized 3DS for saved cards (user MUST be authenticated)
app.post('/payments/setup-tokenized-3ds', authMiddleware, async (req, res) => {
  const { cardId, sdkOrderId } = req.body;
  const payerAuthentication = await client.setupTokenized3DS(cardId, sdkOrderId);
  res.json({ payerAuthentication });
});

// 3. Webhook handler (public — no auth)
// POK does not provide webhook signatures. This handler cross-verifies
// with the POK API before marking orders complete.
app.post('/webhooks/pok', async (req, res) => {
  const result = await handlePokWebhook(pokConfig, req.body, {
    async getOrder(orderId) {
      // Return your order from DB (must include pokOrderId and userId)
      return db.orders.findUnique({ where: { id: orderId } });
    },
    async markOrderCompleted(orderId) {
      await db.orders.update({ where: { id: orderId }, data: { status: 'COMPLETED' } });
    },
    async saveCardToken(userId, pokCardId) {
      // Upsert — do not throw, card saving must not break payment confirmation
      await db.savedCard.upsert({
        where: { userId_pokCardId: { userId, pokCardId } },
        create: { userId, pokCardId },
        update: {},
      });
    },
  });
  res.status(200).json(result); // Always return 200 to POK
});
```

## Quick Start — NestJS

```typescript
// app.module.ts
import { PokPayModule } from '@pok-pay/server-sdk/nestjs';

@Module({
  imports: [
    PokPayModule.forRoot({
      env: process.env.POKPAY_ENV as 'staging' | 'production',
      keyId: process.env.POKPAY_KEY_ID!,
      keySecret: process.env.POKPAY_KEY_SECRET!,
      merchantId: process.env.POKPAY_MERCHANT_ID!,
    }),
  ],
})
export class AppModule {}

// payment.controller.ts
import { PokPayService } from '@pok-pay/server-sdk/nestjs';

@Controller('payments')
export class PaymentController {
  constructor(private readonly pokPay: PokPayService) {}

  @Post('create-order')
  @UseGuards(JwtAuthGuard)
  async createOrder(@Body() body: CreateOrderDto) {
    const sdkOrderId = await this.pokPay.createOrder({
      amount: body.amount,
      currencyCode: body.currencyCode,
      webhookUrl: `${process.env.API_URL}/webhooks/pok?orderId=${body.orderId}`,
      redirectUrl: `${process.env.APP_URL}/payment/success`,
    });
    return { sdkOrderId };
  }

  @Post('setup-tokenized-3ds')
  @UseGuards(JwtAuthGuard)  // Must be authenticated
  async setup3DS(@Body() body: Setup3DSDto) {
    const payerAuthentication = await this.pokPay.setupTokenized3DS(
      body.cardId,
      body.sdkOrderId,
    );
    return { payerAuthentication };
  }
}
```

## Database Schema

### Prisma

Copy `node_modules/@pok-pay/server-sdk/src/schema/saved-card.prisma` into your `schema.prisma`.

### TypeORM

Copy `node_modules/@pok-pay/server-sdk/src/schema/saved-card.typeorm.ts` into your entities directory and adjust the User relation import.

## Payment Flow

```
1. Backend: createOrder() → sdkOrderId
2a. Card:   Frontend renders POK card form with sdkOrderId
2b. Wallet: Frontend displays QR code at {APP_URL}/sdk-orders/{sdkOrderId}
            (user scans with POK app)
2c. Saved card: Frontend calls POST /payments/setup-tokenized-3ds
                → payerAuthentication → frontend completes 3DS
3. User pays
4. POK → POST /webhooks/pok?orderId={your-internal-id}
5. handlePokWebhook() cross-verifies with POK API, marks order COMPLETED,
   saves card token (if applicable and user is logged in)
```

## API Reference

### `new PokPayClient(config)`

| Param | Type | Description |
|---|---|---|
| `env` | `'staging' \| 'production' \| 'live'` | Resolves API URLs automatically (`'live'` is an alias for `'production'`) |
| `keyId` | `string` | POK API key ID |
| `keySecret` | `string` | POK API key secret |
| `merchantId` | `string` | POK merchant UUID |

### `client.createOrder(params)` → `Promise<string>`

Returns `sdkOrderId`. Obtains a fresh access token on each call (tokens are short-lived).

### `client.getOrderStatus(sdkOrderId)` → `Promise<{ isCompleted: boolean, status: string, canBeCaptured: boolean }>`

Cross-verify order completion with POK's API. `canBeCaptured` is `true` only for orders
created with `autoCapture: false` where payment has been frozen but not yet captured.

### `client.getCardDetails(cardId)` → `Promise<PokCardDetails>`

**Always call once per card.** POK does not guarantee response ordering for multi-card requests.

### `client.setupTokenized3DS(cardId, sdkOrderId)` → `Promise<unknown>`

Returns `payerAuthentication` blob. Pass it unmodified to the frontend `setUpCardTokenPayment()` call.

### `handlePokWebhook(config, body, deps)` → `Promise<{ ok: boolean }>`

Idempotent webhook handler. Cross-verifies with POK API before confirming. Card saving is best-effort (errors are swallowed).

### `client.captureOrder(sdkOrderId)` → `Promise<Record<string, unknown>>`

Capture a frozen payment. Only valid when `canBeCaptured` is `true` (orders created
with `autoCapture: false`).
Endpoint: `POST /merchants/{merchantId}/sdk-orders/{sdkOrderId}/capture`.

### `client.cancelOrder(sdkOrderId)` → `Promise<Record<string, unknown>>`

Release frozen funds without capturing. Use to cancel an `autoCapture: false` order.
Endpoint: `POST /merchants/{merchantId}/sdk-orders/{sdkOrderId}/cancel`.

### `client.refundOrder(sdkOrderId, amount?)` → `Promise<Record<string, unknown>>`

Refund a captured order. Omit `amount` for a full refund; supply it for a partial
refund. Only valid when `isRefundable` is `true` on the order.
Endpoint: `POST /merchants/{merchantId}/sdk-orders/{sdkOrderId}/refund`.

### `handleCaptureOrder(config, input)` / `handleCancelOrder` / `handleRefundOrder`

Framework-agnostic route handlers — same pattern as `handleCreateOrder`. Wire into
Express/NestJS/etc. with your own auth middleware.

## Security Notes

- Never expose `keyId`, `keySecret`, or `merchantId` to the browser or mobile app
- Access tokens are obtained server-side only and never returned to the client
- POK does not provide webhook HMAC signatures; this SDK uses API cross-verification instead
- The `setup-tokenized-3ds` endpoint must be protected by your authentication middleware
- Card token saving (`saveCardToken`) must never throw — wrap your DB call in try/catch