saligpay-node
v0.1.2
Published
SaligPay Node.js SDK - Type-safe client for SaligPay payment integration
Maintainers
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.
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-nodeQuick 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 paymentTable of Contents
- Configuration
- Authentication
- Checkout Sessions
- Webhooks
- Error Handling
- TypeScript Usage
- CommonJS Usage
- Testing
- Server-Side Usage
- Troubleshooting
- Security Best Practices
- Support
Configuration
Environment Variables
Create a .env file (recommended):
SALIGPAY_CLIENT_ID=your_client_id
SALIGPAY_CLIENT_SECRET=your_client_secret
SALIGPAY_ENV=sandboxSDK 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:
- Sign in — Authenticate user with email/password
- Get merchant — Retrieve associated merchant details
- Register OAuth — Create OAuth credentials for the merchant (idempotent)
- 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.*.local2. 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
