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

saligpay-node

v0.1.2

Published

SaligPay Node.js SDK - Type-safe client for SaligPay payment integration

Readme

saligpay-node

Official Node.js SDK for SaligPay payment integration. A type-safe, production-ready client for processing payments, managing checkout sessions, and handling webhooks.

npm version TypeScript Node.js

Features

  • Type-safe - Full TypeScript support with exported types
  • Dual build - Works with both ESM and CommonJS
  • Authentication - Client credentials OAuth flow
  • Checkout - Create and manage payment sessions
  • Webhooks - Parse and verify webhook events
  • Error handling - Custom error classes with detailed context
  • Node.js 18+ - Uses native fetch API
  • Zero dependencies - Minimal runtime footprint

Installation

npm install saligpay-node
# or
yarn add saligpay-node
# or
pnpm add saligpay-node
# or
bun add saligpay-node

Quick Start

import { SaligPay } from "saligpay-node";

// Initialize the SDK
const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID!,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET!,
    env: "sandbox", // or 'production'
});

// Authenticate
await saligpay.authenticate();

// Create a checkout session
const checkout = await saligpay.checkout.create({
    externalId: "order-123",
    amount: 10000, // in centavos (₱100.00)
    description: "Premium Plan Subscription",
    webhookUrl: "https://yourapp.com/webhooks/saligpay",
    returnUrl: "https://yourapp.com/payment/success",
    contact: {
        name: "John Doe",
        email: "[email protected]",
        contact: "+639123456789",
    },
});

console.log("Checkout URL:", checkout.checkoutUrl);
// Redirect user to checkout.checkoutUrl to complete payment

Table of Contents

Configuration

Environment Variables

Create a .env file (recommended):

SALIGPAY_CLIENT_ID=your_client_id
SALIGPAY_CLIENT_SECRET=your_client_secret
SALIGPAY_ENV=sandbox

SDK Configuration Options

interface SaligPayConfig {
    /** OAuth Client ID */
    clientId?: string;

    /** OAuth Client Secret */
    clientSecret?: string;

    /** Admin API Key for platform operations */
    adminKey?: string;

    /** Custom base URL (overrides env setting) */
    baseUrl?: string;

    /** Environment: 'production' | 'sandbox' */
    env?: "production" | "sandbox";
}

Example Configuration

// Using environment variables (recommended)
const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET,
    env: process.env.SALIGPAY_ENV as "production" | "sandbox",
});

// Custom base URL for staging/testing
const saligpayStaging = new SaligPay({
    clientId: "your-id",
    clientSecret: "your-secret",
    baseUrl: "https://staging-api.saligpay.com",
});

Authentication

Client Credentials Flow

The SDK uses OAuth 2.0 client credentials for authentication:

import { SaligPay } from "saligpay-node";

const saligpay = new SaligPay({
    clientId: "your-client-id",
    clientSecret: "your-client-secret",
});

// Authenticate and store tokens
const tokens = await saligpay.authenticate();
console.log("Access Token:", tokens.accessToken);
console.log("Expires At:", tokens.expiresAt);
console.log("Refresh Token:", tokens.refreshToken);

// Check if authenticated
if (saligpay.isAuthenticated()) {
    console.log("Still authenticated!");
}

// Refresh token if expired
await saligpay.ensureAuthenticated();

Manual Authentication

You can also authenticate on-the-fly:

// Authenticate with provided credentials
const tokens = await saligpay.auth.authenticate("client-id", "client-secret");

Refresh Tokens

// Refresh an existing token
const newTokens = await saligpay.auth.refreshToken(refreshToken);

// Automatically refreshes when needed
await saligpay.ensureAuthenticated();

Token Validation

// Validate an access token
const isValid = await saligpay.auth.validateToken(accessToken);
console.log("Token valid:", isValid);

Full Login Flow (Platform/Admin)

For platform integrations that need to onboard merchants programmatically, use loginAndRetrieveCredentials. This method performs the complete authentication flow:

  1. Sign in — Authenticate user with email/password
  2. Get merchant — Retrieve associated merchant details
  3. Register OAuth — Create OAuth credentials for the merchant (idempotent)
  4. Authenticate — Get access tokens using the new credentials

[!IMPORTANT] This method requires an Admin Key and is intended for platform-level operations, not end-user authentication.

import { SaligPay } from "saligpay-node";

const saligpay = new SaligPay({
    adminKey: process.env.SALIGPAY_ADMIN_KEY!,
    env: "sandbox",
});

// Full login flow for a merchant
const result = await saligpay.auth.loginAndRetrieveCredentials(
    "[email protected]",
    "merchant-password",
);

// Result contains everything needed for subsequent API calls
console.log("User:", result.user);
console.log("Merchant:", result.merchant);
console.log("OAuth Credentials:", result.credentials);
console.log("Access Token:", result.tokens.accessToken);

LoginResult Structure

interface LoginResult {
    user: {
        id: string;
        email: string;
        name: string;
    };
    merchant: {
        id: string;
        email: string;
        tradeName: string;
        // ... other merchant fields
    };
    credentials: {
        clientId: string;
        clientSecret: string;
    };
    tokens: SaligPayAuthTokens;
}

Use Cases

Merchant Onboarding Flow:

// Platform backend onboarding a new merchant
async function onboardMerchant(email: string, password: string) {
    const saligpay = new SaligPay({
        adminKey: process.env.SALIGPAY_ADMIN_KEY!,
        env: "production",
    });

    const result = await saligpay.auth.loginAndRetrieveCredentials(
        email,
        password,
    );

    // Store credentials securely for future API calls
    await db.merchants.update({
        where: { email },
        data: {
            saligpayClientId: result.credentials.clientId,
            saligpayClientSecret: result.credentials.clientSecret,
            saligpayMerchantId: result.merchant.id,
        },
    });

    return result;
}

Multi-Tenant Platform:

// Create checkout on behalf of a merchant
async function createCheckoutForMerchant(
    merchantId: string,
    checkoutData: CreateCheckoutOptions,
) {
    const merchant = await db.merchants.findUnique({
        where: { id: merchantId },
    });

    const saligpay = new SaligPay({
        clientId: merchant.saligpayClientId,
        clientSecret: merchant.saligpayClientSecret,
        env: "production",
    });

    await saligpay.ensureAuthenticated();
    return saligpay.checkout.create(checkoutData);
}

Checkout Sessions

Creating a Checkout Session

const checkout = await saligpay.checkout.create({
    externalId: "order-123",
    amount: 10000, // ₱100.00 in centavos
    description: "Premium Plan Subscription",
    webhookUrl: "https://yourapp.com/webhooks/saligpay",
    returnUrl: "https://yourapp.com/payment/success",
    contact: {
        name: "John Doe",
        email: "[email protected]",
        contact: "+639123456789",
    },
    metadata: {
        orderId: "12345",
        userId: "user-abc",
    },
    isThirdParty: true,
});

console.log("Checkout ID:", checkout.id);
console.log("Session Token:", checkout.sessionToken);
console.log("Checkout URL:", checkout.checkoutUrl);
console.log("Expires At:", checkout.expiresAt);

Checkout Options

interface CreateCheckoutOptions {
    /** Unique external reference ID (required) */
    externalId: string;

    /** Amount in centavos (required) */
    amount: number;

    /** Description of the payment (required) */
    description: string;

    /** URL to receive webhook notifications (required) */
    webhookUrl: string;

    /** URL to redirect after payment (required) */
    returnUrl: string;

    /** Customer contact information (required) */
    contact: {
        name: string;
        email: string;
        contact?: string;
    };

    /** Additional metadata (optional) */
    metadata?: Record<string, unknown>;

    /** Is third party integration (optional, default: true) */
    isThirdParty?: boolean;
}

Contact Information

const contact = {
    name: "Juan Dela Cruz",
    email: "[email protected]",
    contact: "+639123456789", // Philippine format
};

Using Access Tokens

The SDK automatically uses stored tokens, but you can provide a custom token:

const checkout = await saligpay.checkout.create(
    {
        externalId: "order-456",
        amount: 25000,
        description: "One-time purchase",
        webhookUrl: "https://yourapp.com/webhooks/saligpay",
        returnUrl: "https://yourapp.com/payment/success",
        contact: {
            name: "Jane Smith",
            email: "[email protected]",
        },
    },
    "custom-access-token", // optional custom token
);

Webhooks

Express.js Handler

import express from "express";
import { SaligPay } from "saligpay-node";

const app = express();
const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET,
});

// Raw body parser for signature verification (if needed in future)
app.use("/webhooks/saligpay", express.raw({ type: "application/json" }));

app.post("/webhooks/saligpay", async (req, res) => {
    await saligpay.webhooks.listen(req, res, async (payload) => {
        console.log("Webhook received:", payload);

        switch (payload.status) {
            case "COMPLETED":
                // Update your database
                await db.orders.update(payload.externalId, {
                    status: "PAID",
                    paidAt: new Date(),
                });
                break;

            case "FAILED":
                await db.orders.update(payload.externalId, {
                    status: "FAILED",
                });
                break;

            case "PENDING":
                console.log("Payment pending for:", payload.externalId);
                break;

            default:
                console.log("Unknown status:", payload.status);
        }
    });
});

app.listen(3000, () => {
    console.log("Server running on port 3000");
});

Fastify Handler

import Fastify from "fastify";
import { SaligPay } from "saligpay-node";

const fastify = Fastify();
const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET,
});

fastify.post("/webhooks/saligpay", async (request, reply) => {
    try {
        const payload = saligpay.webhooks.constructEvent(request.body);

        // Process webhook
        console.log("Payment:", payload.externalId, payload.status);

        return reply.send({ received: true });
    } catch (error) {
        return reply.code(400).send({ error: "Invalid webhook" });
    }
});

fastify.listen({ port: 3000 });

Manual Webhook Processing

// Process webhook manually
const payload = saligpay.webhooks.constructEvent(req.body);

// Access webhook data
console.log("External ID:", payload.externalId);
console.log("Amount:", payload.amount / 100, "PHP");
console.log("Status:", payload.status);
console.log("Payment Method:", payload.paymentMethod);
console.log("Contact:", payload.contact);
console.log("Metadata:", payload.metadata);

Webhook Payload Structure

interface SaligPayWebhookPayload {
    id?: string;
    externalId: string;
    amount: number; // in centavos
    status: "COMPLETED" | "FAILED" | "PENDING" | "CANCELLED";
    paymentMethod: {
        id: string;
        type: string;
    };
    contact?: {
        name: string;
        email: string;
        contact?: string;
    };
    metadata?: Record<string, unknown>;
    createdAt?: string;
    updatedAt?: string;
}

Error Handling

Error Classes

The SDK provides custom error classes for better error handling:

import {
    SaligPayError,
    AuthenticationError,
    ValidationError,
    NotFoundError,
} from "saligpay-node";

Try-Catch Pattern

try {
    const checkout = await saligpay.checkout.create({
        externalId: "order-123",
        amount: 10000,
        description: "Test payment",
        webhookUrl: "https://yourapp.com/webhooks/saligpay",
        returnUrl: "https://yourapp.com/success",
        contact: { name: "John", email: "[email protected]" },
    });
} catch (error) {
    if (error instanceof AuthenticationError) {
        console.error("Authentication failed:", error.message);
        // Re-authenticate
        await saligpay.authenticate();
    } else if (error instanceof ValidationError) {
        console.error("Validation error:", error.message);
        console.error("Details:", error.details);
    } else if (error instanceof NotFoundError) {
        console.error("Resource not found:", error.message);
    } else if (error instanceof SaligPayError) {
        console.error("API error:", error.message);
        console.error("Status code:", error.statusCode);
        console.error("Error code:", error.code);
        console.error("Details:", error.details);
    } else {
        console.error("Unknown error:", error);
    }
}

Error Response Structure

try {
    await saligpay.checkout.create(options);
} catch (error) {
    if (error instanceof SaligPayError) {
        console.error(error.statusCode); // HTTP status code
        console.error(error.code); // API error code
        console.error(error.message); // Error message
        console.error(error.details); // Additional details
    }
}

Common Errors

| Error Class | Status Code | Description | | --------------------- | ----------- | -------------------------------- | | ValidationError | 400 | Invalid input data | | AuthenticationError | 401 | Invalid credentials | | NotFoundError | 404 | Resource not found | | SaligPayError | 500 | Server error or unexpected issue |

TypeScript Usage

The SDK is written in TypeScript and exports all types:

import {
    SaligPay,
    SaligPayConfig,
    SaligPayAuthTokens,
    CreateCheckoutOptions,
    CreateCheckoutApiResponse,
    SaligPayWebhookPayload,
    ContactInfo,
} from "saligpay-node";

// Strongly typed configuration
const config: SaligPayConfig = {
    clientId: "your-id",
    clientSecret: "your-secret",
    env: "sandbox",
};

// Typed response
const checkout: CreateCheckoutApiResponse = await saligpay.checkout.create({
    externalId: "order-123",
    amount: 10000,
    description: "Test",
    webhookUrl: "https://example.com/webhook",
    returnUrl: "https://example.com/success",
    contact: {
        name: "John Doe",
        email: "[email protected]",
    },
});

// Typed webhook payload
const handleWebhook = (payload: SaligPayWebhookPayload) => {
    if (payload.status === "COMPLETED") {
        // TypeScript knows payload has all required properties
        console.log(`Payment ${payload.externalId} completed`);
    }
};

CommonJS Usage

The SDK supports both ESM and CommonJS:

// Using require()
const { SaligPay } = require("saligpay-node");

const saligpay = new SaligPay({
    clientId: "your-client-id",
    clientSecret: "your-client-secret",
    env: "sandbox",
});

async function createCheckout() {
    try {
        const checkout = await saligpay.checkout.create({
            externalId: "order-123",
            amount: 10000,
            description: "Test payment",
            webhookUrl: "https://example.com/webhook",
            returnUrl: "https://example.com/success",
            contact: {
                name: "John Doe",
                email: "[email protected]",
            },
        });

        console.log(checkout.checkoutUrl);
    } catch (error) {
        console.error(error);
    }
}

createCheckout();

Testing

Example Test Suite

import { SaligPay } from "saligpay-node";

describe("SaligPay SDK", () => {
    let saligPay: SaligPay;

    beforeAll(() => {
        saligPay = new SaligPay({
            clientId: process.env.TEST_CLIENT_ID,
            clientSecret: process.env.TEST_CLIENT_SECRET,
            env: "sandbox",
        });
    });

    it("should authenticate successfully", async () => {
        const tokens = await saligPay.authenticate();

        expect(tokens).toBeDefined();
        expect(tokens.accessToken).toBeDefined();
        expect(tokens.refreshToken).toBeDefined();
        expect(tokens.expiresAt).toBeInstanceOf(Date);
    });

    it("should create a checkout session", async () => {
        await saligPay.authenticate();

        const checkout = await saligPay.checkout.create({
            externalId: "test-order",
            amount: 10000,
            description: "Test payment",
            webhookUrl: "https://example.com/webhook",
            returnUrl: "https://example.com/success",
            contact: {
                name: "Test User",
                email: "[email protected]",
            },
        });

        expect(checkout).toBeDefined();
        expect(checkout.id).toBeDefined();
        expect(checkout.checkoutUrl).toBeDefined();
    });

    it("should handle webhook payloads", () => {
        const payload = {
            externalId: "test-order",
            amount: 10000,
            status: "COMPLETED",
            paymentMethod: {
                id: "pm-test",
                type: "gcash",
            },
        };

        const event = saligPay.webhooks.constructEvent(payload);

        expect(event.externalId).toBe("test-order");
        expect(event.status).toBe("COMPLETED");
});

Server-Side Usage

The SDK is designed for server-side use only. Here are patterns for popular frameworks.

Singleton Pattern (Recommended)

Create a shared SDK instance to avoid re-initializing on every request:

// lib/saligpay.ts
import { SaligPay } from "saligpay-node";

let saligpayInstance: SaligPay | null = null;

export function getSaligPay(): SaligPay {
    if (!saligpayInstance) {
        saligpayInstance = new SaligPay({
            clientId: process.env.SALIGPAY_CLIENT_ID!,
            clientSecret: process.env.SALIGPAY_CLIENT_SECRET!,
            env: process.env.NODE_ENV === "production" ? "production" : "sandbox",
        });
    }
    return saligpayInstance;
}

React Router 7 (Remix)

Action: Create Checkout

// app/routes/checkout.tsx
import type { ActionFunctionArgs } from "react-router";
import { redirect } from "react-router";
import { getSaligPay } from "~/lib/saligpay.server";

export async function action({ request }: ActionFunctionArgs) {
    const formData = await request.formData();
    const planId = formData.get("planId") as string;
    const email = formData.get("email") as string;

    const saligpay = getSaligPay();
    await saligpay.ensureAuthenticated();

    const checkout = await saligpay.checkout.create({
        externalId: `order-${Date.now()}`,
        amount: planId === "premium" ? 149900 : 49900, // ₱1,499 or ₱499
        description: `${planId} Plan Subscription`,
        webhookUrl: `${process.env.APP_URL}/api/webhooks/saligpay`,
        returnUrl: `${process.env.APP_URL}/checkout/success`,
        contact: {
            name: formData.get("name") as string,
            email,
        },
        metadata: { planId, userId: formData.get("userId") },
    });

    return redirect(checkout.checkoutUrl);
}

export default function CheckoutPage() {
    return (
        <form method="post">
            <input type="hidden" name="planId" value="premium" />
            <input type="text" name="name" placeholder="Full Name" required />
            <input type="email" name="email" placeholder="Email" required />
            <button type="submit">Proceed to Payment</button>
        </form>
    );
}

Loader: Check Payment Status

// app/routes/payment.$orderId.tsx
import type { LoaderFunctionArgs } from "react-router";
import { json } from "react-router";
import { db } from "~/lib/db.server";

export async function loader({ params }: LoaderFunctionArgs) {
    const order = await db.order.findUnique({
        where: { id: params.orderId },
    });

    if (!order) {
        throw new Response("Order not found", { status: 404 });
    }

    return json({
        orderId: order.id,
        status: order.status,
        amount: order.amount,
        paidAt: order.paidAt,
    });
}

Resource Route: Webhook Handler

// app/routes/api.webhooks.saligpay.ts
import type { ActionFunctionArgs } from "react-router";
import { json } from "react-router";
import { getSaligPay } from "~/lib/saligpay.server";
import { db } from "~/lib/db.server";

export async function action({ request }: ActionFunctionArgs) {
    const saligpay = getSaligPay();
    const body = await request.json();

    try {
        const payload = saligpay.webhooks.constructEvent(body);

        switch (payload.status) {
            case "COMPLETED":
                await db.order.update({
                    where: { externalId: payload.externalId },
                    data: {
                        status: "PAID",
                        paidAt: new Date(),
                        paymentMethod: payload.paymentMethod.type,
                    },
                });
                break;

            case "FAILED":
                await db.order.update({
                    where: { externalId: payload.externalId },
                    data: { status: "FAILED" },
                });
                break;
        }

        return json({ received: true });
    } catch (error) {
        console.error("Webhook error:", error);
        return json({ error: "Invalid webhook" }, { status: 400 });
    }
}

Next.js (App Router)

Server Action: Create Checkout

// app/actions/checkout.ts
"use server";

import { redirect } from "next/navigation";
import { getSaligPay } from "@/lib/saligpay";

export async function createCheckout(formData: FormData) {
    const saligpay = getSaligPay();
    await saligpay.ensureAuthenticated();

    const checkout = await saligpay.checkout.create({
        externalId: `order-${Date.now()}`,
        amount: Number(formData.get("amount")),
        description: formData.get("description") as string,
        webhookUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/saligpay`,
        returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success`,
        contact: {
            name: formData.get("name") as string,
            email: formData.get("email") as string,
        },
    });

    redirect(checkout.checkoutUrl);
}

Route Handler: Webhook

// app/api/webhooks/saligpay/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getSaligPay } from "@/lib/saligpay";
import { prisma } from "@/lib/prisma";

export async function POST(request: NextRequest) {
    const saligpay = getSaligPay();
    const body = await request.json();

    try {
        const payload = saligpay.webhooks.constructEvent(body);

        if (payload.status === "COMPLETED") {
            await prisma.order.update({
                where: { externalId: payload.externalId },
                data: {
                    status: "PAID",
                    paidAt: new Date(),
                },
            });
        }

        return NextResponse.json({ received: true });
    } catch (error) {
        console.error("Webhook error:", error);
        return NextResponse.json({ error: "Invalid webhook" }, { status: 400 });
    }
}

Server Component with Checkout Button

// app/checkout/page.tsx
import { createCheckout } from "@/app/actions/checkout";

export default function CheckoutPage() {
    return (
        <form action={createCheckout}>
            <input type="hidden" name="amount" value="10000" />
            <input type="hidden" name="description" value="Premium Plan" />
            <input type="text" name="name" placeholder="Full Name" required />
            <input type="email" name="email" placeholder="Email" required />
            <button type="submit">Pay ₱100.00</button>
        </form>
    );
}

Next.js (Pages Router)

API Route: Create Checkout

// pages/api/checkout/create.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getSaligPay } from "@/lib/saligpay";

export default async function handler(
    req: NextApiRequest,
    res: NextApiResponse,
) {
    if (req.method !== "POST") {
        return res.status(405).json({ error: "Method not allowed" });
    }

    const saligpay = getSaligPay();
    await saligpay.ensureAuthenticated();

    try {
        const { amount, description, name, email } = req.body;

        const checkout = await saligpay.checkout.create({
            externalId: `order-${Date.now()}`,
            amount,
            description,
            webhookUrl: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/saligpay`,
            returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success`,
            contact: { name, email },
        });

        return res.json({ checkoutUrl: checkout.checkoutUrl });
    } catch (error) {
        console.error("Checkout error:", error);
        return res.status(500).json({ error: "Failed to create checkout" });
    }
}

API Route: Webhook Handler

// pages/api/webhooks/saligpay.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { getSaligPay } from "@/lib/saligpay";
import { prisma } from "@/lib/prisma";

export default async function handler(
    req: NextApiRequest,
    res: NextApiResponse,
) {
    if (req.method !== "POST") {
        return res.status(405).json({ error: "Method not allowed" });
    }

    const saligpay = getSaligPay();

    try {
        const payload = saligpay.webhooks.constructEvent(req.body);

        if (payload.status === "COMPLETED") {
            await prisma.order.update({
                where: { externalId: payload.externalId },
                data: { status: "PAID", paidAt: new Date() },
            });
        }

        return res.json({ received: true });
    } catch (error) {
        console.error("Webhook error:", error);
        return res.status(400).json({ error: "Invalid webhook" });
    }
}

Hono

// src/index.ts
import { Hono } from "hono";
import { SaligPay } from "saligpay-node";

const app = new Hono();

const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID!,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET!,
    env: "sandbox",
});

// Create checkout
app.post("/checkout", async (c) => {
    await saligpay.ensureAuthenticated();
    const { amount, description, name, email } = await c.req.json();

    const checkout = await saligpay.checkout.create({
        externalId: `order-${Date.now()}`,
        amount,
        description,
        webhookUrl: `${process.env.APP_URL}/webhooks/saligpay`,
        returnUrl: `${process.env.APP_URL}/success`,
        contact: { name, email },
    });

    return c.json({ checkoutUrl: checkout.checkoutUrl });
});

// Webhook handler
app.post("/webhooks/saligpay", async (c) => {
    const body = await c.req.json();

    try {
        const payload = saligpay.webhooks.constructEvent(body);

        if (payload.status === "COMPLETED") {
            // Update your database
            console.log(`Payment ${payload.externalId} completed!`);
        }

        return c.json({ received: true });
    } catch (error) {
        return c.json({ error: "Invalid webhook" }, 400);
    }
});

export default app;

NestJS

Service

// src/saligpay/saligpay.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { SaligPay, CreateCheckoutOptions } from "saligpay-node";

@Injectable()
export class SaligPayService implements OnModuleInit {
    private client: SaligPay;

    constructor(private configService: ConfigService) {
        this.client = new SaligPay({
            clientId: this.configService.get("SALIGPAY_CLIENT_ID"),
            clientSecret: this.configService.get("SALIGPAY_CLIENT_SECRET"),
            env: this.configService.get("NODE_ENV") === "production"
                ? "production"
                : "sandbox",
        });
    }

    async onModuleInit() {
        await this.client.authenticate();
    }

    async createCheckout(options: CreateCheckoutOptions) {
        await this.client.ensureAuthenticated();
        return this.client.checkout.create(options);
    }

    parseWebhook(body: unknown) {
        return this.client.webhooks.constructEvent(body);
    }
}

Controller

// src/saligpay/saligpay.controller.ts
import { Controller, Post, Body, Res, HttpStatus } from "@nestjs/common";
import { Response } from "express";
import { SaligPayService } from "./saligpay.service";
import { OrdersService } from "../orders/orders.service";

@Controller("webhooks")
export class WebhooksController {
    constructor(
        private saligpayService: SaligPayService,
        private ordersService: OrdersService,
    ) {}

    @Post("saligpay")
    async handleWebhook(@Body() body: unknown, @Res() res: Response) {
        try {
            const payload = this.saligpayService.parseWebhook(body);

            if (payload.status === "COMPLETED") {
                await this.ordersService.markAsPaid(payload.externalId);
            }

            return res.status(HttpStatus.OK).json({ received: true });
        } catch (error) {
            return res.status(HttpStatus.BAD_REQUEST).json({
                error: "Invalid webhook",
            });
        }
    }
}

Elysia (Bun)

// src/index.ts
import { Elysia } from "elysia";
import { SaligPay } from "saligpay-node";

const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID!,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET!,
    env: "sandbox",
});

const app = new Elysia()
    .post("/checkout", async ({ body }) => {
        await saligpay.ensureAuthenticated();

        const checkout = await saligpay.checkout.create({
            externalId: `order-${Date.now()}`,
            amount: body.amount,
            description: body.description,
            webhookUrl: `${process.env.APP_URL}/webhooks/saligpay`,
            returnUrl: `${process.env.APP_URL}/success`,
            contact: { name: body.name, email: body.email },
        });

        return { checkoutUrl: checkout.checkoutUrl };
    })
    .post("/webhooks/saligpay", async ({ body, set }) => {
        try {
            const payload = saligpay.webhooks.constructEvent(body);

            if (payload.status === "COMPLETED") {
                console.log(`Payment ${payload.externalId} completed!`);
            }

            return { received: true };
        } catch {
            set.status = 400;
            return { error: "Invalid webhook" };
        }
    })
    .listen(3000);

console.log(`Server running at ${app.server?.hostname}:${app.server?.port}`);

Troubleshooting

Authentication Issues

Problem: Getting AuthenticationError

// Solution: Verify credentials are correct
const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID, // Double-check
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET, // Double-check
    env: "sandbox", // Ensure correct environment
});

// Test authentication
try {
    await saligpay.authenticate();
    console.log("Authentication successful!");
} catch (error) {
    console.error("Auth failed:", error);
}

Token Expiration

Problem: AuthenticationError: Invalid access token

// Solution: Use ensureAuthenticated()
await saligPay.ensureAuthenticated();

// This automatically refreshes expired tokens
const checkout = await saligpay.checkout.create(options);

Validation Errors

Problem: ValidationError: Amount must be greater than 0

// Solution: Validate input before API call
const createCheckout = async (options: CreateCheckoutOptions) => {
    // Client-side validation
    if (options.amount <= 0) {
        throw new Error("Amount must be greater than 0");
    }

    if (!options.externalId) {
        throw new Error("External ID is required");
    }

    return saligpay.checkout.create(options);
};

Webhook Issues

Problem: Invalid JSON payload

// Solution: Ensure raw body is passed to webhook handler
app.use(express.raw({ type: "application/json" }));

app.post("/webhooks/saligpay", async (req, res) => {
    await saligpay.webhooks.listen(req, res, handler);
});

Environment Setup

Problem: Module not found or import errors

// Solution: Ensure Node.js 18+ is installed
// Check Node version
console.log(process.version); // Should be v18.x.x or higher

// If using TypeScript, ensure tsconfig.json has correct module settings
{
    "compilerOptions": {
        "module": "NodeNext",
        "moduleResolution": "NodeNext",
        "target": "ES2022"
    }
}

Security Best Practices

1. Never Commit Secrets

# .gitignore
.env
.env.local
.env.*.local

2. Use Environment Variables

// ✅ Good - Use environment variables
const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET,
});

// ❌ Bad - Hardcoded credentials
const saligpay = new SaligPay({
    clientId: "hardcoded-client-id",
    clientSecret: "hardcoded-secret",
});

3. Validate Webhook Origin

// Store and verify webhook secret (future feature)
const WEBHOOK_SECRET = process.env.SALIGPAY_WEBHOOK_SECRET;

// Always validate payload structure
app.post("/webhooks/saligpay", async (req, res) => {
    try {
        const payload = saligpay.webhooks.constructEvent(req.body);

        // Additional validation
        if (!payload.externalId || !payload.status) {
            return res.status(400).send({ error: "Invalid payload" });
        }

        // Process webhook
        await handlePayment(payload);

        return res.send({ received: true });
    } catch (error) {
        console.error("Webhook error:", error);
        return res.status(400).send({ error: "Invalid webhook" });
    }
});

4. Use Sandbox Environment

// Always use sandbox for development
const saligpay = new SaligPay({
    clientId: process.env.SALIGPAY_CLIENT_ID,
    clientSecret: process.env.SALIGPAY_CLIENT_SECRET,
    env: process.env.NODE_ENV === "production" ? "production" : "sandbox",
});

5. Implement Rate Limiting

import rateLimit from "express-rate-limit";

const webhookLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // Limit each IP to 100 requests per windowMs
});

app.post("/webhooks/saligpay", webhookLimiter, async (req, res) => {
    await saligpay.webhooks.listen(req, res, handler);
});

6. Secure Webhook Endpoints

// Use HTTPS in production
// Add authentication headers to webhooks (if needed)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

app.post("/webhooks/saligpay", async (req, res) => {
    // Verify webhook secret (future enhancement)
    const signature = req.headers["x-webhook-signature"];
    if (signature !== WEBHOOK_SECRET) {
        return res.status(401).send({ error: "Unauthorized" });
    }

    // Process webhook
    await saligpay.webhooks.listen(req, res, handler);
});

API Reference

SaligPay (Main Client)

| Method | Returns | Description | | ----------------------- | ----------------------------- | ---------------------------------------- | | authenticate() | Promise<SaligPayAuthTokens> | Authenticate and store tokens | | isAuthenticated() | boolean | Check if currently authenticated | | getAccessToken() | string \| undefined | Get current access token | | ensureAuthenticated() | Promise<void> | Ensure authentication, refresh if needed |

AuthResource

| Method | Returns | Description | | --------------------------------------------------------- | ----------------------------- | ----------------------- | | authenticate(clientId?, clientSecret?) | Promise<SaligPayAuthTokens> | Get access tokens | | refreshToken(refreshToken) | Promise<SaligPayAuthTokens> | Refresh access token | | loginAndRetrieveCredentials(email, password, adminKey?) | Promise<LoginResult> | Full login flow | | validateToken(accessToken) | Promise<boolean> | Validate token validity |

CheckoutResource

| Method | Returns | Description | | ------------------------------- | ------------------------------------ | ------------------------ | | create(options, accessToken?) | Promise<CreateCheckoutApiResponse> | Create checkout session | | setAccessToken(token) | void | Set default access token |

WebhookResource

| Method | Returns | Description | | --------------------------- | ------------------------ | -------------------------- | | constructEvent(payload) | SaligPayWebhookPayload | Parse webhook body | | listen(req, res, handler) | Promise<void> | Express middleware handler | | process(payload, handler) | Promise<void> | Manual processing |

License

MIT © SaligPay

Support

  • 📧 Email: [email protected]
  • 📚 Documentation: https://docs.saligpay.com
  • 🐛 Report Issues: https://github.com/saligpay/node-sdk/issues
  • 💬 Discord: https://discord.gg/saligpay

Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.


Built with ❤️ by SaligPay