Skip to main content

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

  1. Event Occurs: A payment event happens (e.g., payment successful, failed, etc.)
  2. Webhook Triggered: ResolutPay sends an HTTP POST request to your webhook URL
  3. Your Server Processes: Your server receives and processes the webhook data
  4. 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

  1. Log in to your ResolutPay Dashboard
  2. Navigate to SettingsWebhooks
  3. Enter your webhook URL (must be HTTPS)
  4. Select the events you want to receive
  5. 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

EventDescriptionWhen Triggered
charge.successPayment successfulPayment completed successfully
charge.failedPayment failedPayment failed or declined
charge.pendingPayment pendingPayment is being processed
transfer.successTransfer successfulMoney transfer completed
transfer.failedTransfer failedMoney transfer failed
refund.processedRefund processedRefund has been processed
subscription.createdSubscription createdNew subscription created
subscription.cancelledSubscription cancelledSubscription 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

  1. Go to webhook.site
  2. Copy the unique URL
  3. Use it as your webhook URL in the dashboard
  4. Monitor incoming webhooks

3. Test Events

You can trigger test webhooks from the dashboard:

  1. Go to SettingsWebhooks
  2. Click Send Test Webhook
  3. Select an event type
  4. Send the test webhook

Best Practices

  1. Always respond with 200: Even if processing fails, respond with 200 to prevent retries
  2. Verify signatures: Always verify webhook signatures for security
  3. Handle idempotency: Process webhooks idempotently to handle duplicates
  4. Log everything: Log all webhook events for debugging
  5. Use HTTPS: Always use HTTPS for webhook URLs in production
  6. Process asynchronously: Don't block webhook processing with long operations

Next Steps