npm package: @pspline/sdk
Description: Official Node.js SDK for PSP Line Merchant API — crypto invoices, payments, wallets
Author: PSP Line
Latest version: 1.0.0

# PSP Line Node.js SDK

[![CI](https://github.com/pspline/node-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/pspline/node-sdk/actions)
[![npm](https://img.shields.io/npm/v/@pspline/sdk.svg)](https://www.npmjs.com/package/@pspline/sdk)
[![Node](https://img.shields.io/node/v/@pspline/sdk.svg)](https://www.npmjs.com/package/@pspline/sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)

Official Node.js/TypeScript SDK for the [PSP Line Merchant API](https://app.pspline.com/docs/redoc) — create crypto invoices, check payment statuses, and verify webhook signatures.

## Features

- **TypeScript-first** — full type definitions out of the box
- **Zero dependencies** — uses native `fetch` (Node.js 18+)
- **ESM + CommonJS** — works everywhere
- **Async/await** — all methods return `Promise`
- **Auto-retry** — exponential backoff on 5xx and network errors
- **Webhook verification** — HMAC-SHA256 signature validation

## Requirements

- Node.js 18 or higher

## Installation

```bash
npm install @pspline/sdk
```

## Quick Start

```typescript
import PspLine from '@pspline/sdk';

const psp = new PspLine({
  merchantId: 232,
  publicKey: 'your-public-key',
  privateKey: 'your-private-key',
});

// Create a crypto invoice
const invoice = await psp.invoices.create({
  serviceId: 885,
  walletId: 52205,
  currency: 'USDT_TRON',
  estimatedAmount: 15_000_000,  // 15 USDT (precision 6)
  callbackUrl: 'https://your-site.test/webhook',
  confirmUrl: 'https://your-site.test/success',
  fields: { userId: 'customer-123' },
});

console.log(invoice.walletAddress);  // Crypto address
console.log(invoice.verifyUrl);      // Web form URL
console.log(invoice.status);         // 0 = waiting
```

### CommonJS

```javascript
const { PspLine } = require('@pspline/sdk');

const psp = new PspLine({
  merchantId: 232,
  publicKey: 'your-public-key',
  privateKey: 'your-private-key',
});
```

## Configuration

| Option | Type | Default | Description |
|---|---|---|---|
| `merchantId` | `number` | *required* | Merchant ID |
| `publicKey` | `string` | *required* | Public key for authentication |
| `privateKey` | `string` | *required* | Private key for HMAC signature |
| `baseUrl` | `string` | `https://app.pspline.com/api/cpci/v1` | API base URL |
| `timeout` | `number` | `30000` | Request timeout in milliseconds |
| `retries` | `number` | `2` | Retry count on 5xx/network errors |
| `httpClient` | `HttpClient` | `FetchHttpClient` | Custom HTTP transport |

### Custom Base URL

```typescript
const psp = new PspLine({
  merchantId: 232,
  publicKey: process.env.PSPLINE_PUBLIC_KEY!,
  privateKey: process.env.PSPLINE_PRIVATE_KEY!,
  baseUrl: process.env.PSPLINE_BASE_URL!,
});
```

## API Reference

### Invoices

#### Create Invoice

```typescript
const invoice = await psp.invoices.create({
  // Required
  serviceId: 885,               // Service ID
  walletId: 52205,              // Wallet to credit
  currency: 'USDT_TRON',       // Currency code
  estimatedAmount: 15_000_000,  // Amount in min units

  // Optional
  externalId: 'order-789',     // Your system ID
  callbackUrl: 'https://...',
  confirmUrl: 'https://...',
  fields: { userId: 'u001' },
});
```

**Response type:**

```typescript
interface Invoice {
  id: string;                    // Invoice UUID
  externalId: string | null;     // Your system ID
  merchantId: number;
  serviceId: number;
  walletId: number;
  currency: string;              // e.g. 'USDT_TRON'
  estimatedAmount: number;       // Expected amount
  receivedAmount: number;        // Actually received
  verifyUrl: string | null;      // Web payment form URL
  walletAddress: string | null;  // Crypto address
  walletMemo: string | null;     // Memo (XRP, TON, etc.)
  status: number;                // Status code
  statusLabel: string;           // Human-readable status
  fields: Record<string, unknown> | null;

  // Computed helpers
  isSuccessful: boolean;
  isFailed: boolean;
  isPending: boolean;
  isFinal: boolean;

  raw: InvoiceRaw;              // Original API data
}
```

#### Find Invoice

```typescript
// By PSP Line invoice ID
const invoice = await psp.invoices.find({ id: 'invoice-uuid' });

// By your external ID
const invoice = await psp.invoices.find({ externalId: 'order-789' });

// Check status
if (invoice.isSuccessful) {
  // Payment completed — deliver goods
} else if (invoice.isPending) {
  // Still waiting — check again later
} else if (invoice.isFailed) {
  // Payment failed or timed out
}
```

### Invoice Statuses

```typescript
import { InvoiceStatus } from '@pspline/sdk';

// Final
InvoiceStatus.SUCCESS    // 1
InvoiceStatus.FAILED     // 2
InvoiceStatus.CANCELLED  // 3
InvoiceStatus.TIMEOUT    // 4

// Non-final
InvoiceStatus.WAITING           // 0
InvoiceStatus.CHECKING          // 10
InvoiceStatus.PARTIAL_SUCCESS   // 11
InvoiceStatus.UNCOMPLETED_MERGE // 12
```

### Cryptocurrency Amounts

PSP Line uses integer amounts in minimum currency units:

| Currency | Precision | Example |
|---|---|---|
| USDT_TRON | 6 | 15 USDT → `15_000_000` |
| BTC | 8 | 0.001 BTC → `100_000` |
| ETH | 18 | 0.027 ETH → `27_000_000_000_000_000` |

## Webhook Verification

PSP Line sends POST callbacks with HMAC signatures. Always verify them:

```typescript
import { verifyWebhook } from '@pspline/sdk/webhook';

// Express example
app.post('/webhook', (req, res) => {
  const isValid = verifyWebhook({
    body: req.rawBody,             // raw string, not parsed JSON
    signature: req.headers['x-signature'] as string,
    timestamp: req.headers['timestamp'] as string,
    privateKey: process.env.PSPLINE_PRIVATE_KEY!,
    tolerance: 120,                // max age in seconds (default)
  });

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Process webhook safely...
  res.json({ status: 'ok' });
});
```

**Important:** You need the raw request body for signature verification. With Express:

```typescript
app.use(express.json({
  verify: (req: any, _res, buf) => {
    req.rawBody = buf.toString();
  },
}));
```

## Error Handling

```typescript
import {
  PspLineError,
  AuthenticationError,
  ApiError,
  ValidationError,
  NetworkError,
} from '@pspline/sdk';

try {
  const invoice = await psp.invoices.create({ ... });
} catch (err) {
  if (err instanceof AuthenticationError) {
    // Invalid keys, timestamp, or signature (API codes 2001-2006)
    console.error('Auth error:', err.message);

  } else if (err instanceof ValidationError) {
    // Missing or invalid parameters (client-side)
    console.error('Validation:', err.message, err.field);

  } else if (err instanceof ApiError) {
    // PSP Line API returned an error
    console.error(`API error [${err.apiCode}]: ${err.message}`);
    console.error('HTTP:', err.httpCode);
    console.error('Raw:', err.rawBody);

  } else if (err instanceof NetworkError) {
    // Connection timeout, DNS failure, etc.
    console.error('Network error:', err.message);

  } else if (err instanceof PspLineError) {
    // Base catch-all
    console.error('SDK error:', err.message);
  }
}
```

### Error Hierarchy

```
PspLineError (base)
├── NetworkError            — fetch/connection failures
├── ValidationError         — client-side input validation
└── ApiError                — API returned error code
    └── AuthenticationError — auth errors (2001-2006)
```

## Framework Examples

### Express

```typescript
import express from 'express';
import PspLine from '@pspline/sdk';

const psp = new PspLine({
  merchantId: Number(process.env.PSPLINE_MERCHANT_ID),
  publicKey: process.env.PSPLINE_PUBLIC_KEY!,
  privateKey: process.env.PSPLINE_PRIVATE_KEY!,
});

const app = express();

app.post('/create-payment', async (req, res) => {
  const invoice = await psp.invoices.create({
    serviceId: 885,
    walletId: 52205,
    currency: 'USDT_TRON',
    estimatedAmount: req.body.amount,
    callbackUrl: `${process.env.BASE_URL}/webhook`,
  });

  res.json({
    paymentUrl: invoice.verifyUrl,
    walletAddress: invoice.walletAddress,
    invoiceId: invoice.id,
  });
});
```

### NestJS

```typescript
import { Injectable } from '@nestjs/common';
import PspLine from '@pspline/sdk';

@Injectable()
export class PaymentService {
  private readonly psp: PspLine;

  constructor() {
    this.psp = new PspLine({
      merchantId: Number(process.env.PSPLINE_MERCHANT_ID),
      publicKey: process.env.PSPLINE_PUBLIC_KEY!,
      privateKey: process.env.PSPLINE_PRIVATE_KEY!,
    });
  }

  async createInvoice(amount: number) {
    return this.psp.invoices.create({
      serviceId: 885,
      walletId: 52205,
      currency: 'USDT_TRON',
      estimatedAmount: amount,
    });
  }
}
```

### Next.js API Route

```typescript
// app/api/payment/route.ts
import PspLine from '@pspline/sdk';

const psp = new PspLine({
  merchantId: Number(process.env.PSPLINE_MERCHANT_ID),
  publicKey: process.env.PSPLINE_PUBLIC_KEY!,
  privateKey: process.env.PSPLINE_PRIVATE_KEY!,
});

export async function POST(request: Request) {
  const { amount } = await request.json();

  const invoice = await psp.invoices.create({
    serviceId: 885,
    walletId: 52205,
    currency: 'USDT_TRON',
    estimatedAmount: amount,
  });

  return Response.json({ invoiceId: invoice.id, paymentUrl: invoice.verifyUrl });
}
```

## Development

```bash
# Install dependencies
npm install

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Type check
npm run typecheck

# Lint
npm run lint

# Format
npm run format

# Build
npm run build
```

## Contributing

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/my-feature`
3. Write tests for your changes
4. Ensure all checks pass: `npm test && npm run lint && npm run typecheck`
5. Submit a Pull Request

## Security

If you discover a security vulnerability, please email security@pspline.com instead of creating a public issue.

## License

MIT — see [LICENSE](LICENSE) for details.