Webhooks Overview
Webhooks allow you to receive real-time notifications when payment events occur on ResolutPay. Instead of polling our API to check payment status, webhooks automatically send HTTP POST requests to your server when events happen.
How Webhooks Work
- Event Occurs: A payment event happens (e.g., payment successful, failed, etc.)
- Webhook Triggered: ResolutPay sends an HTTP POST request to your webhook URL
- Your Server Processes: Your server receives and processes the webhook data
- Response Sent: Your server responds with a 200 status code to acknowledge receipt
Webhook URL Setup
You can configure your webhook URL in two ways:
1. Dashboard Configuration
- Log in to your ResolutPay Dashboard
- Navigate to Settings → Webhooks
- Enter your webhook URL (must be HTTPS)
- Select the events you want to receive
- Save your configuration
2. API Configuration
const axios = require("axios");
const setupWebhook = async () => {
try {
const response = await axios.post(
"https://api.resolutpay.com/webhook",
{
url: "https://yourwebsite.com/webhook",
events: ["charge.success", "charge.failed", "transfer.success"],
},
{
headers: {
Authorization: "Bearer YOUR_SECRET_KEY",
"Content-Type": "application/json",
},
}
);
console.log("Webhook configured:", response.data);
} catch (error) {
console.error("Error setting up webhook:", error.response.data);
}
};
Webhook Security
1. Signature Verification
All webhooks include a signature header for verification:
const crypto = require("crypto");
const verifyWebhookSignature = (payload, signature, secret) => {
const expectedSignature = crypto
.createHmac("sha512", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
};
// In your webhook handler
app.post("/webhook", (req, res) => {
const signature = req.headers["x-resolutpay-signature"];
const payload = JSON.stringify(req.body);
if (
!verifyWebhookSignature(
payload,
signature,
process.env.RESOLUTPAY_WEBHOOK_SECRET
)
) {
return res.status(400).send("Invalid signature");
}
// Process webhook
res.status(200).send("OK");
});
2. HTTPS Requirement
Webhook URLs must use HTTPS in production. For testing, you can use services like:
Webhook Events
Event | Description | When Triggered |
---|---|---|
charge.success | Payment successful | Payment completed successfully |
charge.failed | Payment failed | Payment failed or declined |
charge.pending | Payment pending | Payment is being processed |
transfer.success | Transfer successful | Money transfer completed |
transfer.failed | Transfer failed | Money transfer failed |
refund.processed | Refund processed | Refund has been processed |
subscription.created | Subscription created | New subscription created |
subscription.cancelled | Subscription cancelled | Subscription cancelled |
Webhook Payload Structure
All webhooks follow this structure:
{
"event": "charge.success",
"data": {
"id": 123456,
"domain": "test",
"amount": 5000,
"currency": "GHS",
"status": "successful",
"reference": "REF_1234567890",
"tx_ref": "FLW1234567890",
"flw_ref": "FLW1234567890",
"device_fingerprint": "fingerprint",
"ip": "192.168.1.1",
"narration": "Payment for order",
"payment_type": "card",
"created_at": "2023-01-01T12:00:00.000Z",
"updated_at": "2023-01-01T12:05:00.000Z",
"customer": {
"id": 123,
"name": "John Doe",
"phone_number": "+2348012345678",
"email": "customer@example.com",
"created_at": "2023-01-01T12:00:00.000Z",
"updated_at": "2023-01-01T12:00:00.000Z"
},
"meta": {
"custom_fields": [
{
"display_name": "Invoice ID",
"variable_name": "invoice_id",
"value": "INV-001"
}
]
}
}
}
Complete Webhook Handler
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json());
app.post("/webhook", async (req, res) => {
try {
// Verify webhook signature
const signature = req.headers["x-resolutpay-signature"];
const payload = JSON.stringify(req.body);
if (
!verifyWebhookSignature(
payload,
signature,
process.env.RESOLUTPAY_WEBHOOK_SECRET
)
) {
console.error("Invalid webhook signature");
return res.status(400).send("Invalid signature");
}
const { event, data } = req.body;
// Handle different events
switch (event) {
case "charge.success":
await handlePaymentSuccess(data);
break;
case "charge.failed":
await handlePaymentFailed(data);
break;
case "charge.pending":
await handlePaymentPending(data);
break;
case "transfer.success":
await handleTransferSuccess(data);
break;
case "transfer.failed":
await handleTransferFailed(data);
break;
case "refund.processed":
await handleRefundProcessed(data);
break;
default:
console.log("Unhandled event:", event);
}
// Always respond with 200 to acknowledge receipt
res.status(200).send("OK");
} catch (error) {
console.error("Webhook processing error:", error);
// Still respond with 200 to prevent retries
res.status(200).send("OK");
}
});
// Event handlers
async function handlePaymentSuccess(data) {
console.log("Payment successful:", data.reference);
// Update order status
await updateOrderStatus(data.reference, "paid");
// Send confirmation email
await sendConfirmationEmail(data.customer.email, data);
// Update inventory
await updateInventory(data.meta);
}
async function handlePaymentFailed(data) {
console.log("Payment failed:", data.reference);
// Update order status
await updateOrderStatus(data.reference, "failed");
// Send failure notification
await sendFailureEmail(data.customer.email, data);
}
async function handlePaymentPending(data) {
console.log("Payment pending:", data.reference);
// Update order status
await updateOrderStatus(data.reference, "pending");
}
async function handleTransferSuccess(data) {
console.log("Transfer successful:", data.reference);
// Update transfer status
await updateTransferStatus(data.reference, "completed");
}
async function handleTransferFailed(data) {
console.log("Transfer failed:", data.reference);
// Update transfer status
await updateTransferStatus(data.reference, "failed");
}
async function handleRefundProcessed(data) {
console.log("Refund processed:", data.reference);
// Update order status
await updateOrderStatus(data.reference, "refunded");
// Send refund confirmation
await sendRefundEmail(data.customer.email, data);
}
// Helper functions
async function updateOrderStatus(reference, status) {
// Update your database
console.log(`Updating order ${reference} to status: ${status}`);
}
async function sendConfirmationEmail(email, data) {
// Send confirmation email
console.log(`Sending confirmation email to: ${email}`);
}
async function sendFailureEmail(email, data) {
// Send failure notification
console.log(`Sending failure email to: ${email}`);
}
async function updateInventory(meta) {
// Update inventory based on metadata
console.log("Updating inventory");
}
async function updateTransferStatus(reference, status) {
// Update transfer status in database
console.log(`Updating transfer ${reference} to status: ${status}`);
}
async function sendRefundEmail(email, data) {
// Send refund confirmation
console.log(`Sending refund email to: ${email}`);
}
// Signature verification
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac("sha512", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.listen(3000, () => {
console.log("Webhook server running on port 3000");
});
Webhook Retry Logic
If your webhook endpoint doesn't respond with a 200 status code, ResolutPay will retry the webhook:
- Retry Schedule: 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours, 12 hours
- Max Retries: 6 attempts
- Timeout: 30 seconds per attempt
Testing Webhooks
1. Using ngrok
# Install ngrok
npm install -g ngrok
# Start your server
node server.js
# In another terminal, expose your local server
ngrok http 3000
# Use the ngrok URL as your webhook URL
# https://abc123.ngrok.io/webhook
2. Using webhook.site
- Go to webhook.site
- Copy the unique URL
- Use it as your webhook URL in the dashboard
- Monitor incoming webhooks
3. Test Events
You can trigger test webhooks from the dashboard:
- Go to Settings → Webhooks
- Click Send Test Webhook
- Select an event type
- Send the test webhook
Best Practices
- Always respond with 200: Even if processing fails, respond with 200 to prevent retries
- Verify signatures: Always verify webhook signatures for security
- Handle idempotency: Process webhooks idempotently to handle duplicates
- Log everything: Log all webhook events for debugging
- Use HTTPS: Always use HTTPS for webhook URLs in production
- Process asynchronously: Don't block webhook processing with long operations
Next Steps
- Webhook Events - Learn about specific webhook events
- Webhook Setup - Detailed setup instructions
- Webhook Security - Security best practices
- Webhook Testing - Testing webhook integration