Webhook Events Reference
Complete reference of all webhook events sent by Paysera Checkout.
Events​
Paysera Checkout sends a small, fixed set of webhook events. Each event carries an
event object with a type and a name. Route on event.type, then act on the
state inside the payload (order.status for order events, payment.status for
payment events) — do not depend on a rich event-name taxonomy.
event.type | event.name | Envelope | Meaning |
|---|---|---|---|
order | amount_paid_updated | Order snapshot | The amount paid on the order changed (a payment was registered). Carries the full order state. |
payment | status_updated | Thin payment | A single payment's status changed. |
refund | status_updated | Thin payment | A refund's status changed (same shape as payment, with event.type = refund). |
Split-payment integrations additionally receive distribution events
(paysera.fund-distributor.distribution.*), which use a different envelope — see
Distribution Events.
There is no order.created / order.status_updated / order.reference_updated
event. Order lifecycle changes (including becoming paid) reach you through the
order snapshot event and the thin payment event. Treat any unknown event.name
as a no-op rather than failing.
- All timestamps are Unix epoch (seconds, UTC)
- All amounts are in minor currency units (e.g., cents)
- All payload fields are snake_case
Order Snapshot Event​
event.type = "order". The body has two top-level keys, event and order, and
delivers the full order snapshot at the moment the event fired — including every
payment link under order.payment_links[] and each link's payments under
order.payment_links[].payments[]. Branch on order.status.
{
"event": {
"name": "amount_paid_updated",
"type": "order"
},
"order": {
"paysera_order_id": "019ed03a-84f0-7ba0-874a-f7473738875b",
"merchant_order_id": "ORDER-12345",
"source": "paysera-marketplace",
"amount": 2500,
"amount_paid": 2500,
"currency": "EUR",
"status": "paid",
"created_at": 1736433270,
"updated_at": 1736433570,
"merchant_data": [],
"payment_links": [
{
"id": "019ed03a-8591-7197-9aad-d9c029286b77",
"name": "Order #12345",
"created_at": 1736433270,
"updated_at": 1736433570,
"payer_name": "Jonas Jonaitis",
"payer_email": "jonas.jonaitis@example.com",
"payments": [
{
"id": "019ed03a-8f12-7503-8369-9c01999bf6cb",
"method": "paysera",
"status": "settled",
"original_amount": null,
"original_currency": null,
"payment_currency": "EUR",
"payment_amount": 2500,
"updated_at": 1736433570,
"payer_name": "Jonas Jonaitis",
"payer_email": "jonas.jonaitis@example.com",
"payment_country": "LT",
"payer_ip_country": "LT",
"payer_country": "LT",
"purpose": "Payment for order #12345"
}
]
}
]
}
}
Actions to take:
- Look the order up by
paysera_order_id(stable) ormerchant_order_id. - Reconcile
amount_paidagainstamount. Only fulfill whenorder.status == "paid"(equivalently,amount_paid >= amount) — a snapshot can arrive for a partial payment. - The same callback can arrive more than once; de-duplicate on the
X-Paysera-Callback-Idheader.
Thin Payment Event​
event.type = "payment" (or "refund"). A compact, payment-centric envelope — not
the full order snapshot. It includes a top-level version, the minimal order
identifiers, the affected payment block, and a timestamp.
{
"version": 1,
"event": {
"type": "payment",
"name": "status_updated"
},
"order": {
"paysera_order_id": "019ed03a-84f0-7ba0-874a-f7473738875b",
"merchant_order_id": "ORDER-12345"
},
"payment": {
"id": "019ed03a-8f12-7503-8369-9c01999bf6cb",
"status": "settled",
"amount": 2500,
"currency": "EUR",
"method": "paysera"
},
"timestamp": 1736433570
}
Actions to take:
- Branch on
payment.status(see Payment Statuses). - Refunds use the identical shape with
event.type = "refund".
Distribution Events​
Only sent to split-payment integrations. These use a flat envelope (no nested
event object); the event kind is the top-level type.
type | Meaning |
|---|---|
paysera.fund-distributor.distribution.recipient.settled | A recipient's share of a split payment settled |
paysera.fund-distributor.distribution.failed | A split distribution failed |
{
"id": "evt_019eba8f-f582-71ef-b404-5a20b51b8e3e",
"type": "paysera.fund-distributor.distribution.recipient.settled",
"created": 1736433570,
"payment_id": "019eba8b-8c78-7d2d-9153-640e6a9e1c8a",
"order_id": "019eba8a-ffa4-7180-a47c-319fa865dcf0",
"status": "settled",
"data": {
"beneficiary_id": "019e2a8a-6dcc-7245-a24d-23e8561f8fda",
"amount": 4000,
"currency": "EUR"
}
}
Webhook Headers​
| Header | Description |
|---|---|
Content-Type | application/json |
X-Paysera-Signature | Hex-encoded HMAC of the raw body |
X-Paysera-Signature-Alg | HMAC-SHA256 |
X-Paysera-Created-At | Unix timestamp (seconds, UTC) of webhook generation |
X-Paysera-Request-Id | Unique request identifier (for tracing) |
X-Paysera-Callback-Id | Unique callback identifier (use for idempotency) |
X-Paysera-Event headerThe event identity is in the JSON body at event.type / event.name (or the top-level
type for distribution events). There is no X-Paysera-Event HTTP header — do not
branch on headers for the event.
Complete Handler Example​
PHP​
<?php
class WebhookHandler
{
private string $secret; // Your OAuth Client Secret (used for webhook signing)
private OrderRepository $orders;
public function handle(): void
{
// Read raw body and headers
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PAYSERA_SIGNATURE'] ?? '';
$callbackId = $_SERVER['HTTP_X_PAYSERA_CALLBACK_ID'] ?? '';
// Verify signature
if (!$this->verifySignature($payload, $signature)) {
http_response_code(401);
return;
}
// Idempotency check — skip if this callback ID was already processed
if ($this->orders->callbackProcessed($callbackId)) {
http_response_code(200);
return;
}
$data = json_decode($payload, true);
// Distribution events use a flat envelope with a top-level "type"
$type = $data['event']['type'] ?? ($data['type'] ?? null);
match (true) {
// Order snapshot — branch on order.status, not the event name
$type === 'order' => $this->handleOrderSnapshot($data['order']),
// Thin payment / refund event
$type === 'payment', $type === 'refund' => $this->handlePayment($data['order'], $data['payment']),
// Split-payment distribution events
str_starts_with((string) $type, 'paysera.fund-distributor.') => $this->handleDistribution($data),
default => error_log("Unknown webhook type: $type"),
};
$this->orders->markCallbackProcessed($callbackId);
http_response_code(200);
echo 'OK';
}
private function verifySignature(string $payload, string $signature): bool
{
$expected = hash_hmac('sha256', $payload, $this->secret);
return hash_equals($expected, $signature);
}
private function handleOrderSnapshot(array $order): void
{
if (($order['status'] ?? null) === 'paid') {
$this->orders->markPaid($order['paysera_order_id']);
// Trigger fulfillment, send confirmation email, etc.
}
}
private function handlePayment(array $order, array $payment): void
{
// React to payment.status for this order
}
private function handleDistribution(array $event): void
{
// Split payments only — reconcile the recipient's share
}
}