List view
Getting Started
Getting Started
Authentication Guide
Authentication Guide
QR Bank Transfers Bolivia
QR Bank Transfers Bolivia
Real-Time Bank Transfers Argentina
Real-Time Bank Transfers Argentina
PIX Brazil
PIX Brazil
PagoMovil Venezuela
PagoMovil Venezuela
SPEI Mexico
SPEI Mexico
PSE Colombia
PSE Colombia
Khipu Chile
Khipu Chile
WebPay Chile
WebPay Chile
MACH Chile
MACH Chile
Bank Transfers Chile
Bank Transfers Chile
Real-Time Bank Transfers Paraguay
Real-Time Bank Transfers Paraguay
VISA & MasterCard
VISA & MasterCard
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
- Payelu processes a transaction
- The transaction status changes (PENDING → COMPLETED, for example)
- Payelu sends an HTTP POST request to your callback URL
- Your server validates the security and processes the information
- 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_hashbefore 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_idto 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
- Log in to your Payelu dashboard
- Go to Settings → Webhooks
- Enter your callback URL (must be HTTPS)
- 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
Callback ConfigurationPayelu Webhook Callback DocumentationIntroductionWhat is a Webhook?Notification FlowCallback StructureHTTP MethodReceived DataCallback FieldsAdditional Fields (Merchant ID = 1 - Prov_11 & Prov_17)Possible StatusesSecurity Validation⚠️ Important: Hash ValidationValidation AlgorithmRequired Parameters🔴 Critical Warning about api_keyImplementation ExamplesPython (FastAPI)PHPJava (Spring Boot)JavaScript (Node.js + Express)Best Practices1. Security2. Asynchronous Processing3. Idempotency4. Logging and Monitoring5. Error HandlingImportant: TestingTest EndpointRecommended ToolsDashboard ConfigurationSupportChangelog