Skip to main content

Webhooks

Receive real-time payment notifications with secure webhook callbacks.

Webhooks (callbacks) notify your server about payment events in real-time. This guide covers setting up and handling webhooks.

Overview​

When a payment status changes, Paysera sends a POST request to your callback URL with event details. Your server must:

  1. Verify the signature
  2. Process the event
  3. Return appropriate HTTP status code
Timestamps and Amounts
  • All timestamps are Unix epoch (seconds since 1970-01-01)
  • All amounts are in minor currency units (e.g., cents)

Webhook Delivery​

Endpoint​

Webhooks are sent to the callback_url specified when creating the payment order.

Retry Policy​

If your endpoint doesn't return a 2xx status code, Paysera retries with exponential backoff:

AttemptDelay
1st retry5 seconds
2nd retry60 seconds
3rd retry3600 seconds (1 hour)

After all retries fail, the webhook is marked as failed.

Timeout​

Your endpoint should respond within 30 seconds. For longer processing, return 200 immediately and process asynchronously.

Webhook Payload​

Headers​

HeaderDescription
Content-Typeapplication/json
X-Paysera-SignatureHMAC signature for verification
X-Paysera-EventEvent type (e.g., order.paid)
X-Paysera-TimestampUnix timestamp of the event

Body Structure​

{
"event": {
"name": "order.paid",
"type": "order",
"timestamp": 1736433570
},
"order": {
"id": "a6f2b8e3-5e5f-47d9-b13f-87ed2db2938a",
"projectId": "01990618-2c90-7103-9169-752bdaac7a52",
"status": "paid",
"amount": 2500,
"currency": "EUR",
"reference": "ORDER-12345",
"amountPaid": 2500,
"balanceDue": 0,
"createdAt": 1736433270,
"updatedAt": 1736433570
},
"paymentLink": {
"id": "c8d9e0f1-2a3b-4c5d-6e7f-8a9b0c1d2e3f",
"status": "completed"
}
}

Event Types​

EventOrder StatusDescription
order.paidpaidOrder has been fully paid
order.pending_paymentpending_paymentOrder awaiting payment
payment_link.completed-Payment link completed
payment_link.expired-Payment link expired
payment_link.canceled-Payment link canceled

Signature Verification​

Always verify webhook signatures before processing to prevent fraud.

Verification Process​

  1. Get the signature from X-Paysera-Signature header
  2. Get the raw request body
  3. Compute HMAC-SHA256 of the body using your client secret
  4. Compare signatures (constant-time comparison)
<?php

function verifyWebhookSignature(string $payload, string $signature, string $secret): bool
{
$expectedSignature = hash_hmac('sha256', $payload, $secret);
return hash_equals($expectedSignature, $signature);
}

// In your webhook handler
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PAYSERA_SIGNATURE'] ?? '';
$secret = getenv('PAYSERA_CLIENT_SECRET');

if (!verifyWebhookSignature($payload, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}

// Process the webhook
$data = json_decode($payload, true);
// ...

Complete Webhook Handler​

PHP Example​

<?php

class PayseraWebhookHandler
{
private string $secret;

public function __construct(string $secret)
{
$this->secret = $secret;
}

public function handle(): void
{
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PAYSERA_SIGNATURE'] ?? '';

// Verify signature
if (!$this->verifySignature($payload, $signature)) {
http_response_code(401);
echo 'Invalid signature';
return;
}

// Parse payload
$data = json_decode($payload, true);
if (!$data) {
http_response_code(400);
echo 'Invalid payload';
return;
}

// Process event
try {
$this->processEvent($data);
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
error_log('Webhook processing failed: ' . $e->getMessage());
http_response_code(500);
echo 'Processing failed';
}
}

private function verifySignature(string $payload, string $signature): bool
{
$expected = hash_hmac('sha256', $payload, $this->secret);
return hash_equals($expected, $signature);
}

private function processEvent(array $data): void
{
$eventName = $data['event']['name'] ?? null;
$order = $data['order'] ?? null;

if (!$eventName || !$order) {
throw new Exception('Missing event or order data');
}

$reference = $order['reference'] ?? null;
$status = $order['status'] ?? null;

switch ($eventName) {
case 'order.paid':
$this->handleOrderPaid($reference, $order);
break;

case 'order.pending_payment':
$this->handleOrderPendingPayment($reference, $order);
break;

case 'payment_link.completed':
$this->handlePaymentLinkCompleted($reference, $data);
break;

case 'payment_link.expired':
$this->handlePaymentLinkExpired($reference, $data);
break;

default:
error_log("Unhandled event: $eventName");
}
}

private function handleOrderPaid(string $reference, array $order): void
{
// Update order status in your database
// Send confirmation email
// Fulfill the order
error_log("Order paid: $reference");
}

private function handleOrderPendingPayment(string $reference, array $order): void
{
// Order is still awaiting payment
error_log("Order pending payment: $reference");
}

private function handlePaymentLinkCompleted(string $reference, array $data): void
{
// Payment link was used successfully
error_log("Payment link completed for order: $reference");
}

private function handlePaymentLinkExpired(string $reference, array $data): void
{
// Payment link expired - may need to create a new one
error_log("Payment link expired for order: $reference");
}
}

// Usage
$handler = new PayseraWebhookHandler(getenv('PAYSERA_CLIENT_SECRET'));
$handler->handle();

Idempotency​

Webhooks may be delivered multiple times. Implement idempotent processing:

<?php

class IdempotentWebhookHandler
{
private PDO $db;

public function processEvent(array $data): void
{
$orderId = $data['order']['id'];
$eventName = $data['event']['name'];
$timestamp = $data['event']['timestamp'];

// Check if already processed
$eventKey = md5($orderId . $eventName . $timestamp);

$stmt = $this->db->prepare(
'SELECT id FROM processed_webhooks WHERE event_key = ?'
);
$stmt->execute([$eventKey]);

if ($stmt->fetch()) {
// Already processed, skip
return;
}

// Process the event
$this->doProcess($data);

// Mark as processed
$stmt = $this->db->prepare(
'INSERT INTO processed_webhooks (event_key, processed_at) VALUES (?, NOW())'
);
$stmt->execute([$eventKey]);
}
}

Testing Webhooks​

Local Development​

Use tunneling services to expose your local server:

# Using ngrok
ngrok http 8000

# Use the generated URL as callback_url
# https://abc123.ngrok.io/webhooks/paysera

Webhook Testing Tool​

Use webhook.site to inspect webhook payloads:

  1. Get a unique URL from webhook.site
  2. Use it as your callback_url
  3. Trigger a payment
  4. Inspect the received payload

Response Codes​

CodeMeaningPaysera Action
200-299SuccessNo retry
401UnauthorizedNo retry (logs error)
4xxClient errorNo retry
5xxServer errorRetry with backoff
TimeoutNo response in 30sRetry with backoff