Skip to main content

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.typeevent.nameEnvelopeMeaning
orderamount_paid_updatedOrder snapshotThe amount paid on the order changed (a payment was registered). Carries the full order state.
paymentstatus_updatedThin paymentA single payment's status changed.
refundstatus_updatedThin paymentA 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.

Branch on status, not event name

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.

Timestamps and Amounts
  • 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) or merchant_order_id.
  • Reconcile amount_paid against amount. Only fulfill when order.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-Id header.

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.

typeMeaning
paysera.fund-distributor.distribution.recipient.settledA recipient's share of a split payment settled
paysera.fund-distributor.distribution.failedA 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​

HeaderDescription
Content-Typeapplication/json
X-Paysera-SignatureHex-encoded HMAC of the raw body
X-Paysera-Signature-AlgHMAC-SHA256
X-Paysera-Created-AtUnix timestamp (seconds, UTC) of webhook generation
X-Paysera-Request-IdUnique request identifier (for tracing)
X-Paysera-Callback-IdUnique callback identifier (use for idempotency)
No X-Paysera-Event header

The 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
}
}