JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 12
  • Score
    100M100P100Q72641F
  • License MIT

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

Package Exports

  • saligpay-node
  • saligpay-node/errors
  • saligpay-node/types
  • saligpay-node/types/auth
  • saligpay-node/types/checkout
  • saligpay-node/types/config
  • saligpay-node/types/helpers
  • saligpay-node/types/payment
  • saligpay-node/types/webhook

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

Internal Authentication (Platform/Admin)

For platform integrations that need to authenticate merchants using only a user ID, use internalAuthentication. This method is similar to loginAndRetrieveCredentials but skips the email/password sign-in step and uses the admin key for authentication:

  1. Get merchant — Retrieve associated merchant details by user ID using admin key
  2. Register OAuth — Create OAuth credentials for the merchant (idempotent)
  3. Authenticate — Get access tokens using the new credentials

[!IMPORTANT] This method requires an Admin Key and is intended for platform-level operations where you already have the user ID.

import { SaligPay } from "saligpay-node";

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

// Authenticate using user ID
const result = await saligpay.auth.internalAuthentication(
    "user-id-from-your-system",
    "admin-key-or-leave-empty-to-use-config",
);

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

internalAuthentication Parameters

Parameter Type Required Description
userId string Yes The user's ID from your system
adminKey string No* Admin key (uses config if not provided)

*The adminKey parameter is optional if adminKey is set in the SDK configuration.

InternalAuthResult Structure

interface InternalAuthResult {
    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

Platform Backend Operations:

// Internal admin operations without password
async function authenticateMerchantByUserId(userId: string, adminKey?: string) {
    const saligpay = new SaligPay({
        adminKey: adminKey || process.env.SALIGPAY_ADMIN_KEY!,
        env: "production",
    });

    const result = await saligpay.auth.internalAuthentication(userId, adminKey);

    // Store credentials securely
    await db.merchants.update({
        where: { userId },
        data: {
            saligpayClientId: result.credentials.clientId,
            saligpayClientSecret: result.credentials.clientSecret,
            saligpayMerchantId: result.merchant.id,
            lastAuthenticatedAt: new Date(),
        },
    });

    return result;
}

Automated Service Authentication:

// Background job authenticating multiple merchants
async function refreshMerchantTokens(merchantIds: string[]) {
    const saligpay = new SaligPay({
        adminKey: process.env.SALIGPAY_ADMIN_KEY!,
        env: "production",
    });

    for (const userId of merchantIds) {
        try {
            const result = await saligpay.auth.internalAuthentication(userId);

            // Update stored tokens
            await db.tokens.update({
                where: { userId },
                data: {
                    accessToken: result.tokens.accessToken,
                    refreshToken: result.tokens.refreshToken,
                    expiresAt: result.tokens.expiresAt,
                },
            });
        } catch (error) {
            console.error(`Failed to authenticate user ${userId}:`, error);
        }
    }
}

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

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

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

        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.

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
internalAuthentication(userId, adminKey?) Promise<LoginResult> Internal auth by user ID
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

Contributing

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


Built with ❤️ by SaligPay