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 lunipayWorks 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
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();