Skip to main content

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
Alternative: Polling

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:

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

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:

ParameterTypeDescription
transfer_idintegerUnique transfer identifier
statusstringNew transfer status
dateintegerUNIX timestamp of the event
Not JSON

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
Simple Response

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
No Redirects

Do not return 3xx redirect status codes. Paysera won't follow redirects.

Implementation Examples​

<?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');
}
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;
}
info

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:

  1. Required fields - Check all parameters exist
  2. Transfer ID - Verify it's a valid transfer in your system
  3. Status - Validate against known status values
  4. 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 200 status 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 200 response

Wrong Data Format

Problem: Cannot parse callback data Solutions:

  • Remember: application/x-www-form-urlencoded, not JSON
  • Use proper form parser ($_POST in PHP, body-parser in 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