4Handling Webhooks
SaligPay sends webhook notifications to update you about payment status changes. Set up a webhook endpoint to receive these updates and update your database accordingly.
Webhook Payload Structure
The webhook payload contains comprehensive information about the payment transaction and its current status:
Sample Webhook Payload
{
"id": "txn_456def...",
"externalId": "ext_20250523_xyz123...",
"amount": 10000,
"description": "Premium Subscription",
"paymentMethod": {
"type": "credit_card"
},
"paymentSessionId": "session_123abc...",
"status": "SUCCESS",
"contact": {
"email": "customer@example.com",
"phoneNumber": "1234567890",
"name": "John Doe"
},
"creditCardDetails": {
"cardNumber": "****1234",
"expiryMonth": "12",
"expiryYear": "2025",
"cardholderName": "John Doe"
}
}Webhook Handler Implementation
Implement a webhook handler to process these notifications:
Webhook Handler
// In your webhook route handler
import { type ActionFunctionArgs } from "react-router";
import { PrismaClient, PaymentStatus } from "@prisma/client";
const prisma = new PrismaClient();
interface CreditCardDto {
cardNumber?: string;
expiryMonth?: string;
expiryYear?: string;
cardholderName?: string;
}
interface ContactDto {
email?: string;
phoneNumber?: string;
name?: string;
}
interface PaymentMethodDto {
type: string;
}
interface TransactionDataDto {
id?: string;
externalId?: string;
amount: number;
description?: string;
paymentMethod: PaymentMethodDto;
paymentSessionId?: string;
status?: string;
contact?: ContactDto;
creditCardDetails?: CreditCardDto;
}
export const action = async ({ request }: ActionFunctionArgs) => {
if (request.method !== "POST") {
return {
error: "Method not allowed",
status: 405,
};
}
try {
const payload = (await request.json()) as TransactionDataDto;
console.log("Webhook received:", JSON.stringify(payload, null, 2));
if (!payload.amount || !payload.externalId) {
console.error("Missing required fields in payload:", payload);
return {
error: "Missing required fields",
status: 400,
};
}
const existingPayment = await prisma.payment.findUnique({
where: { externalId: payload.externalId },
include: { statusHistory: true },
});
if (!existingPayment) {
console.error(
`Payment with externalId ${payload.externalId} not found`
);
return {
error: `Payment with externalId ${payload.externalId} not found`,
status: 404,
};
}
console.log(
`Processing webhook for payment ${existingPayment.id}, current status: ${existingPayment.status}`
);
console.log(`Incoming webhook status: ${payload.status}`);
let paymentStatus: PaymentStatus;
const incomingStatus = (payload.status || "").toUpperCase();
switch (incomingStatus) {
case "COMPLETED":
case "SUCCESS":
case "PAID":
paymentStatus = PaymentStatus.COMPLETED;
break;
case "FAILED":
case "FAILURE":
case "ERROR":
paymentStatus = PaymentStatus.FAILED;
break;
case "REFUNDED":
paymentStatus = PaymentStatus.REFUNDED;
break;
case "CANCELLED":
case "CANCELED":
paymentStatus = PaymentStatus.CANCELLED;
break;
case "PROCESSING":
case "PENDING":
paymentStatus = PaymentStatus.PROCESSING;
break;
default:
console.error(`Unknown payment status: ${payload.status}`);
return {
error: `Unknown payment status: ${payload.status}`,
status: 400,
};
}
// Skip updating if the status hasn't changed
if (existingPayment.status === paymentStatus) {
console.log(`Payment status unchanged, still ${paymentStatus}`);
return {
message: "Payment status unchanged",
status: 200,
};
}
// Update the payment status
await prisma.payment.update({
where: { id: existingPayment.id },
data: {
status: paymentStatus,
updatedAt: new Date(),
statusHistory: {
create: {
status: paymentStatus,
timestamp: new Date(),
metadata: JSON.stringify(payload),
},
},
},
});
console.log(`Payment updated successfully, new status: ${paymentStatus}`);
// Trigger other relevant business logic based on status change
if (paymentStatus === PaymentStatus.COMPLETED) {
// Handle successful payment (e.g., fulfill order, send confirmation, etc.)
await fulfillOrder(existingPayment.id);
} else if (paymentStatus === PaymentStatus.FAILED) {
// Handle failed payment (e.g., notify customer, update inventory, etc.)
await handleFailedPayment(existingPayment.id);
}
return {
message: "Webhook processed successfully",
status: 200,
};
} catch (error) {
console.error("Error processing webhook:", error);
return {
error: "Error processing webhook",
details: error instanceof Error ? error.message : String(error),
status: 500,
};
}
};
// Example of other functions that might be triggered by webhook status changes
async function fulfillOrder(paymentId: string) {
console.log(`Fulfilling order for payment: ${paymentId}`);
// Implementation details...
}
async function handleFailedPayment(paymentId: string) {
console.log(`Handling failed payment: ${paymentId}`);
// Implementation details...
}Security Tip: Always verify webhook signatures to ensure the request comes from SaligPay and hasn't been tampered with.
Webhook Signature Verification
To ensure webhook security, verify the signature:
Webhook Signature Verification
// Verify the webhook signature to ensure it came from SaligPay
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const crypto = require('crypto');
// Generate an HMAC with SHA256
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expectedSignature = hmac.digest('hex');
// Use crypto.timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage in your webhook handler
export const action = async ({ request }: ActionFunctionArgs) => {
// Get the payload as text first
const rawBody = await request.text();
// Get the signature from header
const signature = request.headers.get('saligpay-signature');
// Verify the signature
if (!signature || !verifyWebhookSignature(
rawBody,
signature,
process.env.WEBHOOK_SECRET
)) {
return new Response('Invalid signature', { status: 401 });
}
// If verification passes, proceed with processing the webhook
const payload = JSON.parse(rawBody);
// Process the webhook payload
// ...
}