npm package: @nlpay/js
Description: NLPay JavaScript SDK - Secure payment elements for card and ACH capture
Latest version: 1.0.5

# @nlpay/js

Secure, PCI-compliant payment elements for card and ACH capture. The SDK renders payment inputs inside isolated iframes, ensuring that sensitive cardholder data never touches your servers. Tokenization is handled entirely within the iframe, and your application receives only a token suitable for server-side API calls.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
  - [NLPay.init(options)](#nlpayinitoptions)
  - [NLPay.createElement(type, options)](#nlpaycreateelementtype-options)
  - [element.mount(selector)](#elementmountselector)
  - [element.unmount()](#elementunmount)
  - [element.update(options)](#elementupdateoptions)
  - [element.focus(field)](#elementfocusfield)
  - [element.clear()](#elementclear)
  - [element.on(event, callback)](#elementonevent-callback)
  - [NLPay.createToken(element)](#nlpaycreatetokenelement)
  - [NLPay.destroy()](#nlpaydestroy)
- [Styling](#styling)
- [Events](#events)
- [Token Response](#token-response)
- [React Integration](#react-integration)
- [Environments](#environments)
- [Supported Card Types](#supported-card-types)
- [Publishing Updates](#publishing-updates)

---

## Installation

### npm / yarn

```bash
npm install @nlpay/js
```

```bash
yarn add @nlpay/js
```

Then import the default export:

```js
import NLPay from '@nlpay/js';
```

### CDN (UMD)

Include the script directly in your HTML. The SDK is exposed as `window.NLPay`.

```html
<script
  src="https://js.nextlevelpayments.io/nlp.umd.js"
  integrity="sha384-6EWRjP7RKaeylMz6qUXJmnHsNx6/ca8i1k3gxzAObytsCBOSNQNuPLs2TZdYYiax"
  crossorigin="anonymous"
></script>
```

### ES Module (CDN)

```html
<script type="module">
  import NLPay from 'https://js.nextlevelpayments.io/nlp.es.js';
</script>
```

---

## Quick Start

```html
<div id="card-element"></div>
<button id="pay-btn">Pay</button>

<script type="module">
  import NLPay from '@nlpay/js';

  // 1. Initialize the SDK with your public key
  NLPay.init({ publicKey: 'pk_live_xxxxxxxx' });

  // 2. Create a card element
  const card = NLPay.createElement('card');

  // 3. Mount it to the DOM
  card.mount('#card-element');

  // 4. Wait for the element to be ready
  card.on('ready', () => {
    console.log('Card element is ready');
  });

  // 5. Tokenize on form submission
  document.getElementById('pay-btn').addEventListener('click', async () => {
    try {
      const result = await NLPay.createToken(card);
      console.log('Token:', result.token);
      // Send result.token to your server to complete the payment
    } catch (err) {
      console.error('Tokenization failed:', err.error);
    }
  });
</script>
```

---

## API Reference

### NLPay.init(options)

Initialize the SDK. Must be called before creating any elements.

```js
NLPay.init({
  publicKey: 'pk_live_xxxxxxxx',
  mode: 'production',
  fonts: ['Inter', 'Roboto Mono'],
});
```

**Parameters**

| Option      | Type       | Default        | Description                                            |
|-------------|------------|----------------|--------------------------------------------------------|
| `publicKey` | `string`   | **(required)** | Your NLPay public API key                              |
| `mode`      | `string`   | `'production'` | Environment: `'local'`, `'staging'`, `'sandbox'`, or `'production'` |
| `apiUrl`    | `string`   | *(auto)*       | Override the API URL for the selected mode             |
| `cdnUrl`    | `string`   | *(auto)*       | Override the CDN URL where iframe files are hosted     |
| `fonts`     | `string[]` | `[]`           | Google Fonts to load inside the iframe                 |

**Returns** the `NLPay` instance (for chaining).

**Throws** if `publicKey` is not provided.

---

### NLPay.createElement(type, options)

Create a payment element. Call `init()` first.

```js
const card = NLPay.createElement('card', {
  styles: { colorPrimary: '#4F46E5' },
  hideZipCode: false,
  hideCardholderName: false,
});

const ach = NLPay.createElement('ach', {
  styles: { colorPrimary: '#4F46E5' },
  hideAccountType: false,
});
```

**Parameters**

| Parameter  | Type     | Description                 |
|------------|----------|-----------------------------|
| `type`     | `string` | `'card'` or `'ach'`        |
| `options`  | `object` | Optional configuration (see below) |

**Element Options**

| Option                | Type      | Default | Applies To  | Description                              |
|-----------------------|-----------|---------|-------------|------------------------------------------|
| `styles`              | `object`  | `{}`    | card, ach   | Style overrides (see [Styling](#styling)) |
| `labels`              | `object`  | `{}`    | card, ach   | Custom labels for form fields            |
| `placeholders`        | `object`  | `{}`    | card, ach   | Custom placeholder text for form fields  |
| `hideZipCode`         | `boolean` | `false` | card, ach   | Hide the zip/postal code field           |
| `hideCardholderName`  | `boolean` | `false` | card        | Hide the cardholder name field           |
| `hideAccountType`     | `boolean` | `false` | ach         | Hide the account type dropdown           |

**Returns** a `CardElement` or `AchElement` instance.

**Throws** if `init()` has not been called or `type` is invalid.

---

### element.mount(selector)

Mount the element into a DOM container. The SDK creates a secure iframe inside the target container.

```js
card.mount('#card-element');

// Or pass a DOM element directly
const container = document.getElementById('card-element');
card.mount(container);
```

**Parameters**

| Parameter  | Type                    | Description                          |
|------------|-------------------------|--------------------------------------|
| `selector` | `string \| HTMLElement` | A CSS selector or a DOM element      |

**Returns** the element instance (for chaining).

**Throws** if the element is already mounted or the container is not found.

---

### element.unmount()

Remove the element from the DOM, tear down the iframe, and clean up all event listeners.

```js
card.unmount();
```

---

### element.update(options)

Update the element's options after creation. Changes are applied immediately if the element is ready.

```js
card.update({
  styles: { colorPrimary: '#DC2626' },
  labels: { cardNumber: 'Card #' },
  hideZipCode: true,
});
```

Accepts the same options as `createElement` (except `type`).

---

### element.focus(field)

Programmatically focus a specific field inside the iframe.

```js
card.focus('cardNumber');  // 'cardNumber', 'expiry', 'cvv', 'cardholderName', 'zipCode'
```

---

### element.clear()

Clear all form fields inside the element.

```js
card.clear();
```

---

### element.on(event, callback)

Subscribe to element events. Returns an unsubscribe function.

```js
const unsubscribe = card.on('change', (data) => {
  console.log('Valid:', data.complete);
});

// Later, remove the listener
unsubscribe();
```

See the [Events](#events) section for all available events and their payloads.

---

### NLPay.createToken(element)

Validate the element's form data and tokenize the payment information. The actual tokenization occurs inside the secure iframe -- sensitive data never reaches your page.

```js
try {
  const result = await NLPay.createToken(card);
  // result.token  - the payment token
  // result.last4  - last 4 digits of the card number
  // result.cardType - detected card brand
} catch (err) {
  // err.error - error message string
  console.error(err.error);
}
```

**Parameters**

| Parameter | Type      | Description                                |
|-----------|-----------|--------------------------------------------|
| `element` | `Element` | A mounted and ready `CardElement` or `AchElement` |

**Returns** `Promise<object>` -- resolves with token data or rejects with an error object.

**Throws** (rejects) if the element is not mounted or not ready.

---

### NLPay.destroy()

Unmount all elements, remove all event listeners, and reset the SDK. Call this when the payment form is no longer needed.

```js
NLPay.destroy();
```

After calling `destroy()`, you must call `init()` again before creating new elements.

---

## Styling

Pass a `styles` object when creating an element to customize the appearance of the secure iframe inputs.

```js
const card = NLPay.createElement('card', {
  styles: {
    fontFamily: 'Inter, sans-serif',
    fontSize: '16px',
    colorText: '#1E293B',
    colorPrimary: '#4F46E5',
    colorBorder: '#CBD5E1',
    colorError: '#DC2626',
    colorBackground: '#FFFFFF',
    borderRadius: '8px',
    inputHeight: '44px',
    spacing: '12px',
  },
});
```

**Available Style Properties**

| Property          | Type     | Default   | Description                                     |
|-------------------|----------|-----------|-------------------------------------------------|
| `fontFamily`      | `string` | System default | Font family for all inputs                   |
| `fontSize`        | `string` | `'14px'`  | Font size for input text                         |
| `colorText`       | `string` | `'#1E293B'` | Input text color                              |
| `colorPrimary`    | `string` | `'#4F46E5'` | Focus ring and accent color                   |
| `colorBorder`     | `string` | `'#CBD5E1'` | Input border color                            |
| `colorError`      | `string` | `'#DC2626'` | Error state text and border color             |
| `colorBackground` | `string` | `'#FFFFFF'` | Input background color                        |
| `borderRadius`    | `string` | `'6px'`   | Border radius for inputs                         |
| `inputHeight`     | `string` | `'40px'`  | Height of each input field                       |
| `spacing`         | `string` | `'12px'`  | Vertical spacing between fields                  |

To load custom Google Fonts inside the iframe, pass them in `init()`:

```js
NLPay.init({
  publicKey: 'pk_live_xxxxxxxx',
  fonts: ['Inter', 'Roboto Mono'],
});

const card = NLPay.createElement('card', {
  styles: { fontFamily: 'Inter, sans-serif' },
});
```

---

## Events

Subscribe to events with `element.on(event, callback)`. Each call returns an unsubscribe function.

### ready

Fired when the iframe has loaded and the element is ready to accept input.

```js
card.on('ready', () => {
  console.log('Element is interactive');
});
```

**Callback receives:** no data.

### change

Fired when the form state changes (field input, validation state).

```js
card.on('change', (data) => {
  console.log(data.complete);  // boolean - all fields valid
  console.log(data.error);     // string | null - current validation error
  console.log(data.cardType);  // string | null - detected card brand (card only)
});
```

### focus

Fired when any field inside the element gains focus.

```js
card.on('focus', (data) => {
  console.log(data.field);  // string - the field that received focus
});
```

### blur

Fired when any field inside the element loses focus.

```js
card.on('blur', (data) => {
  console.log(data.field);  // string - the field that lost focus
});
```

### error

Fired when a validation error or iframe-level error occurs.

```js
card.on('error', (data) => {
  console.error(data.message);  // string - error description
  console.error(data.field);    // string | undefined - related field
});
```

### Removing Listeners

```js
// Option 1: Use the returned unsubscribe function
const unsub = card.on('change', handler);
unsub();

// Option 2: Use element.off()
card.off('change', handler);
```

---

## Token Response

### Card Element

On a successful `createToken()` call with a card element, the resolved object contains:

```js
{
  token: 'tok_xxxxxxxxxxxxxxxx',  // Payment token for server-side use
  last4: '4242',                   // Last 4 digits of the card number
  cardType: 'visa',                // Card brand identifier
  // ...additional fields as returned by the API
}
```

### ACH Element

For ACH elements, the resolved object contains:

```js
{
  token: 'tok_xxxxxxxxxxxxxxxx',  // Payment token for server-side use
  // ...additional fields as returned by the API
}
```

### Fallback

If the API returns a non-token response, the result will contain a `paymentData` object:

```js
{
  paymentData: { ... }  // Raw payment data from the iframe
}
```

### Error

On failure, the promise rejects with:

```js
{
  error: 'Validation failed: card number is invalid'
}
```

Always wrap `createToken()` in a try/catch block.

---

## React Integration

```jsx
import { useRef, useEffect, useState } from 'react';
import NLPay from '@nlpay/js';

function PaymentForm({ publicKey, onToken }) {
  const cardRef = useRef(null);
  const elementRef = useRef(null);
  const [ready, setReady] = useState(false);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // Initialize the SDK
    NLPay.init({ publicKey });

    // Create and mount the card element
    const card = NLPay.createElement('card', {
      styles: {
        fontFamily: 'Inter, sans-serif',
        colorPrimary: '#4F46E5',
        borderRadius: '8px',
      },
    });

    card.mount(cardRef.current);
    elementRef.current = card;

    card.on('ready', () => setReady(true));
    card.on('change', (data) => setError(data.error || null));

    // Cleanup on unmount
    return () => {
      NLPay.destroy();
    };
  }, [publicKey]);

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!elementRef.current || !ready) return;

    setLoading(true);
    setError(null);

    try {
      const result = await NLPay.createToken(elementRef.current);
      onToken(result);
    } catch (err) {
      setError(err.error || 'Tokenization failed');
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div ref={cardRef} />
      {error && <p style={{ color: '#DC2626' }}>{error}</p>}
      <button type="submit" disabled={!ready || loading}>
        {loading ? 'Processing...' : 'Pay'}
      </button>
    </form>
  );
}

export default PaymentForm;
```

### ACH Variant

```jsx
useEffect(() => {
  NLPay.init({ publicKey });

  const ach = NLPay.createElement('ach', {
    hideAccountType: false,
  });

  ach.mount(achRef.current);
  elementRef.current = ach;

  ach.on('ready', () => setReady(true));

  return () => NLPay.destroy();
}, [publicKey]);
```

---

## Environments

The SDK supports four environments. The mode is set during `init()` and determines which API and CDN endpoints are used.

| Mode          | API URL                                       | CDN URL                                      |
|---------------|-----------------------------------------------|----------------------------------------------|
| `local`       | `http://localhost:8081`                        | `http://localhost:4600`                       |
| `staging`     | `https://api-staging.nextlevelpayments.io`     | `https://js-staging.nextlevelpayments.io`     |
| `sandbox`     | `https://api-sandbox.nextlevelpayments.io`     | `https://js-sandbox.nextlevelpayments.io`     |
| `production`  | `https://api.nextlevelpayments.io`             | `https://js.nextlevelpayments.io`             |

```js
// Development
NLPay.init({ publicKey: 'pk_test_xxxxxxxx', mode: 'staging' });

// Production (default)
NLPay.init({ publicKey: 'pk_live_xxxxxxxx' });
```

You can also override the URLs directly:

```js
NLPay.init({
  publicKey: 'pk_test_xxxxxxxx',
  mode: 'staging',
  apiUrl: 'https://custom-api.example.com',
  cdnUrl: 'https://custom-cdn.example.com',
});
```

---

## Supported Card Types

The SDK automatically detects and validates the following card brands:

| Brand            | Prefix Pattern        | Accepted Lengths | CVV Length |
|------------------|-----------------------|------------------|------------|
| Visa             | `4`                   | 13, 16, 19       | 3          |
| Mastercard       | `51-55`, `22-27`      | 16                | 3          |
| American Express | `34`, `37`            | 15                | 4          |
| Discover         | `6011`, `65`, `644-649` | 16, 19          | 3          |
| Diners Club      | `300-305`, `36`, `38` | 14, 16, 19       | 3          |
| JCB              | `2131`, `1800`, `35`  | 15, 16, 19       | 3          |

Card numbers are validated with the Luhn algorithm. Expiry dates are checked against the current date.

---

## Publishing Updates

When you make changes to the SDK and need to publish a new version to npm:

### 1. Bump the version

```bash
# Patch release (1.0.0 -> 1.0.1) — bug fixes
npm version patch

# Minor release (1.0.0 -> 1.1.0) — new features, backward compatible
npm version minor

# Major release (1.0.0 -> 2.0.0) — breaking changes
npm version major
```

`npm version` automatically updates `package.json` and creates a git tag.

### 2. Publish to npm

```bash
npm publish --access public
```

The `prepublishOnly` script runs `npm run build` automatically before publishing, so the `dist/` output is always fresh.

### 3. Update consumers

After publishing, update the dependency in each app that uses `@nlpay/js`:

```bash
npm update @nlpay/js
```

Current consumers:
- `nlp-portal` — merchant admin payment modal
- `nlp-bill-pay-app` — customer bill pay (BillPayContent, QuickPayModal)
- `nlp-payment-link` — hosted payment page

### Authentication

Publishing requires an npm account that belongs to the `nlpay` organization.

**Recommended: enable 2FA on your npm account** (npmjs.com > Account Settings > Two-Factor Authentication). This protects against unauthorized publishes. Once enabled, publish with:

```bash
npm publish --access public --otp YOUR_CODE
```

Alternatively, create a granular access token at npmjs.com > Access Tokens with "Bypass 2FA" enabled for CI/CD use. Store it as a secret and authenticate via `.npmrc`:

```bash
echo "//registry.npmjs.org/:_authToken=YOUR_TOKEN" > .npmrc
npm publish --access public
```

Never commit `.npmrc` with tokens to version control. The `.gitignore` should include `.npmrc`.

---

## License

Proprietary. All rights reserved.