npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@paylayer/core

v0.1.3

Published

Provider agnostic payments SDK for one-time payments, subscriptions, and webhooks.

Readme

💳 @paylayer/core

Build billing once. Switch providers anytime.

npm version License: MIT Node.js Version

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.

FeaturesQuick StartAPI ReferenceWebhooksProviders


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

| 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/core

Related 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/core

Create a .env file:

PAYLAYER_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_live_YOUR_KEY_HERE
STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET_HERE

2. 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/billing
PAYLAYER_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_HERE
PAYLAYER_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/cancel
PAYLAYER_PROVIDER=lemonsqueezy
PAYLAYER_ENVIRONMENT=production
LEMONSQUEEZY_API_KEY=YOUR_API_KEY_HERE
LEMONSQUEEZY_WEBHOOK_SECRET=YOUR_SECRET_HERE
LEMONSQUEEZY_STORE_ID=YOUR_STORE_ID_HERE
PAYLAYER_PROVIDER=polar
PAYLAYER_ENVIRONMENT=production
POLAR_OAT=YOUR_OAT_HERE
POLAR_WEBHOOK_SECRET=YOUR_SECRET_HERE
POLAR_SUCCESS_URL=https://myapp.com/payment/success

Provider Setup

  1. Create an account with your chosen provider
  2. Get API keys/credentials from the provider dashboard
  3. Create products/prices/plans in the provider dashboard
  4. Set up webhooks pointing to https://yourdomain.com/webhooks/paylayer
  5. 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:

  1. Register event handlers - Define what happens when events occur
  2. Process incoming webhooks - webhook.process() verifies, normalizes, and triggers handlers
  3. Handlers execute - Your registered callbacks run with the normalized event

Setup

  1. 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);
});
  1. 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:

  1. Verifies the signature - Validates the webhook request is authentic using the provider's signing secret
  2. Normalizes the event - Converts provider-specific events (e.g., Stripe's payment_intent.succeeded) to PayLayer's unified format (e.g., payment.success)
  3. Triggers registered handlers - Automatically calls all handlers registered for that event type
  4. 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 401 status - handlers are not executed
  1. Configure webhook URL in provider dashboard:
    • Point to https://yourdomain.com/webhooks/paylayer
    • Copy the signing secret to your environment variables

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 401 response
  • ✅ 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.JPY
  • Currency.AUD, Currency.CAD, Currency.CHF, Currency.CNY
  • Currency.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 .env files 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 file

Missing Required Parameters

// Error: "amount is required"
// Solution: Provide the amount parameter in your charge() call

Invalid API Responses

// Error: "Stripe API error: 400 - { message: 'Invalid request' }"
// Solution: Check your request parameters and API key validity

Webhook Signature Verification Failures

// Returns 401 status if signature verification fails
// Solution: Verify your webhook secret matches the one in your provider dashboard

All 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