# Webhook (reference)

> Each run that debits your wallet is reported to your registered webhook endpoint, in the same format as a direct purchase. `data.reference` is the run's transaction reference (it starts with the schedule reference), and `transactionId` matches the schedule's `last_run.transaction_id`.

```json
{
  "eventType": "transaction.successful",
  "type": "data",
  "data": {
    "reference": "SCH-7F3K9QX2AB12-K3P9XQ2M",
    "amount": "1,000.00",
    "chargedAmount": "970.00",
    "commission": "30.00",
    "biller": "MTN 1GB - 30 Days",
    "customerId": "08031234567",
    "token": "0",
    "unit": "0",
    "voucher": "0",
    "currency": "NGN",
    "transactionId": "API-17596704001234567",
    "transactionDate": "2026-11-01 09:00:12",
    "status": "SUCCESSFUL",
    "message": "Your transaction was successful"
  }
}
```

Paybeta webhooks include a cryptographic signature in the `Signature` header to ensure the webhook originated from **Paybeta** and hasn't been tampered with.

## Signature Generation
The value passed inside the Signature header is calculated using a Hash-based Message Authentication Code (HMAC) with a SHA-256 hashing algorithm.

- **Algorithm**: HMAC-SHA256
- **Key**: Your API `Secret Key` (keep this hidden on your backend server environment).
- **Data**: The reference value located inside the transaction data block (`data.reference`).

### Sample Request

**Sent for every recipient once it is charged (and again if a pending one is later confirmed or reversed):**

``` PHP
{
  "eventType": "transaction.successful|transaction.pending|transaction.failed",
  "type": "vtu|tv|gaming|data|utility|education|insurance|travel",
  "data": {
    "reference": "REF-3AQ780KP87WS1356",
    "amount": "300.00",
    "chargedAmount": "292.50",
    "commission": "7.50",
    "biller": "MTN Airtime",
    "customerId": "08030000000",
    "token": "0",
    "unit": "0",
    "bonusToken": "0",
    "voucher": "0",
    "currency": "NGN",
    "transactionId": "API-4780819357880920990",
    "transactionDate": "2025-03-07 16:29:17",
    "status": "SUCCESSFUL",
    "message": "Your transaction was successful"
  }
}
```

## How to Verify the Signature

- Capture the incoming raw JSON payload and extract the data.reference string.
- Read the value present inside the Signature request header.
- Compute an **HMAC-SHA256** hash using the transaction reference as the data string and your merchant secret key as the signing key.
- Compare your locally computed string with the header value using a **constant-time string comparison** method to safeguard against timing analysis side-channel attacks.

Code Verification Code Examples




**PHP / Laravel**

 
    
    ```PHP
  
    <?php

    namespace App\Http\Controllers;

    use Illuminate\Http\Request;

    class PaybetaWebhookController extends Controller
    {
        public function handle(Request $request)
        {
            $payload = $request->json()->all();
            $headerSignature = $request->header('Signature');

            // Extract transaction reference
            $reference = $payload['data']['reference'] ?? '';

            // Retrieve your secret key securely from configuration
            $secretKey = config('services.paybeta.secret_key');

            // Calculate expected signature
            $computedSignature = hash_hmac('sha256', $reference, $secretKey);

            // Safe string comparison preventing timing attacks
            if (!hash_equals($computedSignature, $headerSignature)) {
                return response()->json(['error' => 'Invalid Signature'], 401);
            }

            // Process transaction event logic below
            // ...

            return response()->json(['status' => 'success'], 200);
        }
    }
    ```



**Node.js (Express)**

    ``` JS
    const express = require('express');
    const crypto = require('crypto');
    const app = express();

    app.use(express.json());

    app.post('/webhooks/paybeta', (req, res) => {
        const headerSignature = req.headers['signature'];
        const payload = req.body;

        // Extract transaction reference
        const reference = payload?.data?.reference || '';
        const secretKey = process.env.PAYBETA_SECRET_KEY;

        // Compute signature
        const computedSignature = crypto
            .createHmac('sha256', secretKey)
            .update(reference)
            .digest('hex');

        // Safe timing-attack-resistant comparison
        try {
            const isVerified = crypto.timingSafeEqual(
                Buffer.from(computedSignature, 'utf-8'),
                Buffer.from(headerSignature, 'utf-8')
            );

            if (!isVerified) {
                return res.status(401).json({ error: 'Invalid Signature' });
            }
        } catch (e) {
            return res.status(401).json({ error: 'Verification failed' });
        }

        // Process event data safely
        res.status(200).json({ status: 'success' });
    });

     ```


**C# (.NET Core / ASP.NET)**

    
```csharp

using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace PaybetaWebhook.Controllers
{
    [ApiController]
    [Route("webhooks")]
    public class PaybetaWebhookController : ControllerBase
    {
        private const string SecretKey = "your_secret_key_here";

        [HttpPost("paybeta")]
        public async Task<IActionResult> HandleWebhook()
        {
            // 1. Extract the Signature from headers
            if (!Request.Headers.TryGetValue("Signature", out var headerSignature))
            {
                return Unauthorized(new { error = "Missing Signature header" });
            }

            // 2. Read and parse the incoming body stream
            using var reader = new StreamReader(Request.Body);
            var rawJson = await reader.ReadToEndAsync();
            
            using var jsonDoc = JsonDocument.Parse(rawJson);
            
            // Navigate safely to payload string: data.reference
            string reference = "";
            if (jsonDoc.RootElement.TryGetProperty("data", out var dataElement) && 
                dataElement.TryGetProperty("reference", out var refElement))
            {
                reference = refElement.GetString() ?? "";
            }

            // 3. Compute expected HMAC-SHA256 signature
            var keyBytes = Encoding.UTF8.GetBytes(SecretKey);
            var referenceBytes = Encoding.UTF8.GetBytes(reference);

            using var hmac = new HMACSHA256(keyBytes);
            var hashBytes = hmac.ComputeHash(referenceBytes);
            
            // Convert byte array to hexadecimal string representation
            var computedSignature = Convert.ToHexString(hashBytes).ToLower();

            // 4. Safe constant-time string comparison
            if (!CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(computedSignature), 
                Encoding.UTF8.GetBytes(headerSignature.ToString())))
            {
                return Unauthorized(new { error = "Invalid Signature" });
            }

            // Execute core backend settlement workflow here
            return Ok(new { status = "success" });
        }
    }
}
```


**Java (Spring Boot)**

    
```java
package com.paybeta.webhook.controller;

import org.json.JSONObject;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

@RestController
@RequestMapping("/webhooks")
public class PaybetaWebhookController {

    private static final String SECRET_KEY = "your_secret_key_here";

    @PostMapping("/paybeta")
    public ResponseEntity<String> handleWebhook(
            @RequestHeader("Signature") String headerSignature,
            @RequestBody String rawJsonPayload) {

        try {
            // 1. Parse JSON block to extract the transaction reference string
            JSONObject jsonObject = new JSONObject(rawJsonPayload);
            String reference = jsonObject.getJSONObject("data").optString("reference", "");

            // 2. Initialize HMAC-SHA256 Engine
            Mac sha256HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(
                SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
            );
            sha256HMAC.init(secretKeySpec);

            // 3. Calculate Signature Bytes and encode to Hex string representation
            byte[] rawMacBytes = sha256HMAC.doFinal(reference.getBytes(StandardCharsets.UTF_8));
            StringBuilder hexStringBuilder = new StringBuilder();
            for (byte b : rawMacBytes) {
                hexStringBuilder.append(String.format("%02x", b));
            }
            String computedSignature = hexStringBuilder.toString();

            // 4. Secure constant-time evaluation to block execution timing attacks
            boolean signaturesMatch = MessageDigest.isEqual(
                    computedSignature.getBytes(StandardCharsets.UTF_8),
                    headerSignature.getBytes(StandardCharsets.UTF_8)
            );

            if (!signaturesMatch) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                        .body("{\"error\": \"Invalid Signature\"}");
            }

            // Process your platform updates here safely
            return ResponseEntity.ok("{\"status\": \"success\"}");

        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("{\"error\": \"Processing exception occurred\"}");
        }
    }
}

```


**Python (Flask)**

    
```python

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

PAYBETA_SECRET_KEY = "your_secret_key_here"

@app.route('/webhooks/paybeta', methods=['POST'])
def paybeta_webhook():
    header_signature = request.headers.get('Signature')
    payload = request.get_json() or {}
    
    # Extract reference path string
    transaction_data = payload.get('data', {})
    reference = transaction_data.get('reference', '')

    # Generate expected hash match
    computed_signature = hmac.new(
        key=PAYBETA_SECRET_KEY.encode('utf-8'),
        msg=reference.encode('utf-8'),
        digestmod=hashlib.sha256
    ).hexdigest()

    # Compare safely
    if not hmac.compare_digest(computed_signature, header_signature):
        return jsonify({"error": "Invalid Signature"}), 401

    # Execute system update routines
    return jsonify({"status": "success"}), 200

if __name__ == '__main__':
    app.run(port=5000)
```


