Webhook Implementation
Technical guide for implementing Paysera webhooks with security best practices and code examples.
Request Structure​
Each webhook POST request contains two parameters:
| Parameter | Description |
|---|---|
data | Base64-encoded event information |
sign | Base64-encoded RSA signature of the data |
Processing Webhooks​
Step 1: Decode Parameters​
Both data and sign are encoded using a modified Base64 scheme:
- Replace characters:
-→+and_→/ - Decode using standard Base64
Step 2: Verify Signature​
- Signature uses RSA with SHA1 hash function
- Public certificate: https://www.paysera.com/download/public.key
- Verify signature before processing data
Step 3: Extract Event Data​
- Decode the verified
dataparameter - Parse URL-encoded string to extract event parameters
Step 4: Send Response​
- Return HTTP response starting with or equal to
OK - Response confirms successful receipt
Implementation Examples​
- PHP
- Python
- Node.js
- Java
- PHP (Library)
<?php
$publicKey = file_get_contents('https://www.paysera.com/download/public.key');
$sign = $_POST['sign'];
$data = $_POST['data'];
// Decode signature
$signReplaced = strtr($sign, array('-' => '+', '_' => '/'));
$signDecoded = base64_decode($signReplaced);
// Verify signature
if (openssl_verify($data, $signDecoded, $publicKey, OPENSSL_ALGO_SHA1) === 1) {
// Decode data
$dataReplaced = strtr($data, array('-' => '+', '_' => '/'));
$dataDecoded = base64_decode($dataReplaced);
parse_str($dataDecoded, $params);
// Process event parameters
// $params contains event data (type, amount, account, etc.)
// Confirm receipt
echo 'OK';
} else {
// Signature verification failed
http_response_code(400);
}
import base64
import urllib.parse
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.hazmat.backends import default_backend
import requests
# Load public key
public_key_pem = requests.get('https://www.paysera.com/download/public.key').content
public_key = load_pem_public_key(public_key_pem, backend=default_backend())
# Get POST parameters
data = request.form.get('data')
sign = request.form.get('sign')
# Decode signature
sign_replaced = sign.replace('-', '+').replace('_', '/')
sign_decoded = base64.b64decode(sign_replaced)
try:
# Verify signature
public_key.verify(
sign_decoded,
data.encode(),
padding.PKCS1v15(),
hashes.SHA1()
)
# Decode data
data_replaced = data.replace('-', '+').replace('_', '/')
data_decoded = base64.b64decode(data_replaced).decode()
params = urllib.parse.parse_qs(data_decoded)
# Process event parameters
# params contains event data (type, amount, account, etc.)
# Confirm receipt
return 'OK', 200
except Exception as e:
# Signature verification failed
return 'Invalid signature', 400
const crypto = require('crypto');
const https = require('https');
const querystring = require('querystring');
// Load public key
https.get('https://www.paysera.com/download/public.key', (res) => {
let publicKey = '';
res.on('data', (chunk) => publicKey += chunk);
res.on('end', () => processWebhook(publicKey));
});
function processWebhook(publicKey) {
const data = req.body.data;
const sign = req.body.sign;
// Decode signature
const signReplaced = sign.replace(/-/g, '+').replace(/_/g, '/');
const signDecoded = Buffer.from(signReplaced, 'base64');
// Verify signature
const verifier = crypto.createVerify('RSA-SHA1');
verifier.update(data);
if (verifier.verify(publicKey, signDecoded)) {
// Decode data
const dataReplaced = data.replace(/-/g, '+').replace(/_/g, '/');
const dataDecoded = Buffer.from(dataReplaced, 'base64').toString();
const params = querystring.parse(dataDecoded);
// Process event parameters
// params contains event data (type, amount, account, etc.)
// Confirm receipt
res.send('OK');
} else {
// Signature verification failed
res.status(400).send('Invalid signature');
}
}
import java.io.*;
import java.net.*;
import java.security.*;
import java.security.spec.*;
import java.util.*;
import java.util.Base64;
public class PayseraWebhookHandler {
public void handleWebhook(String data, String sign) throws Exception {
// Load public key
URL url = new URL("https://www.paysera.com/download/public.key");
BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream())
);
StringBuilder publicKeyPEM = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
publicKeyPEM.append(line).append("\n");
}
reader.close();
// Parse public key
String publicKeyContent = publicKeyPEM.toString()
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyContent);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
// Decode signature
String signReplaced = sign.replace('-', '+').replace('_', '/');
byte[] signDecoded = Base64.getDecoder().decode(signReplaced);
// Verify signature
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes());
if (signature.verify(signDecoded)) {
// Decode data
String dataReplaced = data.replace('-', '+').replace('_', '/');
String dataDecoded = new String(
Base64.getDecoder().decode(dataReplaced)
);
// Parse parameters
Map<String, String> params = parseQueryString(dataDecoded);
// Process event parameters
// params contains event data (type, amount, account, etc.)
// Confirm receipt
response.getWriter().write("OK");
} else {
// Signature verification failed
response.setStatus(400);
}
}
private Map<String, String> parseQueryString(String query) {
Map<String, String> params = new HashMap<>();
for (String param : query.split("&")) {
String[] pair = param.split("=");
if (pair.length == 2) {
params.put(
URLDecoder.decode(pair[0], "UTF-8"),
URLDecoder.decode(pair[1], "UTF-8")
);
}
}
return params;
}
}
Official PHP library for easy webhook integration.
Repository: GitHub - NotificationPhpClient
Installation​
composer require paysera/lib-notification-client
Usage Example​
use Paysera\NotificationClient\NotificationClient;
$client = new NotificationClient();
try {
$params = $client->validateAndParseNotification(
$_POST['data'],
$_POST['sign']
);
// Process the event
$type = $params['type'];
$amount = $params['amount'];
$statementId = $params['statement_id'];
// Your business logic here
echo 'OK';
} catch (\Exception $e) {
// Invalid signature or format
http_response_code(400);
}
Security Best Practices​
Essential Security Measures​
| Practice | Description |
|---|---|
| Verify Signatures | Always validate RSA signature before processing |
| Use HTTPS | Only accept webhooks over secure HTTPS connections |
| Check Statement ID | Implement idempotency using statement_id |
| Validate Data | Verify all parameters meet expected formats |
| Rate Limiting | Implement rate limiting on your endpoint |
| Error Handling | Log failures but don't expose sensitive data |
Idempotency Implementation​
Always check the statement_id to prevent duplicate processing:
// Check if this statement was already processed
if (!isStatementProcessed($params['statement_id'])) {
// Process the transaction
processPayment($params);
// Mark statement as processed
markStatementProcessed($params['statement_id']);
}
echo 'OK';
Public Key Caching​
Cache the public key to improve performance:
$publicKey = getFromCache('paysera_public_key');
if (!$publicKey) {
$publicKey = file_get_contents('https://www.paysera.com/download/public.key');
saveToCache('paysera_public_key', $publicKey, 86400); // Cache for 24h
}
Response Requirements​
Success Response​
Your endpoint must return a response that:
- Starts with or equals
OK - Returns within reasonable timeout (recommended: < 5 seconds)
Valid Responses:
OK
OK - Processed
OK: Statement 123456789 received
Error Responses​
For invalid requests:
- Return HTTP 400 Bad Request
- Do not return 'OK'
- Log the error for investigation
if (!$signatureValid) {
http_response_code(400);
error_log("Invalid signature for statement: " . $_POST['data']);
exit;
}
Troubleshooting​
Common Issues​
| Issue | Possible Cause | Solution |
|---|---|---|
| Webhook not received | Firewall blocking requests | Check firewall settings, whitelist Paysera IPs |
| Signature verification fails | Incorrect public key or decoding | Download latest public key, verify decoding logic |
| Duplicate notifications | No idempotency check | Implement statement_id checking |
| Invalid data format | Incorrect Base64 decoding | Replace - with + and _ with / before decoding |
| Timeout errors | Slow response from endpoint | Optimize processing, respond with 'OK' immediately |
Debug Mode​
Enable detailed logging during development:
error_log("Received webhook data: " . print_r($params, true));
error_log("Statement ID: " . $params['statement_id']);
error_log("Amount: " . $params['amount'] . " " . $params['currency']);
Example Use Cases​
- E-commerce
- Accounting
- Balance Monitoring
- Subscriptions
- Multi-Currency
- Invoicing
Payment Confirmation​
Automatically update order status when payment is received:
if ($params['type'] === 'MK' && $params['credit'] === '1') {
// Incoming payment received
$orderId = extractOrderId($params['details']);
// Update order status
updateOrderStatus($orderId, 'paid');
// Send confirmation email
sendConfirmationEmail($orderId);
// Update inventory
releaseReservedStock($orderId);
}
Full Example:
function processEcommercePayment($params) {
// Extract order ID from payment details
preg_match('/Order #(\d+)/', $params['details'], $matches);
$orderId = $matches[1] ?? null;
if (!$orderId) {
error_log("No order ID found in payment details");
return;
}
// Update order
$order = getOrder($orderId);
$order->status = 'paid';
$order->payment_date = date('Y-m-d H:i:s', $params['created_at']);
$order->transaction_id = $params['transfer_id'];
$order->save();
// Send emails
sendCustomerConfirmation($orderId);
sendAdminNotification($orderId);
// Trigger fulfillment
triggerFulfillment($orderId);
}
Automated Transaction Recording​
Automatically record transactions in your accounting system:
$transaction = [
'date' => date('Y-m-d', $params['created_at']),
'type' => $params['type'],
'amount' => $params['amount'],
'currency' => $params['currency'],
'description' => $params['details'],
'reference' => $params['statement_id'],
'payer' => $params['payer_name'] ?? null,
'account' => $params['account']
];
saveToAccounting($transaction);
Full Example:
function recordAccountingTransaction($params) {
$transaction = [
'date' => date('Y-m-d H:i:s', $params['created_at']),
'statement_id' => $params['statement_id'],
'type' => mapTransactionType($params['type']),
'direction' => $params['credit'] === '1' ? 'incoming' : 'outgoing',
'amount' => $params['amount'],
'currency' => $params['currency'],
'description' => $params['details'],
'account_number' => $params['account'],
'counterparty' => $params['credit'] === '1'
? $params['payer_name']
: $params['beneficiary_name'],
'counterparty_account' => $params['credit'] === '1'
? $params['payer_account']
: $params['beneficiary_account'],
'reference' => $params['reference_number'] ?? null
];
// Save to accounting database
$db->table('transactions')->insert($transaction);
// Update account balance
updateAccountBalance($params['account'], $params['amount'], $params['credit']);
// Generate accounting entry
createDoubleEntry($transaction);
}
function mapTransactionType($type) {
$types = [
'MK' => 'payment',
'HO' => 'deposit',
'FX' => 'exchange',
'MM' => 'other'
];
return $types[$type] ?? 'unknown';
}
Real-time Balance Alerts​
Monitor account balance and send alerts:
if ($params['credit'] === '1') {
// Incoming funds
notifyTeam("Payment received: {$params['amount']} {$params['currency']}");
} else {
// Outgoing funds
checkLowBalance($params['account']);
}
Full Example:
function monitorBalance($params) {
$account = $params['account'];
$amount = floatval($params['amount']);
$currency = $params['currency'];
// Get current balance
$currentBalance = getAccountBalance($account, $currency);
if ($params['credit'] === '1') {
// Incoming payment
$newBalance = $currentBalance + $amount;
// Notify about large deposits
if ($amount >= 1000) {
notifyAdmin([
'type' => 'large_deposit',
'amount' => $amount,
'currency' => $currency,
'from' => $params['payer_name'],
'balance' => $newBalance
]);
}
} else {
// Outgoing payment
$newBalance = $currentBalance - $amount;
// Check for low balance
$threshold = getBalanceThreshold($account, $currency);
if ($newBalance < $threshold) {
notifyAdmin([
'type' => 'low_balance_alert',
'account' => $account,
'balance' => $newBalance,
'threshold' => $threshold,
'currency' => $currency
]);
}
}
// Update balance cache
updateBalanceCache($account, $currency, $newBalance);
}
Subscription Management​
Handle recurring payments and subscription renewals:
function handleSubscriptionPayment($params) {
// Check if this is a subscription payment
if (strpos($params['details'], 'Subscription') === false) {
return;
}
// Extract user ID
preg_match('/User #(\d+)/', $params['details'], $matches);
$userId = $matches[1] ?? null;
if (!$userId) {
error_log("No user ID in subscription payment");
return;
}
// Get subscription
$subscription = getActiveSubscription($userId);
if (!$subscription) {
error_log("No active subscription for user $userId");
return;
}
// Extend subscription
$currentExpiry = $subscription->expires_at;
$newExpiry = date('Y-m-d', strtotime($currentExpiry . ' +1 month'));
$subscription->expires_at = $newExpiry;
$subscription->last_payment_date = date('Y-m-d H:i:s', $params['created_at']);
$subscription->last_payment_amount = $params['amount'];
$subscription->status = 'active';
$subscription->save();
// Send confirmation
sendSubscriptionRenewalEmail($userId, $newExpiry);
// Grant access
grantUserAccess($userId);
}
Failed Payment Handling:
function handleFailedSubscription($userId) {
$subscription = getActiveSubscription($userId);
// Mark as payment failed
$subscription->status = 'payment_required';
$subscription->save();
// Send reminder email
sendPaymentReminderEmail($userId);
// Suspend after 3 days
scheduleAccountSuspension($userId, days: 3);
}
Currency Exchange Handling​
Process currency exchange events:
function handleCurrencyExchange($params) {
if ($params['type'] !== 'FX') {
return; // Not a currency exchange
}
$exchange = [
'from_amount' => $params['from_amount'],
'from_currency' => $params['from_currency'],
'to_amount' => $params['to_amount'],
'to_currency' => $params['to_currency'],
'exchange_rate' => $params['to_amount'] / $params['from_amount'],
'account' => $params['account'],
'statement_id' => $params['statement_id'],
'created_at' => date('Y-m-d H:i:s', $params['created_at'])
];
// Record exchange
saveExchangeRecord($exchange);
// Update balances
decreaseBalance($params['account'], $params['from_currency'], $params['from_amount']);
increaseBalance($params['account'], $params['to_currency'], $params['to_amount']);
// Calculate and record exchange fee
$marketRate = getMarketRate($params['from_currency'], $params['to_currency']);
$actualRate = $exchange['exchange_rate'];
$fee = calculateExchangeFee($params['from_amount'], $marketRate, $actualRate);
recordExchangeFee($fee, $exchange);
}
Automatic Invoice Matching​
Match payments to invoices automatically:
function matchPaymentToInvoice($params) {
// Extract invoice number from payment details
preg_match('/INV-(\d+)/', $params['details'], $matches);
$invoiceNumber = $matches[1] ?? null;
if (!$invoiceNumber) {
// Try reference field
$invoiceNumber = $params['reference_to_beneficiary'] ?? null;
}
if (!$invoiceNumber) {
error_log("Cannot match payment to invoice - no invoice number");
return;
}
// Find invoice
$invoice = getInvoiceByNumber($invoiceNumber);
if (!$invoice) {
error_log("Invoice $invoiceNumber not found");
return;
}
// Check amount
if (abs($invoice->amount - $params['amount']) > 0.01) {
// Partial payment or amount mismatch
handlePartialPayment($invoice, $params);
return;
}
// Mark invoice as paid
$invoice->status = 'paid';
$invoice->paid_date = date('Y-m-d H:i:s', $params['created_at']);
$invoice->payment_reference = $params['statement_id'];
$invoice->save();
// Send receipt
sendInvoiceReceipt($invoice);
// Update customer account
updateCustomerBalance($invoice->customer_id, -$params['amount']);
}
Next Steps​
- Event Reference → - Learn about all event types and parameters
- Webhooks Overview → - Return to main documentation
Support​
- 📧 Email: support@paysera.com
- 📞 Phone: Contact Information