<!-- canonical: https://docs.costplus.io/docs/sdks/node -->

> Official Node.js SDK for the Cost+ payment gateway
Official Node.js/TypeScript SDK for the Cost+ payment gateway. Simplifies the HPP (Hosted Payment Page) redirect flow, HMAC payload signing, and webhook verification.

## Features

- **Zero dependencies** — uses only built-in Node.js `crypto` and `fetch`
- Full TypeScript types with declaration maps
- HMAC-SHA256 signature generation and constant-time verification
- Automatic snake_case/camelCase mapping between the API and SDK
- Webhook parsing + API-based order verification
- Tested across Node.js 18, 20, and 22

## Requirements

- Node.js 18 or later (uses built-in `fetch`)
- A Cost+ merchant account — [dashboard.costplus.io](https://dashboard.costplus.io/)

## Installation

```bash
npm install nopayn-node-sdk
```

## Quick Start

### 1. Initialise the Client

```typescript

const nopayn = new NoPaynClient({
  apiKey: 'your-api-key',      // From the NoPayn merchant portal
  merchantId: 'your-project',  // Your project/merchant ID
});
```

### 2. Create a Payment and Redirect to the HPP

```typescript
const result = await nopayn.generatePaymentUrl({
  amount: 1295,            // €12.95 in cents
  currency: 'EUR',
  merchantOrderId: 'ORDER-001',
  description: 'Premium Widget',
  returnUrl: 'https://shop.example.com/success',
  failureUrl: 'https://shop.example.com/failure',
  webhookUrl: 'https://shop.example.com/webhook',
  locale: 'en-GB',
  expirationPeriod: 'PT30M',
});

// Redirect the customer
// result.orderUrl   → HPP (customer picks payment method)
// result.paymentUrl → direct link to the first transaction's payment method
// result.signature  → HMAC-SHA256 for verification
// result.orderId    → NoPayn order UUID
```

### 3. Handle the Webhook

```typescript
app.post('/webhook', async (req, res) => {
  const verified = await nopayn.verifyWebhook(JSON.stringify(req.body));

  console.log(verified.order.status); // 'completed', 'cancelled', etc.
  console.log(verified.isFinal);      // true when the order won't change

  if (verified.order.status === 'completed') {
    // Fulfil the order
  }

  res.sendStatus(200);
});
```

## API Reference

### `new NoPaynClient(config)`

| Parameter | Type | Required | Default |
|---|---|---|---|
| `apiKey` | `string` | Yes | — |
| `merchantId` | `string` | Yes | — |
| `baseUrl` | `string` | No | `https://api.nopayn.co.uk` |

### `client.createOrder(params)`

Creates an order via `POST /v1/orders/`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `amount` | `number` | Yes | Amount in smallest currency unit (cents) |
| `currency` | `string` | Yes | ISO 4217 code (`EUR`, `GBP`, `USD`, `NOK`, `SEK`) |
| `merchantOrderId` | `string` | No | Your internal order reference |
| `description` | `string` | No | Order description |
| `returnUrl` | `string` | No | Redirect after successful payment |
| `failureUrl` | `string` | No | Redirect on cancel/expiry/error |
| `webhookUrl` | `string` | No | Async status-change notifications |
| `locale` | `string` | No | HPP language (`en-GB`, `de-DE`, `nl-NL`, etc.) |
| `paymentMethods` | `PaymentMethod[]` | No | Filter HPP methods |
| `expirationPeriod` | `string` | No | ISO 8601 duration (`PT30M`) |

**Available payment methods:** `credit-card`, `apple-pay`, `google-pay`, `vipps-mobilepay`

### `client.getOrder(orderId)`

Fetches order details via `GET /v1/orders/{id}/`.

### `client.createRefund(orderId, amount, description?)`

Issues a full or partial refund via `POST /v1/orders/{id}/refunds/`.

### `client.generatePaymentUrl(params)`

Convenience method that creates an order and returns:

```typescript
{
  orderId: string;     // NoPayn order UUID
  orderUrl: string;    // HPP URL
  paymentUrl?: string; // Direct payment URL (first transaction)
  signature: string;   // HMAC-SHA256 of amount:currency:orderId
  order: Order;        // Full order object
}
```

### `client.generateSignature(amount, currency, orderId)`

Generates an HMAC-SHA256 hex signature. The canonical message is `${amount}:${currency}:${orderId}`, signed with the API key.

### `client.verifySignature(amount, currency, orderId, signature)`

Constant-time verification of an HMAC-SHA256 signature. Returns `true` if valid.

### `client.verifyWebhook(rawBody)`

Parses the webhook body, then calls `GET /v1/orders/{id}/` to verify the actual status. Returns:

```typescript
{
  orderId: string;
  order: Order;     // Verified via API
  isFinal: boolean; // true for completed/cancelled/expired/error
}
```

### `client.parseWebhookBody(rawBody)`

Parses and validates a webhook body without calling the API.

### Standalone HMAC Utilities

```typescript

const sig = generateSignature('your-api-key', 1295, 'EUR', 'order-uuid');
const ok  = verifySignature('your-api-key', 1295, 'EUR', 'order-uuid', sig);
```

## Error Handling

```typescript

try {
  await nopayn.createOrder({ amount: 100, currency: 'EUR' });
} catch (err) {
  if (err instanceof NoPaynApiError) {
    console.error(err.statusCode); // 401, 400, etc.
    console.error(err.errorBody);  // Raw API error response
  } else if (err instanceof NoPaynError) {
    console.error(err.message);    // Network or parsing error
  }
}
```

## Order Statuses

| Status | Final? | Description |
|---|---|---|
| `new` | No | Order created |
| `processing` | No | Payment in progress |
| `completed` | Yes | Payment successful — deliver the goods |
| `cancelled` | Yes | Payment cancelled by customer |
| `expired` | Yes | Payment link timed out |
| `error` | Yes | Technical failure |

## Webhook Best Practices

1. **Always verify via the API** — the webhook payload only contains the order ID, never the status. The SDK's `verifyWebhook()` does this automatically.
2. **Return HTTP 200** to acknowledge receipt. Any other code triggers up to 10 retries (2 minutes apart).
3. **Implement a backup poller** — for orders older than 10 minutes that haven't reached a final status, poll `getOrder()` as a safety net.
4. **Be idempotent** — you may receive the same webhook multiple times.

## Test Cards

Use these cards in Cost+ test mode (sandbox website):

| Card | Number | Notes |
|---|---|---|
| Visa (success) | `4111 1111 1111 1111` | Any CVV |
| Mastercard (success) | `5544 3300 0003 7` | Any CVV |
| Visa (declined) | `4111 1111 1111 1105` | Do Not Honor |
| Visa (insufficient funds) | `4111 1111 1111 1151` | Insufficient Funds |

Use any future expiry date and any 3-digit CVC.

## Demo App

A Docker-based demo app is included in the [GitHub repository](https://github.com/NoPayn/node-sdk) for testing the full payment flow:

```bash
cd demo

cat > .env << EOF
NOPAYN_API_KEY=your-api-key
NOPAYN_MERCHANT_ID=your-merchant-id
PUBLIC_URL=http://localhost:3000
EOF

docker compose up --build
```

Open `http://localhost:3000` to see the demo checkout page.

## Support

Need help? Reach out to our support team at **support@costplus.io**.
