S
ShadhinPay Docs

Webhook Integration

Complete guide to handling ShadhinPay webhooks

Webhook Integration Guide

This guide walks you through setting up webhook handlers to receive real-time payment notifications.

Overview

Webhooks are HTTP POST requests that ShadhinPay sends to your server when events occur. They enable you to:

  • Update order status in real-time
  • Send confirmation emails to customers
  • Trigger fulfillment workflows
  • Keep your database in sync

Setting Up Your Webhook Endpoint

Step 1: Create an Endpoint

Create a POST endpoint that can receive webhook requests:

Node.js (Express)
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

app.post('/webhooks/shadhinpay', async (req, res) => {
  // 1. Verify signature
  const signature = req.headers['x-shadhinpay-signature'];
  if (!verifySignature(req.body, signature)) {
    return res.status(401).send('Invalid signature');
  }

  // 2. Process the event
  const { event, data } = req.body;

  try {
    await handleWebhookEvent(event, data);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).send('Error processing webhook');
  }
});

function verifySignature(payload, signature) {
  const expected = crypto
    .createHmac('sha256', process.env.SHADHINPAY_SECRET_KEY)
    .update(JSON.stringify(payload))
    .digest('hex');

  return `sha256=${expected}` === signature;
}

async function handleWebhookEvent(event, data) {
  switch (event) {
    case 'payment.completed':
      await fulfillOrder(data.payment_id, data.metadata.order_id);
      break;
    case 'payment.failed':
      await notifyCustomerOfFailure(data.customer_phone);
      break;
    case 'payment.refunded':
      await processRefund(data.payment_id);
      break;
  }
}

app.listen(3000);
Python (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os

app = Flask(__name__)

@app.route('/webhooks/shadhinpay', methods=['POST'])
def handle_webhook():
    # 1. Verify signature
    signature = request.headers.get('X-ShadhinPay-Signature')
    if not verify_signature(request.json, signature):
        return 'Invalid signature', 401

    # 2. Process the event
    event = request.json['event']
    data = request.json['data']

    try:
        handle_event(event, data)
        return 'OK', 200
    except Exception as e:
        print(f'Webhook error: {e}')
        return 'Error', 500

def verify_signature(payload, signature):
    secret = os.environ['SHADHINPAY_SECRET_KEY']
    expected = hmac.new(
        secret.encode(),
        json.dumps(payload).encode(),
        hashlib.sha256
    ).hexdigest()
    return f'sha256={expected}' == signature

def handle_event(event, data):
    if event == 'payment.completed':
        fulfill_order(data['payment_id'])
    elif event == 'payment.failed':
        notify_failure(data['customer_phone'])

Step 2: Register Your Endpoint

When creating payments, include your webhook URL:

const payment = await createPayment({
  amount: 1000,
  currency: 'BDT',
  callback_url: 'https://yoursite.com/webhooks/shadhinpay',
  // ... other fields
});

Handling Different Events

Payment Completed

The most important event - indicates successful payment:

async function handlePaymentCompleted(data) {
  const { payment_id, amount, transaction_id, metadata } = data;

  // 1. Verify the payment in your database
  const order = await Order.findOne({
    where: { payment_id }
  });

  if (!order) {
    console.error('Order not found for payment:', payment_id);
    return;
  }

  // 2. Update order status
  await order.update({
    status: 'PAID',
    transaction_id,
    paid_at: new Date()
  });

  // 3. Trigger fulfillment
  await fulfillOrder(order);

  // 4. Send confirmation email
  await sendConfirmationEmail(order);
}

Payment Failed

Handle failures gracefully:

async function handlePaymentFailed(data) {
  const { payment_id, customer_phone, metadata } = data;

  // Update order status
  await Order.update(
    { status: 'PAYMENT_FAILED' },
    { where: { payment_id } }
  );

  // Notify customer
  await sendSMS(customer_phone, 'Your payment could not be processed.');
}

Ensuring Idempotency

Webhooks may be delivered multiple times. Always check if you've already processed an event:

async function handleWebhookEvent(event, data) {
  const webhookId = `${event}_${data.payment_id}`;

  // Check if already processed
  const existing = await ProcessedWebhook.findOne({
    where: { webhook_id: webhookId }
  });

  if (existing) {
    console.log('Webhook already processed:', webhookId);
    return;
  }

  // Process the webhook
  await processWebhook(event, data);

  // Mark as processed
  await ProcessedWebhook.create({
    webhook_id: webhookId,
    processed_at: new Date()
  });
}

Testing Locally

Use ngrok to expose your local server:

# Install ngrok
npm install -g ngrok

# Start your server
node server.js

# In another terminal, start ngrok
ngrok http 3000

Use the ngrok URL as your callback_url:

https://abc123.ngrok.io/webhooks/shadhinpay

Troubleshooting

Webhooks Not Arriving

  1. Check URL accessibility - Ensure your endpoint is publicly accessible
  2. Verify HTTPS - We only send to HTTPS endpoints
  3. Check firewall - Allow incoming POST requests
  4. Review logs - Check your webhook dashboard for delivery attempts

Signature Verification Failing

  1. Use raw body - Don't modify the payload before verifying
  2. Check secret key - Ensure you're using the correct key
  3. Encoding - Use UTF-8 encoding for the payload

Duplicate Webhooks

  1. Implement idempotency - Track processed webhook IDs
  2. Check timeout - Return 200 within 30 seconds
  3. Async processing - Process heavy work in background jobs

On this page