Webhook Documentation

This documentation explains how to integrate with and process payment notifications from dSociopay. When a payment is completed successfully, dSociopay sends a webhook notification to your specified endpoint. The webhook payload includes key transaction details such as payment status, amount, sender, receiver, and other pertinent information.

Webhook Notification Format

The webhook sends a JSON payload to your endpoint structured as follows:


{
  "event": "payment_received",
  "data": {
    "transaction_id": "PLwUu2dPJ2kSJSHcUNn3cT",
    "amount": "10.0",
    "fee": "0.0992",
    "settlement": "9.9008",
    "currency": "NGN",
    "customer": {
      "name": "dSocio (dSociosms)",
      "email": "[email protected]",
      "account_number": "0776620402"
    }
  }
}

Webhook Data Breakdown

  • transaction_id: A unique identifier assigned to the transaction.

  • amount: The total amount paid by the customer in the transaction currency.

  • settlement: The net amount to be settled after applicable fees have been deducted.

  • fee: The fee charged and deducted from the payment before settlement.

Customer Details

Information about the customer associated with the transaction:

  • name: Customer's full name.

  • email: Customer's email address.

  • account_number: Customer's account number.

Webhook Integration Examples

import hashlib
import hmac
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt

# Secret key shared between dSociopay and the webhook receiver
# This is used to generate and verify HMAC signatures
security_key = 'xxx'  # Move this to environment variables in production


@csrf_exempt  # Disable CSRF because webhooks are server-to-server requests
def webhook(request):

    # Only allow POST requests for webhook delivery
    if request.method != 'POST':
        return JsonResponse({"error": "Invalid method"}, status=405)

    # Retrieve signature sent by dSociopay in request headers
    signature = request.headers.get('dSociopay-Signature')

    # Reject request if signature is missing
    if not signature:
        return JsonResponse({"error": "Missing signature"}, status=400)

    # Raw request body (IMPORTANT: must use raw bytes for signature verification)
    webhook_data = request.body

    # Generate our own signature using the same secret key and payload
    calculated_signature = hmac.new(
        security_key.encode('utf-8'),  # Convert secret key to bytes
        webhook_data,                   # Raw request body (bytes)
        hashlib.sha256                 # Hashing algorithm
    ).hexdigest()

    # Compare signatures securely (prevents timing attacks)
    if not hmac.compare_digest(calculated_signature, signature):
        return JsonResponse({"error": "Invalid signature"}, status=403)

    try:
        # Convert JSON bytes into Python dictionary
        data = json.loads(webhook_data.decode("utf-8"))

        # Extract transaction details from webhook payload
        transaction_id = data.get('transaction_id')
        amount_paid = data.get('amount')
        settlement_amount = data.get('settlement')
        currency = data.get('currency')

        # OPTIONAL: Prevent duplicate processing (idempotency check)
        # This ensures the same transaction is not credited twice
        # if Transaction.objects.filter(transaction_id=transaction_id).exists():
        #     return JsonResponse({"status": "duplicate"}, status=200)

        # Process the transaction (e.g., update wallet, store record, etc.)
        print(f"Transaction ID: {transaction_id}")
        print(f"Amount Paid: {amount_paid}")
        print(f"Settlement Amount: {settlement_amount}")
        print(f"Currency: {currency}")

        # Return success response to acknowledge webhook receipt
        return JsonResponse({"status": "success"}, status=200)

    except json.JSONDecodeError:
        # Handle cases where payload is not valid JSON
        return JsonResponse({"error": "Invalid JSON format"}, status=400)
```php
<?php

// Secret key shared between dSociopay and your application
// Store this in an environment variable in production
$securityKey = 'xxx';

// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    echo json_encode([
        'error' => 'Invalid method'
    ]);
    exit;
}

// Retrieve signature from request headers
$signature = $_SERVER['HTTP_DSOCIOPAY_SIGNATURE'] ?? null;

// Reject request if signature is missing
if (!$signature) {
    http_response_code(400);
    echo json_encode([
        'error' => 'Missing signature'
    ]);
    exit;
}

// Get the raw request body
$webhookData = file_get_contents('php://input');

// Generate HMAC-SHA256 signature
$calculatedSignature = hash_hmac(
    'sha256',
    $webhookData,
    $securityKey
);

// Compare signatures securely
if (!hash_equals($calculatedSignature, $signature)) {
    http_response_code(403);
    echo json_encode([
        'error' => 'Invalid signature'
    ]);
    exit;
}

// Decode JSON payload
$data = json_decode($webhookData, true);

if (json_last_error() !== JSON_ERROR_NONE) {
    http_response_code(400);
    echo json_encode([
        'error' => 'Invalid JSON format'
    ]);
    exit;
}

// Extract transaction details
$transactionId = $data['transaction_id'] ?? null;
$amountPaid = $data['amount_paid'] ?? null;
$settlementAmount = $data['settlement_amount'] ?? null;
$currency = $data['currency'] ?? null;

// OPTIONAL: Prevent duplicate processing
// if (transactionExists($transactionId)) {
//     http_response_code(200);
//     echo json_encode(['status' => 'duplicate']);
//     exit;
// }

// Process the transaction
error_log("Transaction ID: " . $transactionId);
error_log("Amount Paid: " . $amountPaid);
error_log("Settlement Amount: " . $settlementAmount);
error_log("Currency: " . $currency);

// Acknowledge successful receipt
http_response_code(200);
echo json_encode([
    'status' => 'success'
]);
```


To verify that a webhook request was genuinely sent by dSociopay and has not been modified in transit, you should validate the webhook signature. This is done by generating a hash of the webhook payload using your secret security key and comparing the result with the value provided in the dSociopay-Signature request header.

Below is the updated webhook documentation, including a Python and PHP example that demonstrates how to perform signature verification.


Verifying the dSociopay Signature

How Signature Verification Works

To ensure that a webhook request originates from dSociopay and has not been altered during transmission, every webhook request includes a dSociopay-Signature header. This signature is generated by hashing the raw JSON payload using your private key.

When your server receives a webhook request, it should independently generate a hash from the received payload using the same private key and compare it with the signature provided in the request header. A matching signature confirms that the request is authentic and has not been tampered with.

Steps to Verify the Signature

  1. Retrieve the Signature

    • Extract the dSociopay-Signature value from the incoming request headers.
  2. Obtain the Raw Payload

    • Read the raw JSON payload exactly as it was received in the request body.
  3. Generate a Verification Hash

    • Using your private key and the same hashing algorithm used by dSociopay (for example, HMAC-SHA256), generate a hash from the raw payload.
  4. Compare the Signatures

    • Compare the hash you generated with the value of the dSociopay-Signature header.
  5. Validate the Request

    • If both signatures match, the webhook request is valid and can be processed.
    • If the signatures do not match, reject the request as it may have been modified or originated from an unauthorized source.