@paylayer/core
v0.1.3
Published
Provider agnostic payments SDK for one-time payments, subscriptions, and webhooks.
Maintainers
Readme
💳 @paylayer/core
Build billing once. Switch providers anytime.
A production-grade, provider-agnostic payments SDK for Node.js that provides a unified API for one-time payments, subscriptions, and webhooks across multiple payment providers.
Features • Quick Start • API Reference • Webhooks • Providers
What is PayLayer?
PayLayer is a unified payments SDK that lets you integrate billing into your application once and switch between payment providers (Stripe, Paddle, PayPal, Lemon Squeezy, Polar.sh) without changing your code. Write your billing logic once, deploy anywhere.
Key Benefits:
- Provider Flexibility - Switch providers without code changes
- Unified API - One consistent interface for all providers
- Type Safety - Full TypeScript support with autocomplete
- Production Ready - Fully implemented for all supported providers
📋 Table of Contents
- Features
- Installation
- Quick Start
- Configuration
- API Reference
- Webhooks
- Supported Providers
- TypeScript Support
- Security
- Error Handling
- License
✨ Features
| Feature | Description | | ------------------------ | ------------------------------------------------------------ | | 💰 One-time payments | Charge customers with a simple API | | 🔄 Subscriptions | Create and manage recurring billing | | 🔔 Webhooks | Normalized event handling across providers | | 🏢 Billing portal | Customer self-service portal URLs | | 🔀 Provider-agnostic | Switch providers without changing your code | | 📘 TypeScript | Full type safety out of the box | | 📦 ESM + CJS | Works with both module systems | | 🚀 Production-ready | Fully functional implementations for all supported providers |
📦 Installation
npm install @paylayer/coreRelated Packages & Examples
- @paylayer/react - React hooks and components for PayLayer integration
- PayLayer Express Example - Complete Express.js quickstarter with service layer architecture, webhook handling, and all payment operations
🚀 Quick Start
1. Install and Configure
npm install @paylayer/coreCreate a .env file:
PAYLAYER_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_live_YOUR_KEY_HERE
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET_HERE2. Use the SDK
import { pay } from "@paylayer/core";
// One-time payment
const charge = await pay.charge({
amount: 29.99,
currency: "USD",
email: "[email protected]",
});
// Create subscription
const subscription = await pay.subscribe({
plan: "pro-monthly",
currency: "USD",
email: "[email protected]",
});
// Checkout session
const checkout = await pay.checkout({
amount: 29.99,
currency: "USD",
email: "[email protected]",
successUrl: "https://myapp.com/success",
cancelUrl: "https://myapp.com/cancel",
});
// Billing portal
const portalUrl = await pay.portal({
email: "[email protected]",
});
// Subscription management
await pay.cancel("sub_1234567890");
await pay.pause("sub_1234567890");
await pay.resume("sub_1234567890");Complete Example Application
For a complete working example with Express.js integration, see the PayLayer Express Example repository. It includes:
- Full Express.js application setup
- Service layer architecture
- Webhook handling implementation
- All payment operations demonstrated
- Ready-to-run code examples
⚙️ Configuration
Core Variables
| Variable | Required | Description | Valid Values |
| ---------------------- | -------- | ------------------------------------------- | ------------------------------------------------------------- |
| PAYLAYER_PROVIDER | ✅ | Payment provider to use | stripe, paddle, paypal, lemonsqueezy, polar, mock |
| PAYLAYER_ENVIRONMENT | ❌ | Environment mode (defaults to production) | sandbox, test, production, live |
Provider Credentials
Each provider requires specific environment variables:
| Provider | Required Variables | Optional Variables |
| ----------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| Stripe | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET | STRIPE_PORTAL_RETURN_URL |
| Paddle | PADDLE_API_KEY, PADDLE_WEBHOOK_SECRET, PADDLE_DEFAULT_PRICE_ID | PADDLE_BASE_URL, PADDLE_PORTAL_BASE_URL |
| PayPal | PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_SECRET | PAYPAL_BASE_URL, PAYPAL_BRAND_NAME, PAYPAL_RETURN_URL, PAYPAL_CANCEL_URL |
| Lemon Squeezy | LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_WEBHOOK_SECRET, LEMONSQUEEZY_STORE_ID | LEMONSQUEEZY_BASE_URL, LEMONSQUEEZY_DEFAULT_VARIANT_ID |
| Polar.sh | POLAR_OAT (or POLAR_ACCESS_TOKEN), POLAR_WEBHOOK_SECRET | POLAR_BASE_URL, POLAR_SUCCESS_URL |
Configuration Examples
PAYLAYER_PROVIDER=stripe
PAYLAYER_ENVIRONMENT=production
STRIPE_SECRET_KEY=sk_live_YOUR_KEY_HERE
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET_HERE
STRIPE_PORTAL_RETURN_URL=https://myapp.com/settings/billingPAYLAYER_PROVIDER=paddle
PAYLAYER_ENVIRONMENT=sandbox
PADDLE_API_KEY=YOUR_API_KEY_HERE
PADDLE_WEBHOOK_SECRET=YOUR_SECRET_HERE
PADDLE_DEFAULT_PRICE_ID=pri_YOUR_PRICE_ID_HEREPAYLAYER_PROVIDER=paypal
PAYLAYER_ENVIRONMENT=sandbox
PAYPAL_CLIENT_ID=YOUR_CLIENT_ID_HERE
PAYPAL_CLIENT_SECRET=YOUR_SECRET_HERE
PAYPAL_WEBHOOK_SECRET=YOUR_SECRET_HERE
PAYPAL_RETURN_URL=https://myapp.com/payment/success
PAYPAL_CANCEL_URL=https://myapp.com/payment/cancelPAYLAYER_PROVIDER=lemonsqueezy
PAYLAYER_ENVIRONMENT=production
LEMONSQUEEZY_API_KEY=YOUR_API_KEY_HERE
LEMONSQUEEZY_WEBHOOK_SECRET=YOUR_SECRET_HERE
LEMONSQUEEZY_STORE_ID=YOUR_STORE_ID_HEREPAYLAYER_PROVIDER=polar
PAYLAYER_ENVIRONMENT=production
POLAR_OAT=YOUR_OAT_HERE
POLAR_WEBHOOK_SECRET=YOUR_SECRET_HERE
POLAR_SUCCESS_URL=https://myapp.com/payment/successProvider Setup
- Create an account with your chosen provider
- Get API keys/credentials from the provider dashboard
- Create products/prices/plans in the provider dashboard
- Set up webhooks pointing to
https://yourdomain.com/webhooks/paylayer - Copy the webhook signing secret to your environment variables
Provider Dashboards:
- Stripe: Dashboard → API Keys, Products, Webhooks
- Paddle: Dashboard → Authentication, Catalog, Notifications
- PayPal: Developer Dashboard → Apps, Billing, Webhooks
- Lemon Squeezy: Dashboard → Settings → API, Stores, Webhooks
- Polar: Dashboard → Settings → Access Tokens, Products, Webhooks
Note: For Stripe, use lookup_key on prices as the plan parameter. For other providers, use the price/plan/variant ID directly.
📚 API Reference
One-Time Payments
pay.charge(input)
Creates a one-time payment charge.
Parameters:
| Parameter | Type | Required | Description |
| ------------ | -------- | -------- | ---------------------------------------------------- |
| amount | number | ✅* | Payment amount (e.g., 29.99 for $29.99) |
| currency | string | ✅ | ISO 4217 currency code (e.g., 'USD', 'EUR') |
| email | string | ❌ | Customer email address |
| priceId | string | ✅* | Provider-specific price ID (alternative to amount) |
| productId | string | ✅* | Provider-specific product ID (alternative to amount) |
| successUrl | string | ❌ | URL to redirect after successful payment |
| cancelUrl | string | ❌ | URL to redirect if payment is cancelled |
| metadata | object | ❌ | Additional metadata to attach to the payment |
*Either amount, priceId, or productId must be provided.
Returns: Promise<ChargeResult>
interface ChargeResult {
id: string; // Payment ID from provider
status: "pending" | "succeeded" | "failed";
amount: number;
currency: string;
provider: string;
email?: string;
url?: string;
}Example:
const result = await pay.charge({
amount: 29.99,
currency: "USD",
email: "[email protected]",
});pay.checkout(input)
Creates a checkout session/payment link. Returns a URL that can be opened in a browser to complete payment.
Parameters:
| Parameter | Type | Required | Description |
| ------------ | -------- | -------- | ---------------------------------------- |
| amount | number | ❌ | Payment amount (for one-time payments) |
| plan | string | ❌ | Plan identifier (for subscriptions) |
| currency | string | ✅ | ISO 4217 currency code |
| email | string | ❌ | Customer email address |
| successUrl | string | ✅ | URL to redirect after successful payment |
| cancelUrl | string | ✅ | URL to redirect if payment is cancelled |
Returns: Promise<CheckoutResult> with url property
Example:
const checkout = await pay.checkout({
amount: 29.99,
currency: "USD",
email: "[email protected]",
successUrl: "https://myapp.com/success",
cancelUrl: "https://myapp.com/cancel",
});
// Redirect user to checkout.url
res.redirect(checkout.url);Subscriptions
pay.subscribe(input)
Creates a new subscription.
Parameters:
| Parameter | Type | Required | Description |
| ------------ | -------- | -------- | ------------------------------------------------- |
| plan | string | ✅ | Plan identifier (format varies by provider) |
| currency | string | ✅ | ISO 4217 currency code |
| email | string | ❌ | Customer email address |
| successUrl | string | ❌ | URL to redirect after successful subscription |
| cancelUrl | string | ❌ | URL to redirect if subscription is cancelled |
| metadata | object | ❌ | Additional metadata to attach to the subscription |
Plan Identifier Formats:
- Stripe:
lookup_key(e.g.,"pro-monthly") - Paddle: Price ID (e.g.,
"pri_01h8xce2x86dt3sfhkjqbpde65") - PayPal: Plan ID (e.g.,
"P-1234567890") - Lemon Squeezy: Variant ID (e.g.,
"67890") - Polar: Product ID (e.g.,
"prod_1234567890")
Returns: Promise<SubscriptionResult>
interface SubscriptionResult {
id: string; // Subscription ID from provider
status: "active" | "paused" | "cancelled" | "past_due";
plan: string;
currency: string;
provider: string;
email?: string;
url?: string;
}Example:
const subscription = await pay.subscribe({
plan: "pro-monthly",
currency: "USD",
email: "[email protected]",
});pay.cancel(subscriptionId)
Cancels an active subscription. Remains active until end of billing period.
await pay.cancel("sub_1234567890");pay.pause(subscriptionId)
Pauses an active subscription. Billing is paused.
await pay.pause("sub_1234567890");pay.resume(subscriptionId)
Resumes a paused subscription. Billing resumes immediately.
await pay.resume("sub_1234567890");Billing Portal
pay.portal(input)
Generates a billing portal URL for customer self-service.
Parameters:
| Parameter | Type | Required | Description |
| --------- | -------- | -------- | ---------------------- |
| email | string | ✅ | Customer email address |
Returns: Promise<string> - Billing portal URL
Example:
const portalUrl = await pay.portal({
email: "[email protected]",
});
// Redirect user to portalUrl
res.redirect(portalUrl);What customers can do:
- Update payment methods
- View billing history
- Cancel subscriptions
- Update billing information
- Download invoices
🔔 Webhooks
Webhooks allow payment providers to notify your application about payment events in real-time. PayLayer normalizes all webhook events to a consistent format.
How It Works
The webhook system works in three steps:
- Register event handlers - Define what happens when events occur
- Process incoming webhooks -
webhook.process()verifies, normalizes, and triggers handlers - Handlers execute - Your registered callbacks run with the normalized event
Setup
- Register event handlers before processing webhook requests:
import { webhook } from "@paylayer/core";
webhook.onPaymentSuccess((event) => {
console.log("Payment succeeded:", event);
// Update database, send confirmation emails, etc.
});
webhook.onPaymentFailed((event) => {
console.log("Payment failed:", event);
});
webhook.onSubscriptionCreated((event) => {
console.log("Subscription created:", event);
});
webhook.onSubscriptionCancelled((event) => {
console.log("Subscription cancelled:", event);
});
webhook.onSubscriptionUpdated((event) => {
console.log("Subscription updated:", event);
});
webhook.onSubscriptionDeleted((event) => {
console.log("Subscription deleted:", event);
});
webhook.onSubscriptionPaused((event) => {
console.log("Subscription paused:", event);
});
webhook.onSubscriptionResumed((event) => {
console.log("Subscription resumed:", event);
});- Create a webhook endpoint in your application:
import express from "express";
import { webhook } from "@paylayer/core";
const app = express();
// Important: Use raw body for webhook signature verification
app.post(
"/webhooks/paylayer",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
const result = await webhook.process(req);
res.status(result.status).json(result.body);
} catch (error) {
console.error("Webhook error:", error);
res.status(500).json({ error: "Internal server error" });
}
}
);Understanding webhook.process()
The webhook.process(request) method handles the entire webhook processing flow:
What it does:
- Verifies the signature - Validates the webhook request is authentic using the provider's signing secret
- Normalizes the event - Converts provider-specific events (e.g., Stripe's
payment_intent.succeeded) to PayLayer's unified format (e.g.,payment.success) - Triggers registered handlers - Automatically calls all handlers registered for that event type
- Returns a response - Provides status and body for your HTTP response
Return Value:
{
status: number; // 200 for success, 401 for invalid signature
body: {
received: boolean;
}
}Example Flow:
// 1. Provider sends webhook to your endpoint
POST /webhooks/paylayer
{
"type": "payment_intent.succeeded", // Stripe-specific format
"data": { ... }
}
// 2. webhook.process() is called
const result = await webhook.process(req);
// 3. Internally, PayLayer:
// - Verifies signature ✓
// - Normalizes to: { type: "payment.success", ... }
// - Finds handlers registered with webhook.onPaymentSuccess()
// - Executes all registered handlers asynchronously
// 4. Returns response
// { status: 200, body: { received: true } }Important Notes:
- Handlers are executed asynchronously -
webhook.process()doesn't wait for handlers to complete - Multiple handlers can be registered for the same event type - all will be called
- Handler errors are caught and logged, but don't affect the webhook response
- Invalid signatures return
401status - handlers are not executed
- Configure webhook URL in provider dashboard:
- Point to
https://yourdomain.com/webhooks/paylayer - Copy the signing secret to your environment variables
- Point to
Event Object
All webhook events are normalized to a consistent format:
interface NormalizedEvent {
type:
| "payment.success"
| "payment.failed"
| "subscription.created"
| "subscription.updated"
| "subscription.deleted"
| "subscription.cancelled"
| "subscription.paused"
| "subscription.resumed";
amount?: number;
currency?: string;
email?: string;
provider: string;
subscriptionId?: string;
paymentId?: string;
customerId?: string;
customer?: CustomerInfo;
status?: string;
description?: string;
createdAt?: string;
plan?: string;
productId?: string;
metadata?: Record<string, unknown>;
providerResponse?: unknown;
}Example Event:
{
type: "payment.success",
amount: 29.99,
currency: "USD",
email: "[email protected]",
provider: "stripe",
paymentId: "pi_1234567890",
metadata: {}
}Security
- ✅ All webhook signatures are automatically verified
- ✅ Invalid signatures result in a
401response - ✅ Constant-time comparison prevents timing attacks
- ✅ Never process webhooks without signature verification
🏦 Supported Providers
| Provider | Status | Features | | ----------------- | ------ | ----------------------------------------------- | | Stripe | ✅ | Payments, subscriptions, and billing portal | | Paddle | ✅ | Merchant of record, subscriptions, and checkout | | PayPal | ✅ | Payments and subscriptions | | Lemon Squeezy | ✅ | Checkout and subscriptions | | Polar.sh | ✅ | Billing infrastructure and subscriptions |
All providers are fully implemented with proper webhook verification, error handling, and API integration.
📘 TypeScript Support
The SDK is written in TypeScript and provides full type definitions:
import { pay } from "@paylayer/core";
import type {
ChargeResult,
SubscriptionResult,
NormalizedEvent,
} from "@paylayer/core";
const result: ChargeResult = await pay.charge({
amount: 29.99,
currency: "USD",
email: "[email protected]",
});Currency Enum
The SDK includes a comprehensive Currency enum with 150+ currencies for type safety and autocomplete:
import { pay, Currency } from "@paylayer/core";
// Type-safe currency with autocomplete
const result = await pay.charge({
amount: 29.99,
currency: Currency.USD, // TypeScript autocomplete available
email: "[email protected]",
});
// String literals also work
const result2 = await pay.charge({
amount: 29.99,
currency: "USD", // Also valid
email: "[email protected]",
});Common Currencies:
Currency.USD,Currency.EUR,Currency.GBP,Currency.JPYCurrency.AUD,Currency.CAD,Currency.CHF,Currency.CNYCurrency.HKD,Currency.NZD,Currency.SGD
For a complete list, use your IDE's autocomplete or refer to the TypeScript definitions.
🔒 Security
- ✅ Webhook Signature Verification - All webhook signatures verified using provider-specific methods
- ✅ Timing Attack Prevention - Constant-time comparison for signature verification
- ✅ No Sensitive Data Logging - No API keys or payment details logged
- ✅ Environment Variable Security - All credentials via environment variables (never hardcode)
- ✅ Production Safety - Defaults to production mode (explicitly set sandbox for testing)
Best Practices:
- Never commit
.envfiles to version control - Use different API keys for development and production
- Rotate API keys regularly
- Monitor webhook endpoints for suspicious activity
- Use HTTPS for all webhook endpoints
⚠️ Error Handling
The SDK provides clear, actionable error messages:
Missing Environment Variables
// Error: "STRIPE_SECRET_KEY environment variable is required for Stripe provider"
// Solution: Add STRIPE_SECRET_KEY to your .env fileMissing Required Parameters
// Error: "amount is required"
// Solution: Provide the amount parameter in your charge() callInvalid API Responses
// Error: "Stripe API error: 400 - { message: 'Invalid request' }"
// Solution: Check your request parameters and API key validityWebhook Signature Verification Failures
// Returns 401 status if signature verification fails
// Solution: Verify your webhook secret matches the one in your provider dashboardAll errors include context:
- Which provider caused the error
- What operation was being performed
- The original error message from the provider
📄 License
MIT
🤝 Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Made with ❤️ by PayLayer
