Skip to content

ajagatobby/paylayer-core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PayLayer Logo

💳 @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: "customer@example.com",
});

// Create subscription
const subscription = await pay.subscribe({
  plan: "pro-monthly",
  currency: "USD",
  email: "customer@example.com",
});

// Checkout session
const checkout = await pay.checkout({
  amount: 29.99,
  currency: "USD",
  email: "customer@example.com",
  successUrl: "https://myapp.com/success",
  cancelUrl: "https://myapp.com/cancel",
});

// Billing portal
const portalUrl = await pay.portal({
  email: "customer@example.com",
});

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

Stripe
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
Paddle
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
PayPal
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
Lemon Squeezy
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
Polar.sh
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: "customer@example.com",
});

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: "customer@example.com",
  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: "customer@example.com",
});

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: "customer@example.com",
});

// 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: "customer@example.com",
  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: "customer@example.com",
});

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: "customer@example.com",
});

// String literals also work
const result2 = await pay.charge({
  amount: 29.99,
  currency: "USD", // Also valid
  email: "customer@example.com",
});

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

About

One sdk for stripe, paypal, paddle, lemon squeezy, and polar.

Resources

License

Stars

Watchers

Forks

Packages

No packages published