Getting started
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`.
{
"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):
{
"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
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)
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)
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)
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)
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)