@rivium/pay-nodejs-sdk
v0.1.0
Published
RiviumPay SDK for Node.js - Provider-agnostic payments with Lemon Squeezy support (subscriptions, checkouts, webhooks, signature verification)
Maintainers
Readme
@rivium/pay-nodejs-sdk
RiviumPay SDK for Node.js — provider-agnostic payments with subscriptions, hosted checkouts, and signed webhook verification.
Lemon Squeezy is the first supported provider. The interface is designed so additional providers (Stripe, Paddle, NowPayments, …) can plug in behind the same PaymentProvider contract without consumer code needing to change.
Installation
npm install @rivium/pay-nodejs-sdkRequires Node.js ≥ 18 (uses native fetch and crypto). Zero runtime dependencies.
Quick start
import { LemonSqueezyProvider } from '@rivium/pay-nodejs-sdk';
const provider = new LemonSqueezyProvider({
apiKey: process.env.LS_API_KEY!,
webhookSecret: process.env.LS_WEBHOOK_SECRET!,
storeId: process.env.LS_STORE_ID!,
});
// 1. Create a checkout session for a customer.
const checkout = await provider.createCheckout({
variantId: 'starter-monthly', // your LS variant id
customerEmail: '[email protected]',
customerName: 'Bester Realty',
redirectUrl: 'https://yourapp.com/billing/success',
customData: { companyId: 'abc123' }, // travels back in webhook events
});
// Redirect the customer's browser to checkout.url.Handling webhooks
Webhook signature verification must happen against the raw, unparsed request body. If your framework parses JSON before your handler runs, the signature check will fail.
Express
import express from 'express';
import {
LemonSqueezyProvider,
SignatureVerificationError,
} from '@rivium/pay-nodejs-sdk';
const app = express();
// IMPORTANT: register raw-body parsing for the webhook route BEFORE
// any global express.json() middleware.
app.post(
'/webhooks/lemon-squeezy',
express.raw({ type: 'application/json' }),
async (req, res) => {
try {
const event = await provider.verifyAndParseWebhook(
req.body, // Buffer
req.headers['x-signature'] as string,
);
if (!event) {
// LS event that doesn't map to a canonical type we care about.
// 2xx so LS stops retrying.
return res.status(200).send();
}
// Persist whatever your app needs to track. Use event.eventId as
// an idempotency key — LS retries failed deliveries.
await persist(event);
res.status(200).send();
} catch (err) {
if (err instanceof SignatureVerificationError) {
// 401 so LS keeps retrying. Fix the secret on your side and the
// retries will succeed without re-firing the event from LS.
return res.status(401).send('Invalid signature');
}
// Any other error: 500 so LS retries. Don't 2xx a real failure.
console.error('[webhook] error', err);
res.status(500).send();
}
},
);
// global JSON parser AFTER — so it doesn't interfere with the webhook route.
app.use(express.json());Canonical event types
The provider normalizes Lemon Squeezy's ~15 native event names onto a small canonical set:
| event.type | Triggers from LS events |
| ----------------------------- | ------------------------------------------------------------------- |
| subscription.created | subscription_created |
| subscription.updated | subscription_updated, subscription_resumed, subscription_plan_changed, subscription_paused, subscription_unpaused |
| subscription.cancelled | subscription_cancelled, subscription_expired |
| payment.succeeded | subscription_payment_success |
| payment.failed | subscription_payment_failed |
Native LS events outside this list (refunds, license keys, raw orders) return null from verifyAndParseWebhook. Consumer code should 2xx those so LS doesn't retry.
Subscription status
type SubscriptionStatus =
| 'active' // paid, in good standing
| 'trialing' // inside free trial
| 'past_due' // last payment failed; retries pending
| 'cancelled' // explicit cancel; service may still run until period end
| 'expired' // period ended without renewal
| 'incomplete'; // created but never confirmedThe cancelAtPeriodEnd flag distinguishes "cancelled now, expires later" from "expired right now". Your service stays available while cancelAtPeriodEnd === true and currentPeriodEnd is in the future.
API surface
provider.createCheckout(input): Promise<CheckoutSession>
provider.getSubscription(id): Promise<PaymentSubscription | null>
provider.getCustomer(id): Promise<PaymentCustomer | null>
provider.cancelSubscription(id, { immediate? }): Promise<PaymentSubscription>
provider.resumeSubscription(id): Promise<PaymentSubscription>
provider.verifyAndParseWebhook(rawBody, signature): Promise<WebhookEvent | null>cancelSubscription({ immediate: true }) throws — Lemon Squeezy doesn't support immediate hard-cancel via the public API. Cancel without immediate schedules for period end.
Errors
import {
RiviumPayError,
SignatureVerificationError,
InvalidInputError,
ProviderApiError,
} from '@rivium/pay-nodejs-sdk';SignatureVerificationError— webhook signature didn't match. Respond 4xx.InvalidInputError— missing required input from your code (e.g. no signature header).ProviderApiError— the provider's API rejected the call..statusCodeand.causecarry the original.RiviumPayError— base class for everything above. Catch this if you only need to distinguish package errors from runtime errors.
License
MIT
Support
- Landing Page: https://rivium.co/cloud/rivium-pay
- Documentation: https://rivium.co/cloud/rivium-pay/docs/sdks-nodejs
- Issues: https://github.com/Rivium-co/rivium-pay-nodejs-sdk/issues
- Email: [email protected]
