Skip to main content

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:

  1. Verifies the HMAC signature
  2. Decodes the request body
  3. Resolves the event to a supported handler
  4. Returns a typed, validated callback object
warning

Never process callback data without a successful verification. processCallback() throws CallbackVerificationIntegrationException when the signature does not match.

Supported Events​

Event (type:name)Returned objectMeaning
order:amount_paid_updatedOrderAmountPaidCallbackThe order's paid amount changed

Any other event raises UnsupportedCallbackIntegrationException. See Webhook Events for the full event catalogue served by the API.

<?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);
}
Credentials repository required

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.

ExceptionWhenSuggested status
CallbackVerificationIntegrationExceptionHMAC signature does not match401 Unauthorized
CallbackBuildIntegrationExceptionBody cannot be decoded or fails validation400 Bad Request
UnsupportedCallbackIntegrationExceptionEvent type or name is not supported by the SDK200 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​

  1. Verify before processing — always go through processCallback(); never trust unverified data
  2. Acknowledge unknown events — catch UnsupportedCallbackIntegrationException and return 200
  3. Idempotency — process each order id only once; store it to prevent duplicate processing
  4. Correct status codes — 200 success and unsupported, 401 verification failure, 400 invalid payload
  5. 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);
}
}