Callback

 
Queries on Transactions

Callback Configuration


Payelu Webhook Callback Documentation

Introduction

Payelu sends real-time notifications about transaction status through a webhook configured in your merchant dashboard. This document explains how to properly implement and validate the callback endpoint to securely receive these notifications.

What is a Webhook?

A webhook is a URL on your server that Payelu will automatically call when a transaction status changes (payment received, payment completed, error, etc.). Your application must be prepared to receive and process these notifications.

Notification Flow

  1. Payelu processes a transaction
  1. The transaction status changes (PENDING → COMPLETED, for example)
  1. Payelu sends an HTTP POST request to your callback URL
  1. Your server validates the security and processes the information
  1. Your server responds with HTTP 200 to confirm receipt

Callback Structure

HTTP Method

  • Method: POST
  • Content-Type: application/json

Received Data

{ "transaction_id": "abc123xyz789", "api_key": 1234567890, "security_hash": "a1b2c3d4e5f6...", "status": "COMPLETED", "message": "Transaction completed successfully", "reference": "ORDER-12345", "updated_at": "2025-01-15T10:30:00Z", "pay_type": "payin" }

Callback Fields

Field
Type
Required
Description
transaction_id
string
Yes
Unique transaction identifier in Payelu
api_key
integer
Yes
Random 10-digit number used for security validation
security_hash
string
Yes
Security hash generated with your credentials
status
string
Yes
Current status: PENDING, COMPLETED, ERROR
message
string
Yes
Status description or additional information
reference
string
No
Merchant reference assigned to the transaction
updated_at
string
No
ISO 8601 timestamp of the last update
pay_type
string
No
Transaction type: payin or payout

Additional Fields (Merchant ID = 1 - Prov_11 & Prov_17)

For certain merchants, additional fields are included:
Field
Type
Description
sender_name
string
Payer name (payin)
sender_document_number
string
Payer document number (payin)
beneficiary_name
string
Receiver name (payout)
beneficiary_document_number
string
Receiver document number (payout)
beneficiary_bank_name
string
Receiver bank name (payout)
endToEndId
string
End-to-end transaction ID

Possible Statuses

  • PENDING: Transaction in process, awaiting confirmation
  • COMPLETED: Transaction completed successfully
  • ERROR: Error in transaction processing

Security Validation

⚠️ Important: Hash Validation

You must ALWAYS validate the received security_hash to ensure that the notification actually comes from Payelu and has not been tampered with.

Validation Algorithm

The hash is generated using HMAC-SHA256:
message = api_key + auth_point_id security_hash = HMAC-SHA256(auth_api_token, message)

Required Parameters

  • auth_api_token: Your API token (get it from the Payelu dashboard)
  • auth_point_id: Your Point ID in UUID format (get it from the dashboard)
  • api_key: The 10-digit number received in the callback

🔴 Critical Warning about api_key

The api_key MUST be treated as an integer, not as a string:
  • Correct: 1234567890 (integer)
  • Incorrect: "1234567890" (string)
  • Incorrect: "0123456789" (with leading zeros)
Valid range: 1 to 9,999,999,999 (maximum 10 digits)
If you convert the api_key to a string or add additional formatting, the hash validation will fail.

Implementation Examples

Python (FastAPI)

from fastapi import APIRouter, Request, HTTPException from pydantic import BaseModel, Field from typing import Optional import hmac import hashlib import os router = APIRouter() class CallbackData(BaseModel): transaction_id: str api_key: int security_hash: str status: str message: str reference: Optional[str] = None updated_at: Optional[str] = None pay_type: Optional[str] = None def validate_security_hash(auth_api_token: str, auth_point_id: str, api_key: int) -> str: """ Generates the security hash for validation """ part1 = str(api_key).encode('utf-8') part2 = str(auth_point_id).encode('utf-8') message = part1 + part2 secret = auth_api_token.encode('utf-8') return hmac.new(secret, message, hashlib.sha256).hexdigest() @router.post("/Payelu/callback") async def Payelu_callback(request: Request): try: # Parse callback data data = await request.json() callback_data = CallbackData(**data) # Get credentials (use environment variables) AUTH_API_TOKEN = os.getenv("Payelu_API_TOKEN") AUTH_POINT_ID = os.getenv("Payelu_POINT_ID") # Validate security hash calculated_hash = validate_security_hash( AUTH_API_TOKEN, AUTH_POINT_ID, callback_data.api_key ) if not hmac.compare_digest(calculated_hash, callback_data.security_hash): raise HTTPException(status_code=401, detail="Invalid security hash") # Process based on status if callback_data.status == "COMPLETED": # Update order as paid print(f"Payment completed for transaction {callback_data.transaction_id}") # TODO: Update database, send emails, etc. elif callback_data.status == "PENDING": # Transaction in process print(f"Payment pending for transaction {callback_data.transaction_id}") elif callback_data.status == "ERROR": # Handle error print(f"Payment error: {callback_data.message}") # Respond with success return {"status": "success"} except Exception as e: print(f"Error processing callback: {str(e)}") raise HTTPException(status_code=500, detail=str(e))

PHP

<?php header('Content-Type: application/json'); /** * Validates the callback security hash */ function validateSecurityHash($authApiToken, $authPointId, $apiKey) { $part1 = strval($apiKey); $part2 = strval($authPointId); $message = $part1 . $part2; return hash_hmac('sha256', $message, $authApiToken); } /** * Processes the Payelu callback */ function processPayeluCallback() { // Get callback data $rawData = file_get_contents('php://input'); $data = json_decode($rawData, true); if (json_last_error() !== JSON_ERROR_NONE) { http_response_code(400); echo json_encode(['error' => 'Invalid JSON']); return; } // Validate required fields $requiredFields = ['transaction_id', 'api_key', 'security_hash', 'status', 'message']; foreach ($requiredFields as $field) { if (!isset($data[$field])) { http_response_code(400); echo json_encode(['error' => "Missing field: $field"]); return; } } // Get credentials (use environment variables) $authApiToken = getenv('Payelu_API_TOKEN'); $authPointId = getenv('Payelu_POINT_ID'); // Validate security hash $calculatedHash = validateSecurityHash( $authApiToken, $authPointId, intval($data['api_key']) // Important: convert to integer ); if (!hash_equals($calculatedHash, $data['security_hash'])) { http_response_code(401); echo json_encode(['error' => 'Invalid security hash']); return; } // Process based on status switch ($data['status']) { case 'COMPLETED': // Update order as paid error_log("Payment completed for transaction: " . $data['transaction_id']); // TODO: Update database, send emails, etc. break; case 'PENDING': // Transaction in process error_log("Payment pending for transaction: " . $data['transaction_id']); break; case 'ERROR': // Handle error error_log("Payment error: " . $data['message']); break; } // Respond with success http_response_code(200); echo json_encode(['status' => 'success']); } // Process the callback processPayeluCallback(); ?>

Java (Spring Boot)

package com.example.Payelu; import org.springframework.web.bind.annotation.*; import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.HexFormat; @RestController @RequestMapping("/api") public class PayeluCallbackController { // Inject from configuration or environment variables private final String AUTH_API_TOKEN = System.getenv("Payelu_API_TOKEN"); private final String AUTH_POINT_ID = System.getenv("Payelu_POINT_ID"); /** * Class to map callback data */ public static class CallbackData { public String transaction_id; public Long api_key; // Important: use Long, not String public String security_hash; public String status; public String message; public String reference; public String updated_at; public String pay_type; // Getters and setters omitted for brevity } /** * Validates the security hash */ private String validateSecurityHash(String authApiToken, String authPointId, Long apiKey) throws NoSuchAlgorithmException, InvalidKeyException { String part1 = String.valueOf(apiKey); String part2 = authPointId; String message = part1 + part2; Mac sha256Hmac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec( authApiToken.getBytes(StandardCharsets.UTF_8), "HmacSHA256" ); sha256Hmac.init(secretKey); byte[] hash = sha256Hmac.doFinal(message.getBytes(StandardCharsets.UTF_8)); return HexFormat.of().formatHex(hash); } /** * Endpoint to receive Payelu callbacks */ @PostMapping("/Payelu/callback") public ResponseEntity<String> handleCallback(@RequestBody CallbackData data) { try { // Validate security hash String calculatedHash = validateSecurityHash( AUTH_API_TOKEN, AUTH_POINT_ID, data.api_key ); if (!calculatedHash.equals(data.security_hash)) { return ResponseEntity .status(HttpStatus.UNAUTHORIZED) .body("{\"error\": \"Invalid security hash\"}"); } // Process based on status switch (data.status) { case "COMPLETED": // Update order as paid System.out.println("Payment completed: " + data.transaction_id); // TODO: Update database, send emails, etc. break; case "PENDING": // Transaction in process System.out.println("Payment pending: " + data.transaction_id); break; case "ERROR": // Handle error System.out.println("Payment error: " + data.message); break; } // Respond with success return ResponseEntity.ok("{\"status\": \"success\"}"); } catch (Exception e) { e.printStackTrace(); return ResponseEntity .status(HttpStatus.INTERNAL_SERVER_ERROR) .body("{\"error\": \"" + e.getMessage() + "\"}"); } } }

JavaScript (Node.js + Express)

const express = require('express'); const crypto = require('crypto'); const router = express.Router(); // Get credentials from environment variables const AUTH_API_TOKEN = process.env.Payelu_API_TOKEN; const AUTH_POINT_ID = process.env.Payelu_POINT_ID; /** * Validates the callback security hash */ function validateSecurityHash(authApiToken, authPointId, apiKey) { const part1 = String(apiKey); const part2 = String(authPointId); const message = part1 + part2; const hmac = crypto.createHmac('sha256', authApiToken); hmac.update(message); return hmac.digest('hex'); } /** * Securely compares two hashes (prevents timing attacks) */ function secureCompare(a, b) { if (a.length !== b.length) { return false; } return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); } /** * Endpoint to receive Payelu callbacks */ router.post('/Payelu/callback', express.json(), async (req, res) => { try { const data = req.body; // Validate required fields const requiredFields = ['transaction_id', 'api_key', 'security_hash', 'status', 'message']; for (const field of requiredFields) { if (!data[field]) { return res.status(400).json({ error: `Missing field: ${field}` }); } } // Convert api_key to integer const apiKey = parseInt(data.api_key, 10); if (isNaN(apiKey)) { return res.status(400).json({ error: 'Invalid api_key format' }); } // Validate security hash const calculatedHash = validateSecurityHash( AUTH_API_TOKEN, AUTH_POINT_ID, apiKey ); if (!secureCompare(calculatedHash, data.security_hash)) { return res.status(401).json({ error: 'Invalid security hash' }); } // Process based on status switch (data.status) { case 'COMPLETED': // Update order as paid console.log(`Payment completed for transaction ${data.transaction_id}`); // TODO: Update database, send emails, etc. break; case 'PENDING': // Transaction in process console.log(`Payment pending for transaction ${data.transaction_id}`); break; case 'ERROR': // Handle error console.log(`Payment error: ${data.message}`); break; } // Respond with success res.status(200).json({ status: 'success' }); } catch (error) { console.error('Error processing callback:', error); res.status(500).json({ error: error.message }); } }); module.exports = router;

Best Practices

1. Security

  • ALWAYS validate the security_hash before processing the callback
  • ✅ Use environment variables to store credentials
  • ✅ Implement rate limiting on your endpoint
  • ✅ Use HTTPS for your callback URL
  • ✅ Validate the format of all received fields

2. Asynchronous Processing

  • ✅ Respond quickly (< 5 seconds) with HTTP 200
  • ✅ Process business logic in the background (queues/workers)
  • ✅ Don't block the response with long operations

3. Idempotency

  • ✅ The same callback may arrive multiple times
  • ✅ Save the transaction_id to avoid duplicate processing
  • ✅ Implement duplicate checking before updating

4. Logging and Monitoring

  • ✅ Log all received callbacks
  • ✅ Alert on failed validations
  • ✅ Monitor your endpoint's response time

5. Error Handling

// Example of error response try { // Process callback } catch (error) { console.error('Error:', error); // Still respond with 200 if callback was received // Payelu will retry if it doesn't receive 200 return res.status(200).json({ status: 'received', note: 'Processing in background' }); }

Important:

Always respond with HTTP 200 once you've received and validated the data, even if complete processing will be done asynchronously.

Testing

Test Endpoint

You can test your implementation by sending a manual POST:
curl -X POST https://your-domain.com/api/Payelu/callback \ -H "Content-Type: application/json" \ -d '{ "transaction_id": "test_123", "api_key": 1234567890, "security_hash": "your_calculated_hash", "status": "COMPLETED", "message": "Test transaction", "reference": "ORDER-TEST", "updated_at": "2025-01-15T10:00:00Z", "pay_type": "payin" }'

Recommended Tools

  • Postman: For manual testing
  • ngrok: To expose localhost during development
  • RequestBin: To inspect callbacks in development

Dashboard Configuration

  1. Log in to your Payelu dashboard
  1. Go to SettingsWebhooks
  1. Enter your callback URL (must be HTTPS)
  1. Save and verify the configuration

Support

If you have problems implementing the webhook:
  • 📧 Email: support@Payelu.com
  • 📚 Documentation: https://docs.payelu.com
  • 💬 Chat: Available in the dashboard

Changelog

  • v1.0 (2025-01-15): Initial webhook documentation