Callbacks (Webhooks)
Callbacks allow your server to receive real-time notifications about transfer status changes from Paysera. Instead of polling the API, you receive automatic notifications when important events occur.
Overviewβ
A callback is an HTTP POST request from Paysera servers to your server, triggered by events that change the transfer status. Callbacks are enabled when creating a transfer by providing a callback URL.
Key Benefitsβ
- Real-time updates - Receive instant notifications about status changes
- Reduced API calls - No need to poll the API repeatedly
- Reliable delivery - Failed callbacks are automatically retried
- Simple integration - Standard HTTP POST requests
If your server is not accessible from the internet, you can poll the API directly to check transfer status. However, be aware of rate limits - callbacks are the recommended approach.
Transfer Status Eventsβ
Callbacks are triggered for the following transfer status changes:
- waiting_funds
- waiting_registration
- waiting_password
- reserved
- rejected
- revoked
- failed
- done
When: Transfer accepted by user, but insufficient funds in the selected account
Action Required: Wait for incoming funds
Next Status: reserved (when funds arrive) or failed
User approved transfer β Insufficient balance β waiting_funds
When: Transfer accepted, money reserved, but one or more beneficiaries not yet registered
Action Required: Beneficiaries must complete registration
Next Status: waiting_password or reserved
Transfer accepted β Beneficiary not registered β waiting_registration
When: Transfer accepted, money reserved, but password required for at least one payment
Action Required: Provide password (client or payment recipient)
Next Status: reserved
Transfer accepted β Password required β waiting_password
When: Transfer accepted, money reserved, all beneficiaries registered
Action Required: None - transfer will proceed
Next Status: done or failed
All conditions met β Money reserved β reserved β processing
When: Transfer rejected by the user
Action Required: None - transfer cancelled
Final Status: Yes
User declined transfer β rejected
When: Client revoked the transfer (API call)
Action Required: None - you initiated revocation
Final Status: Yes
Can Revoke: waiting, waiting_funds, waiting_registration, waiting_password, reserved statuses
Client revokes via API β revoked
When: Transfer failed due to an error
Action Required: Review error details, possibly create new transfer
Final Status: Yes
Processing error β failed
When: Transfer completed successfully, money received
Action Required: None - process complete
Final Status: Yes
Transfer processed β Money transferred β done
Status Flow Diagramβ
βββββββββββββββ
β Created β
ββββββββ¬βββββββ
β
ββββββββΌβββββββββββ
β User Approval β
ββββββββ¬βββ ββββββββ
β
βββββββββββββββΌββββββββββββββ
β β β
βββββΌββββ ββββββΌβββββ βββββΌβββββ
βRejectedβ βWaiting β βReservedβ
βββββββββ β(Funds/ β βββββ¬βββββ
βReg/Pass)β β
ββββββ¬βββββ β
β β
ββββββΌβββββ βββββΌββββ
β Revoked β β Done β
βββββββββββ βββββ¬ββββ
β
βββββΌββββ
βFailed β
βββββββββ
Callback Request Formatβ
HTTP Methodβ
POST /your-callback-url HTTP/1.1
Host: your-server.com
Content-Type: application/x-www-form-urlencoded
Parametersβ
Callbacks are sent with application/x-www-form-urlencoded encoding:
| Parameter | Type | Description |
|---|---|---|
transfer_id | integer | Unique transfer identifier |
status | string | New transfer status |
date | integer | UNIX timestamp of the event |
Unlike most API responses, callback data is sent as URL-encoded form data, not JSON.
Example Callback Requestβ
POST /webhooks/transfer HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
User-Agent: Paysera-Webhooks/1.0
transfer_id=239441503&status=done&date=1596014146
Response Requirementsβ
Success Responseβ
Your server must return HTTP status 200 (or any 2xx code) to acknowledge successful processing:
HTTP/1.1 200 OK
Content-Type: text/plain
OK
You only need to return a 2xx status code. The response body content doesn't matter.
Failure & Retriesβ
If your server returns any non-2xx status code, Paysera will retry the callback:
- Retry Schedule: Exponential backoff (1 min, 5 min, 15 min, 1 hour, etc.)
- Max Retries: Multiple attempts over several hours
- Timeout: Each request times out after 30 seconds
Do not return 3xx redirect status codes. Paysera won't follow redirects.
Implementation Examplesβ
- PHP
- Python
- Node.js
<?php
// webhook_handler.php
// Parse callback data
$transferId = $_POST['transfer_id'] ?? null;
$status = $_POST['status'] ?? null;
$date = $_POST['date'] ?? null;
// Validate required fields
if (!$transferId || !$status || !$date) {
http_response_code(400);
exit('Missing required parameters');
}
// Log the callback
error_log("Transfer #{$transferId} status changed to {$status} at " . date('Y-m-d H:i:s', $date));
try {
// Process the callback
switch ($status) {
case 'done':
// Transfer completed successfully
markOrderAsPaid($transferId);
sendConfirmationEmail($transferId);
break;
case 'failed':
case 'rejected':
case 'revoked':
// Transfer cancelled or failed
markOrderAsCancelled($transferId);
break;
case 'reserved':
// Money reserved, transfer will proceed
markOrderAsProcessing($transferId);
break;
default:
// Waiting status - log and continue
updateOrderStatus($transferId, $status);
break;
}
// Return success
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
// Log error and return failure (will trigger retry)
error_log("Callback processing failed: " . $e->getMessage());
http_response_code(500);
exit('Processing failed');
}
from flask import Flask, request
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/webhooks/transfer', methods=['POST'])
def transfer_callback():
# Parse form data
transfer_id = request.form.get('transfer_id')
status = request.form.get('status')
date = request.form.get('date')
# Validate
if not all([transfer_id, status, date]):
return 'Missing required parameters', 400
logging.info(f"Transfer #{transfer_id} status: {status}")
try:
# Process based on status
if status == 'done':
mark_order_paid(transfer_id)
send_confirmation(transfer_id)
elif status in ['failed', 'rejected', 'revoked']:
mark_order_cancelled(transfer_id)
elif status == 'reserved':
mark_order_processing(transfer_id)
else:
update_order_status(transfer_id, status)
return 'OK', 200
except Exception as e:
logging.error(f"Callback error: {e}")
return 'Processing failed', 500
if __name__ == '__main__':
app.run()
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
// Parse URL-encoded bodies
app.use(bodyParser.urlencoded({ extended: true }));
app.post('/webhooks/transfer', async (req, res) => {
const { transfer_id, status, date } = req.body;
// Validate
if (!transfer_id || !status || !date) {
return res.status(400).send('Missing required parameters');
}
console.log(`Transfer #${transfer_id} status changed to ${status}`);
try {
// Process callback
switch (status) {
case 'done':
await markOrderPaid(transfer_id);
await sendConfirmation(transfer_id);
break;
case 'failed':
case 'rejected':
case 'revoked':
await markOrderCancelled(transfer_id);
break;
case 'reserved':
await markOrderProcessing(transfer_id);
break;
default:
await updateOrderStatus(transfer_id, status);
break;
}
// Success
res.status(200).send('OK');
} catch (error) {
console.error('Callback processing error:', error);
res.status(500).send('Processing failed');
}
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});
Security Considerations
HTTPS Only
Always use HTTPS for your callback URL:
- β
https://example.com/webhooks/transfer - β
http://example.com/webhooks/transfer(insecure)
IP Whitelisting (Optional)
Consider restricting webhook access to Paysera IP addresses:
# Nginx example
location /webhooks/transfer {
allow 185.xxx.xxx.xxx; # Paysera IP range
deny all;
}
Contact Paysera support for the current list of webhook IP addresses.
Idempotency
Handle duplicate callbacks gracefully:
// Check if already processed
if (isCallbackAlreadyProcessed($transferId, $status)) {
http_response_code(200);
exit('Already processed');
}
// Process callback
processCallback($transferId, $status);
// Mark as processed
markCallbackAsProcessed($transferId, $status);
Validation
Always validate callback data:
- Required fields - Check all parameters exist
- Transfer ID - Verify it's a valid transfer in your system
- Status - Validate against known status values
- Timestamp - Check it's not too old (optional)
Testing Callbacks
Local Development with ngrok
For local testing, use ngrok to expose your local server:
# Start your local server on port 3000
node webhook-server.js
# In another terminal, start ngrok
ngrok http 3000
# Use the ngrok URL as your callback URL
# Example: https://abc123.ngrok.io/webhooks/transfer
Manual Testing
Create a test request:
curl -X POST https://your-server.com/webhooks/transfer \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "transfer_id=123456&status=done&date=1596014146"
Test All Statuses
Ensure your handler works for all possible statuses:
# Test each status
for status in waiting_funds waiting_registration waiting_password reserved rejected revoked failed done; do
curl -X POST https://your-server.com/webhooks/transfer \
-d "transfer_id=123&status=$status&date=$(date +%s)"
done
Troubleshooting
Callbacks Not Received
Problem: No callbacks arriving Solutions:
- Verify callback URL is accessible from internet
- Check firewall/security group settings
- Ensure HTTPS certificate is valid
- Test with curl from external server
Callbacks Failing
Problem: Callbacks retrying repeatedly Solutions:
- Check server logs for errors
- Verify you're returning
200status code - Ensure request timeout < 30 seconds
- Test callback handler in isolation
Duplicate Callbacks
Problem: Same callback received multiple times Solutions:
- Implement idempotency checks
- Check for network/timeout issues
- Ensure consistent
200response
Wrong Data Format
Problem: Cannot parse callback data Solutions:
- Remember:
application/x-www-form-urlencoded, not JSON - Use proper form parser (
$_POSTin PHP,body-parserin Node.js) - Check Content-Type header handling
Best Practices
1. Process Asynchronously
Don't block the callback response:
// β
Good: Queue for background processing
queueJob('process_transfer_callback', [
'transfer_id' => $transferId,
'status' => $status
]);
http_response_code(200);
echo 'OK';
// β Bad: Heavy processing in callback handler
performHeavyDatabaseOperations($transferId);
sendEmailsToCustomers($transferId);
updateExternalAPIs($transferId);
http_response_code(200);
2. Log Everything
Maintain comprehensive logs:
// Log incoming callback
logger()->info('Transfer callback received', [
'transfer_id' => $transferId,
'status' => $status,
'timestamp' => $date,
'ip' => $_SERVER['REMOTE_ADDR']
]);
3. Monitor Failures
Set up alerts for:
- Repeated callback failures
- Unexpected statuses
- Missing transfers
- Processing errors
4. Handle Edge Cases
// Check for unknown transfer
if (!transferExists($transferId)) {
logger()->warning("Callback for unknown transfer: {$transferId}");
http_response_code(200); // Still return success
exit('Unknown transfer');
}
// Handle status transitions
$currentStatus = getTransferStatus($transferId);
if (!isValidStatusTransition($currentStatus, $status)) {
logger()->error("Invalid status transition: {$currentStatus} -> {$status}");
}
5. Retry Logic
Implement your own retry for critical operations:
try {
processCallback($transferId, $status);
} catch (TemporaryException $e) {
// Queue for retry
retryLater($transferId, $status);
http_response_code(200); // Still return success
} catch (PermanentException $e) {
// Log and alert
logger()->critical("Callback processing failed permanently", [
'transfer_id' => $transferId,
'error' => $e->getMessage()
]);
http_response_code(500); // Trigger Paysera retry
}
Callback URL Configuration
Set the callback URL when creating a transfer:
{
"amount": {
"amount": "10.00",
"currency": "EUR"
},
"beneficiary": {
"type": "bank",
"name": "John Doe",
"bank_account": {
"iban": "LT123..."
}
},
"payer": {
"account_number": "EVP9876543210"
},
"purpose": {
"details": "Payment description"
},
"callback": {
"url": "https://your-server.com/webhooks/transfer"
}
}
Additional Resourcesβ
Supportβ
Need help with complex integrations?
Contact: tech_support@paysera.com