Skip to main content

Webhooks

Stay informed about payment events in real-time with Payvesselโ€™s reliable webhook system. Webhooks allow Payvessel to notify your application immediately when important events occur, such as successful payments, failed transactions, or account updates.

โšก Real-time Updates

Receive instant notifications when events occur

๐Ÿ”’ Secure Delivery

Cryptographically signed payloads for verification

Payment Notification Security

Critical security measures to protect your webhook endpoint from unauthorized access and ensure data integrity.

๐Ÿ” Security Implementation Requirements

1

Verify Payvessel Hash Signature

Validate HMAC SHA-512 signature to ensure data integrity
2

Verify Payvessel IP Address

Check that requests originate from trusted Payvessel servers
3

Prevent Duplicate Transactions

Implement transaction history checks to avoid duplicate processing

Webhook Security Implementation

๐Ÿ›ก๏ธ Hash Signature Verification

When receiving data from a webhook, itโ€™s crucial to ensure the data hasnโ€™t been tampered with during transmission:
Extract the Payvessel signature from the requestโ€™s metadata. This will be available in the HTTP_PAYVESSEL_HTTP_SIGNATURE header.
Use your secret key (PVSECRET-) as the key for an HMAC with the SHA-512 algorithm. The payload of the webhook request is used as the message input for this HMAC function.
Compare the generated hash with the Payvessel signature received in the requestโ€™s metadata. If they match, the data hasnโ€™t been tampered with.

๐ŸŒ IP Address Verification

Validate that incoming webhook requests originate from trusted Payvessel servers: Trusted IP Addresses:
  • 3.255.23.38
  • 162.246.254.36
If the IP address doesnโ€™t match the trusted list, reject the request as it may be unauthorized.

๐Ÿ”„ Duplicate Transaction Prevention

Webhooks can sometimes be delivered multiple times due to network issues or retries:
  • Transaction History Check: Query your payment transaction database to check if a transaction with the same reference already exists
  • Duplicate Handling: If a matching transaction is found, ignore the duplicate request
  • Idempotency: Ensure your webhook processing is idempotent

Implementation Examples

๐Ÿ Python Django Webhook Handler

Complete Django implementation with security verification:
import json
import hmac
import hashlib
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

@require_POST
@csrf_exempt
def payvessel_payment_done(request):
    payload = request.body
    payvessel_signature = request.META.get('HTTP_PAYVESSEL_HTTP_SIGNATURE')
    
    # Get IP address (may differ depending on your server setup)
    # ip_address = u'{}'.format(request.META.get('HTTP_X_FORWARDED_FOR'))
    ip_address = u'{}'.format(request.META.get('REMOTE_ADDR'))
    
    # Your secret key
    secret = bytes("PVSECRET-", 'utf-8')
    hashkey = hmac.new(secret, request.body, hashlib.sha512).hexdigest()
    
    # Trusted IP addresses
    ipAddress = ["3.255.23.38", "162.246.254.36"]
    
    # Verify signature and IP address
    if payvessel_signature == hashkey and ip_address in ipAddress:
        data = json.loads(payload)
        amount = float(data['order']['amount'])
        settlementAmount = float(data['order']['settlement_amount'])
        fee = float(data['order']['fee'])
        reference = data['transaction']['reference']
        description = data['order']['description']
        
        # Check if reference already exists in your payment transaction table   
        if not paymentgateway.objects.filter(reference=reference).exists():
            # Fund user wallet here
            # ... your business logic ...
            return JsonResponse({"message": "success"}, status=200) 
        else:
            return JsonResponse({"message": "transaction already exist"}, status=200) 
    else:
        return JsonResponse({"message": "Permission denied, invalid hash or ip address."}, status=400)

๐Ÿ˜ PHP Webhook Handler

Secure PHP implementation for webhook processing:
<?php

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $payload = file_get_contents('php://input');
    $payvessel_signature = $_SERVER['HTTP_PAYVESSEL_HTTP_SIGNATURE'];
    
    // Get IP address (may differ depending on your server setup)
    // $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR']; 
    $ip_address = $_SERVER['REMOTE_ADDR']; 
    
    // Your secret key
    $secret = "PVSECRET-";
    $hashkey = hash_hmac('sha512', $payload, $secret);
    
    // Trusted IP addresses
    $ipAddress = ["3.255.23.38", "162.246.254.36"];
     
    // Verify signature and IP address
    if ($payvessel_signature == $hashkey && in_array($ip_address, $ipAddress)) {
        $data = json_decode($payload, true);
        $amount = floatval($data['order']['amount']);
        $settlementAmount = floatval($data['order']['settlement_amount']);
        $fee = floatval($data['order']['fee']);
        $reference = $data['transaction']['reference'];
        $description = $data['order']['description'];

        // Check if reference already exists in your payment transaction table
        if (!paymentgateway::where('reference', $reference)->exists()) {
            // Fund user wallet here
            // ... your business logic ...
            echo json_encode(["message" => "success"]);
            http_response_code(200);
        } else {
            echo json_encode(["message" => "transaction already exist"]);
            http_response_code(200);
        }
    } else {
        echo json_encode(["message" => "Permission denied, invalid hash or ip address."]);
        http_response_code(400);
    }
} else {
    echo json_encode(["message" => "Method not allowed"]);
    http_response_code(405);
}
?>

๐ŸŸจ Node.js Express Webhook Handler

Complete Node.js implementation with security verification:
const crypto = require('crypto');
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;

// Use raw body parser for webhook signature verification
app.use('/payvessel_payment_done', bodyParser.raw({type: 'application/json'}));

app.post('/payvessel_payment_done', (req, res) => {
  const payload = req.body;
  const payvessel_signature = req.header('HTTP_PAYVESSEL_HTTP_SIGNATURE');
  
  // Get IP address
  const ip_address = req.connection.remoteAddress || 
                    req.socket.remoteAddress || 
                    req.headers['x-forwarded-for'];
  
  // Your secret key
  const secret = 'PVSECRET-';
  
  // Generate hash for verification
  const hash = crypto.createHmac('sha512', secret)
    .update(payload)
    .digest('hex');
  
  // Trusted IP addresses
  const ipAddress = ["3.255.23.38", "162.246.254.36"];
  
  // Verify signature and IP address
  if (payvessel_signature === hash && ipAddress.includes(ip_address)) {
    const data = JSON.parse(payload);
    const amount = parseFloat(data.order.amount);
    const settlementAmount = parseFloat(data.order.settlement_amount);
    const fee = parseFloat(data.order.fee);
    const reference = data.transaction.reference;
    const description = data.order.description;

    // Check if reference already exists in your payment transaction table
    // Replace this with your actual database check
    checkTransactionExists(reference)
      .then(exists => {
        if (!exists) {
          // Fund user wallet here
          // ... your business logic ...
          res.status(200).json({ message: 'success' });
        } else {
          res.status(200).json({ message: 'transaction already exist' });
        }
      })
      .catch(error => {
        console.error('Database error:', error);
        res.status(500).json({ message: 'internal server error' });
      });
  } else {
    res.status(400).json({ message: 'Permission denied, invalid hash or ip address.' });
  }
});

// Example database check function
async function checkTransactionExists(reference) {
  // Replace with your actual database query
  // Example: return await PaymentTransaction.findOne({ reference });
  return false; // Placeholder
}

app.listen(port, () => {
  console.log(`Webhook server running on port ${port}`);
});

๐Ÿ’Ž Ruby on Rails Webhook Handler

Rails implementation with security verification:
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  def payvessel_payment_done
    payload = request.body.read
    payvessel_signature = request.headers['HTTP_PAYVESSEL_HTTP_SIGNATURE']
    ip_address = request.remote_ip
    
    # Your secret key
    secret = 'PVSECRET-'
    hashkey = OpenSSL::HMAC.hexdigest('SHA512', secret, payload)
    
    # Trusted IP addresses
    trusted_ips = ['3.255.23.38', '162.246.254.36']
    
    # Verify signature and IP address
    if payvessel_signature == hashkey && trusted_ips.include?(ip_address)
      data = JSON.parse(payload)
      amount = data['order']['amount'].to_f
      settlement_amount = data['order']['settlement_amount'].to_f
      fee = data['order']['fee'].to_f
      reference = data['transaction']['reference']
      description = data['order']['description']
      
      # Check if reference already exists
      unless PaymentTransaction.exists?(reference: reference)
        # Fund user wallet here
        # ... your business logic ...
        render json: { message: 'success' }, status: 200
      else
        render json: { message: 'transaction already exist' }, status: 200
      end
    else
      render json: { message: 'Permission denied, invalid hash or ip address.' }, status: 400
    end
  end
end

โ˜• Java Spring Boot Webhook Handler

Spring Boot implementation with security verification:
@RestController
@RequestMapping("/webhook")
public class WebhookController {
    
    private static final String SECRET = "PVSECRET-";
    private static final List<String> TRUSTED_IPS = Arrays.asList("3.255.23.38", "162.246.254.36");
    
    @PostMapping("/payvessel_payment_done")
    public ResponseEntity<?> handlePayvesselWebhook(
            HttpServletRequest request,
            @RequestBody String payload) {
        
        try {
            String signature = request.getHeader("HTTP_PAYVESSEL_HTTP_SIGNATURE");
            String ipAddress = getClientIpAddress(request);
            
            // Verify signature
            String computedHash = computeHmacSha512(payload, SECRET);
            
            // Verify signature and IP
            if (signature.equals(computedHash) && TRUSTED_IPS.contains(ipAddress)) {
                ObjectMapper mapper = new ObjectMapper();
                JsonNode data = mapper.readTree(payload);
                
                double amount = data.get("order").get("amount").asDouble();
                double settlementAmount = data.get("order").get("settlement_amount").asDouble();
                double fee = data.get("order").get("fee").asDouble();
                String reference = data.get("transaction").get("reference").asText();
                String description = data.get("order").get("description").asText();
                
                // Check if transaction exists
                if (!paymentService.transactionExists(reference)) {
                    // Process payment
                    // ... your business logic ...
                    return ResponseEntity.ok(Map.of("message", "success"));
                } else {
                    return ResponseEntity.ok(Map.of("message", "transaction already exist"));
                }
            } else {
                return ResponseEntity.badRequest()
                    .body(Map.of("message", "Permission denied, invalid hash or ip address."));
            }
        } catch (Exception e) {
            return ResponseEntity.status(500)
                .body(Map.of("message", "Internal server error"));
        }
    }
    
    private String computeHmacSha512(String data, String secret) {
        try {
            Mac mac = Mac.getInstance("HmacSHA512");
            SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA512");
            mac.init(keySpec);
            byte[] hash = mac.doFinal(data.getBytes());
            return bytesToHex(hash);
        } catch (Exception e) {
            throw new RuntimeException("Error computing HMAC", e);
        }
    }
    
    private String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
    
    private String getClientIpAddress(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }
}

Webhook Payload Structure

๐Ÿ“‹ Standard Webhook Format

Payvessel webhook payloads contain transaction and order information:
{
  "order": {
    "amount": "1000.00",
    "settlement_amount": "970.00",
    "fee": "30.00",
    "description": "Payment for Order #12345",
    "currency": "USD",
    "status": "completed"
  },
  "transaction": {
    "reference": "TXN_1634567890_ABC123",
    "id": "pay_1234567890abcdef",
    "status": "successful",
    "created_at": "2024-10-27T10:00:00Z",
    "updated_at": "2024-10-27T10:01:00Z"
  },
  "customer": {
    "id": "cust_1234567890",
    "email": "[email protected]",
    "name": "John Doe"
  }
}

Testing Your Webhook Implementation

๐Ÿงช Local Development Setup

Test webhooks locally using ngrok or similar tools:
# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# In another terminal, expose local server
ngrok http 3000

# Use the ngrok URL in your webhook configuration
# Example: https://abc123.ngrok.io/payvessel_payment_done

๏ฟฝ Webhook Testing Checklist

1

Signature Verification

โœ… Verify HMAC SHA-512 signature matches โœ… Use correct secret key (PVSECRET-) โœ… Handle raw payload for hash calculation
2

IP Address Validation

โœ… Check against trusted IP list โœ… Handle different IP header formats โœ… Account for proxy configurations
3

Duplicate Prevention

โœ… Check transaction reference uniqueness โœ… Handle duplicate webhook deliveries โœ… Implement idempotent processing
4

Error Handling

โœ… Return appropriate HTTP status codes โœ… Log webhook events for debugging โœ… Handle malformed payloads gracefully

Webhook Best Practices

โœ… Implementation Guidelines

๏ฟฝ Performance

  • Respond within 30 seconds
  • Process asynchronously when possible
  • Return 200 status immediately
  • Use queues for heavy processing

๐Ÿ”’ Security

  • Always verify signatures
  • Validate IP addresses
  • Use HTTPS endpoints only
  • Log security events

๐Ÿ”„ Reliability Measures

Payvessel Retry Policy:
  • Immediate retry for 5xx errors
  • Exponential backoff for subsequent attempts
  • Up to 3 days of retry attempts
  • Manual replay available in dashboard
Store webhook data:
  • Log all incoming webhooks
  • Store raw payload for debugging
  • Track processing status
  • Maintain audit trails

Troubleshooting Common Issues

Common Causes:
  • Using wrong secret key
  • Modifying payload before verification
  • Incorrect HMAC algorithm (should be SHA-512)
  • Character encoding issues
Solutions:
  • Verify secret key format (starts with PVSECRET-)
  • Use raw payload for hash calculation
  • Ensure UTF-8 encoding
  • Check header name formatting
Common Causes:
  • Proxy or load balancer configuration
  • Different IP header formats
  • Firewall or NAT translation
Solutions:
  • Check X-Forwarded-For header
  • Handle multiple IP formats
  • Update trusted IP list if needed
  • Test with different IP detection methods
Common Causes:
  • Network timeouts causing retries
  • Multiple webhook endpoints
  • Race conditions in processing
Solutions:
  • Check transaction reference before processing
  • Use database transactions for atomicity
  • Implement proper locking mechanisms
  • Return success for already processed transactions
Security Critical: Always implement all three security measures (signature verification, IP validation, and duplicate prevention) to ensure the integrity and security of your webhook endpoint.