Callbacks
Process webhook notifications with built-in signature verification.
Paysera sends a signed server-to-server callback to your callback_url when a relevant event occurs.
This is a notification, not the browser return URL.
Overview​
The Callbacks facade exposes a single entry point, processCallback(), which:
- Verifies the HMAC signature
- Decodes the request body
- Resolves the event to a supported handler
- Returns a typed, validated callback object
Never process callback data without a successful verification. processCallback() throws
CallbackVerificationIntegrationException when the signature does not match.
Supported Events​
Event (type:name) | Returned object | Meaning |
|---|---|---|
order:amount_paid_updated | OrderAmountPaidCallback | The order's paid amount changed |
Any other event raises UnsupportedCallbackIntegrationException. See
Webhook Events for the full event catalogue served by
the API.
Recommended Usage​
<?php
use Paysera\CheckoutSdk\SdkFacade;
use Paysera\CheckoutSdk\Entity\OrderAmountPaidCallback;
use Paysera\CheckoutSdk\Exception\CallbackBuildIntegrationException;
use Paysera\CheckoutSdk\Exception\CallbackVerificationIntegrationException;
use Paysera\CheckoutSdk\Exception\UnsupportedCallbackIntegrationException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @var SdkFacade $sdkFacade
* @var RequestInterface $request
* @var ResponseInterface $response
*/
$callbacksFacade = $sdkFacade->getCallbacksFacade();
try {
// Verifies the signature, decodes the body and returns a validated callback object
$callback = $callbacksFacade->processCallback($request);
if ($callback instanceof OrderAmountPaidCallback) {
$orderInfo = $callback->getOrder()->getOrderInfo();
if ($orderInfo->isPaid()) {
handlePaymentCompleted($orderInfo->getMerchantOrderId(), $callback);
} else {
handleOrderUpdated($orderInfo->getMerchantOrderId(), $callback);
}
}
return $response->withStatus(200);
} catch (UnsupportedCallbackIntegrationException $exception) {
// Unknown event — acknowledge so it is not retried, but do not process it
return $response->withStatus(200);
} catch (CallbackVerificationIntegrationException $exception) {
error_log('Callback verification failed: ' . $exception->getMessage());
return $response->withStatus(401);
} catch (CallbackBuildIntegrationException $exception) {
error_log('Callback build failed: ' . $exception->getMessage());
return $response->withStatus(400);
}
Callback verification uses the stored payment API credentials. Configure a persistent credentials repository as described in Installation.
Exceptions and HTTP Responses​
All three exceptions extend IntegrationException; catch them in the order shown above, most specific
first.
| Exception | When | Suggested status |
|---|---|---|
CallbackVerificationIntegrationException | HMAC signature does not match | 401 Unauthorized |
CallbackBuildIntegrationException | Body cannot be decoded or fails validation | 400 Bad Request |
UnsupportedCallbackIntegrationException | Event type or name is not supported by the SDK | 200 OK (do not process) |
Returning 200 for an unsupported event prevents Paysera from retrying a callback the SDK cannot handle.
Callback Object Structure​
$callback = $callbacksFacade->processCallback($request);
// Event information — available on every callback
$event = $callback->getEvent();
$eventName = $event->getName(); // e.g. 'amount_paid_updated'
$eventType = $event->getType(); // e.g. 'order'
// Order data — available after an instanceof check
$order = $callback->getOrder();
$orderId = $order->getId(); // Paysera order id
$orderInfo = $order->getOrderInfo();
$merchantOrderId = $orderInfo->getMerchantOrderId(); // Your reference
$source = $orderInfo->getSource();
$amount = $orderInfo->getAmount(); // int (minor units)
$amountPaid = $orderInfo->getAmountPaid(); // int (minor units)
$currency = $orderInfo->getCurrency();
$status = $orderInfo->getStatus();
$isPaid = $orderInfo->isPaid(); // bool — status === 'paid'
// Custom reference data — MerchantData implements ArrayAccess
$merchantData = $order->getMerchantData();
$all = $merchantData->getData(); // array<string, mixed>
$reference = $merchantData['reference'] ?? null;
// Payment links and their payments
foreach ($order->getPaymentLinkCollection() as $paymentLink) {
$linkId = $paymentLink->getId();
$linkName = $paymentLink->getName();
$payerInfo = $paymentLink->getPayerInfo();
foreach ($paymentLink->getPayments() as $payment) {
$paymentInfo = $payment->getPaymentInfo();
$method = $paymentInfo->getMethod();
$paymentStatus = $paymentInfo->getStatus();
$purpose = $paymentInfo->getPurpose();
}
}
Determining Payment Completion​
The SDK reports what the callback is and the order data; deciding whether the order is fully paid is up
to your integration. isPaid() checks the order status, and the amounts let you detect partial payments:
$orderInfo = $callback->getOrder()->getOrderInfo();
$isFullyPaid = $orderInfo->getAmountPaid() >= $orderInfo->getAmount();
$isPartiallyPaid = $orderInfo->getAmountPaid() > 0 && !$isFullyPaid;
Best Practices​
- Verify before processing — always go through
processCallback(); never trust unverified data - Acknowledge unknown events — catch
UnsupportedCallbackIntegrationExceptionand return200 - Idempotency — process each order id only once; store it to prevent duplicate processing
- Correct status codes —
200success and unsupported,401verification failure,400invalid payload - Respond quickly — defer heavy work such as emails to background processing
Complete Handler Example​
Framework-agnostic handler built on PSR-7 and PSR-17:
<?php
use DateTimeImmutable;
use Paysera\CheckoutSdk\SdkFacade;
use Paysera\CheckoutSdk\Entity\OrderAmountPaidCallback;
use Paysera\CheckoutSdk\Exception\CallbackBuildIntegrationException;
use Paysera\CheckoutSdk\Exception\CallbackVerificationIntegrationException;
use Paysera\CheckoutSdk\Exception\UnsupportedCallbackIntegrationException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
class PayseraWebhookHandler
{
private SdkFacade $sdkFacade;
private OrderRepository $orders;
private ResponseFactoryInterface $responseFactory;
public function __construct(
SdkFacade $sdkFacade,
OrderRepository $orders,
ResponseFactoryInterface $responseFactory
) {
$this->sdkFacade = $sdkFacade;
$this->orders = $orders;
$this->responseFactory = $responseFactory;
}
public function handle(RequestInterface $request): ResponseInterface
{
try {
$callback = $this->sdkFacade->getCallbacksFacade()->processCallback($request);
} catch (UnsupportedCallbackIntegrationException $exception) {
return $this->responseFactory->createResponse(200);
} catch (CallbackVerificationIntegrationException $exception) {
error_log('Callback verification failed: ' . $exception->getMessage());
return $this->responseFactory->createResponse(401);
} catch (CallbackBuildIntegrationException $exception) {
error_log('Callback build failed: ' . $exception->getMessage());
return $this->responseFactory->createResponse(400);
}
if (!$callback instanceof OrderAmountPaidCallback) {
return $this->responseFactory->createResponse(200);
}
$orderInfo = $callback->getOrder()->getOrderInfo();
$order = $this->orders->findByReference($orderInfo->getMerchantOrderId());
if ($order === null) {
return $this->responseFactory->createResponse(200);
}
// Idempotency check
if ($order->paysera_status === $orderInfo->getStatus()) {
return $this->responseFactory->createResponse(200);
}
$order->paysera_status = $orderInfo->getStatus();
if ($orderInfo->isPaid()) {
$order->status = 'paid';
$order->paid_at = new DateTimeImmutable();
}
$order->save();
return $this->responseFactory->createResponse(200);
}
}