SDKs

JavaScript / Node.js

The official LuniPay SDK for Node.js, Bun, Deno, and edge runtimes — plus a zero-dependency browser package for the embed widget.

LuniPay ships two JavaScript packages. Most integrations use lunipay on the server; the browser package is only needed if you want to mount the hosted checkout widget inside your own page.

Install

npm install lunipay

Works on Node.js 18+, Bun, Deno (via npm:lunipay), and edge runtimes (Vercel Functions, Cloudflare Workers, Next.js edge middleware). Ships dual ESM + CJS with full TypeScript declarations.

Initialize

Create a client by passing your secret key, or an options object:

import LuniPay from 'lunipay';

// Shorthand — pass the secret key directly.
const lunipay = new LuniPay(process.env.LUNIPAY_SECRET_KEY!);

// Full form — override the API base, pass a custom fetch, tune retries.
const lunipay = new LuniPay({
  apiKey: process.env.LUNIPAY_SECRET_KEY!,
  apiBase: 'https://lunipay.io',
  maxNetworkRetries: 2,
});

Server-side only

Never put an sk_… key in client-side code. Call the SDK from your backend (Next.js route handlers, API routes, serverless functions, etc.) and expose a thin proxy endpoint to your frontend.

Resources

Every LuniPay resource is exposed as a typed namespace on the client. Method names match the REST verbs one-to-one.

// Checkout sessions
const session = await lunipay.checkout.sessions.create({
  amount: 5000,
  currency: 'usd',
  success_url: 'https://example.com/thanks?session_id={CHECKOUT_SESSION_ID}',
});

// Customers
const customer = await lunipay.customers.create({
  email: 'ada@example.com',
  first_name: 'Ada',
  last_name: 'Lovelace',
});
const fetched = await lunipay.customers.retrieve(customer.id);
await lunipay.customers.update(customer.id, { phone: '+1-876-555-0100' });
await lunipay.customers.del(customer.id);

// Invoices
const invoice = await lunipay.invoices.create({
  customer: customer.id,
  currency: 'usd',
  payment_terms: 'net_30',
  line_items: [{ description: 'Consulting', quantity: 10, unit_price_cents: 1000 }],
});
await lunipay.invoices.send(invoice.id);

// Payments & refunds
const payment = await lunipay.payments.retrieve('pay_01JRZK...');
await lunipay.payments.refund(payment.id, { reason: 'requested_by_customer' });

// Payment links
await lunipay.paymentLinks.create({
  amount: 2500,
  currency: 'usd',
  type: 'MULTI',
  name: 'Donation drive',
});

// Webhook endpoints
await lunipay.webhookEndpoints.create({
  url: 'https://example.com/webhooks/lunipay',
  enabled_events: ['checkout.session.completed', 'payment.succeeded'],
});

// Events
await lunipay.events.retrieve('evt_01JRZK...');

Pagination

Every list() method returns a cursor-paginated response. For small result sets, await it directly. For large ones, call .autoPagingIter() and iterate — the SDK fetches pages lazily as you consume them.

// One-shot — gives you a single page.
const page = await lunipay.customers.list({ limit: 20 });
console.log(page.data.length, page.has_more);

// Auto-pagination — walks every page.
for await (const customer of lunipay.customers.list().autoPagingIter()) {
  console.log(customer.email);
}

Errors

Every non-2xx response throws a typed error. Branch on the error class or error.code — both are stable.

import { InvalidRequestError, RateLimitError } from 'lunipay';

try {
  await lunipay.checkout.sessions.create({
    amount: 1,
    currency: 'usd',
    success_url: 'https://example.com/x',
  });
} catch (err) {
  if (err instanceof InvalidRequestError && err.code === 'amount_too_small') {
    // Show a friendly "at least $0.50" message in your UI.
  } else if (err instanceof RateLimitError) {
    // Back off and retry.
  } else {
    throw err;
  }
}

Error classes: LuniPayError (base), InvalidRequestError, AuthenticationError, PermissionError, RateLimitError, ApiError.

Idempotency

Every mutating method accepts an idempotencyKey option. Retries with the same key return the original response instead of creating duplicates.

import { randomUUID } from 'node:crypto';

const key = randomUUID();

await lunipay.checkout.sessions.create(
  {
    amount: 5000,
    currency: 'usd',
    success_url: 'https://example.com/thanks',
  },
  { idempotencyKey: key },
);

Webhooks

The SDK ships a webhook signature verifier. Pass the raw request body, the LuniPay-Signatureheader, and your endpoint's signing secret:

import { Webhook, WebhookSignatureError } from 'lunipay';

export async function POST(req: Request) {
  const body = await req.text(); // raw string — do NOT JSON.parse yet
  const sig = req.headers.get('LuniPay-Signature');

  try {
    const event = Webhook.constructEvent(
      body,
      sig,
      process.env.LUNIPAY_WEBHOOK_SECRET!,
    );

    if (event.type === 'checkout.session.completed') {
      // fulfill the order
    }
  } catch (err) {
    if (err instanceof WebhookSignatureError) {
      return new Response('bad signature', { status: 400 });
    }
    throw err;
  }

  return new Response(null, { status: 200 });
}

Browser package (@lunipay/js)

If you want to embed the LuniPay hosted checkout on your own page instead of redirecting, use the separate browser package. It's a zero-dependency IIFE, ~6 KB gzipped, loaded with a <script> tag or imported from npm.

<script src="https://cdn.jsdelivr.net/npm/@lunipay/js@0.1.0/dist/lunipay.js"></script>
// Initialize with your publishable key (not the secret key).
const lunipay = LuniPay('pk_test_YOUR_PUBLISHABLE_KEY');

// Option 1 — hand off to the hosted checkout page.
lunipay.checkout.redirectToCheckout({ sessionId: 'cs_01JRZK...' });

// Option 2 — mount the widget inline on your own page.
const handle = lunipay.checkout.mount('#checkout', {
  sessionId: 'cs_01JRZK...',
  theme: { primaryColor: '#0F172A' },
  onSuccess: ({ payment }) => console.log('paid', payment),
  onError:   ({ message }) => console.error(message),
});

// Tear down when you navigate away.
handle.destroy();