🇬🇧 English version:
Merchant_Webhook_API.en.md
Kimenő webhook a Paynance platformról: a webshopod fizetési eseményeit elküldjük egy általad megadott URL-re, aláírva, újrapróbálkozással, kézbesítési naplóval.
Gépi olvasható séma: Merchant_Webhook_API.hu.openapi.json
(OpenAPI 3.0.3) — kliens- és típusgeneráláshoz, Swagger UI / Redoc megjelenítéshez.
Élő próbához aláírás-számítással: Merchant_Webhook_API.hu.postman_collection.json.
1. Áttekintés
A vásárló böngészőjéből való visszatérés nem megbízható jelzés: a vásárló bezárhatja a fület, a hálózat elszakadhat, a fizetés pedig órákkal később is állapotot változtathat (capture, refund, chargeback). A webhook a szerver-szerver csatorna, amiből biztosan megtudod, mi történt.
Mikor küldünk. Amikor egy webshopos (VPOS) fizetés állapota változik: authorizáció, authorizáció-hiba, capture, cancel, refund, chargeback. POS/terminál tranzakciókról nem küldünk webhookot.
Mit várunk tőled.
| Válaszkód | 2xx, ha átvetted az eseményt — javasolt: 202 Accepted |
| Válaszidő | gyorsan (5 s a timeoutunk) — a feldolgozást tedd háttérbe |
| Idempotencia | kötelező, event_id alapján (lásd 5. pont) |
| Aláírás-hiba | 4xx, soha ne 2xx (lásd 4. pont) — javasolt: 401 Unauthorized |
A konkrét kódot nem kötjük meg, csak az osztályát. Bármely 2xx átvételnek számít, bármely
4xx és 5xx újrapróbálkozást vált ki. A javaslat a két konkrét kódra attól van, hogy pontosan
azt mondják, ami történik — és mert a saját logjaidban is ezekből fogod látni, mi ment félre:
202 Accepted— átvetted az eseményt, a feldolgozás háttérben zajlik. Ez fedi a valóságot: 5 másodperc alatt úgysem fejezed be, a200 OKpedig azt sugallná, hogy kész vagy. (A200természetesen ugyanúgy jó nekünk.)401 Unauthorized— az aláírás nem stimmel. Egyértelműen elkülöníti az „ismeretlen küldő” esetet a hibás payloadtól (400) vagy a te feldolgozási hibádtól (5xx), így a rotáció utáni átmeneti eltérés azonnal felismerhető a logban.
A kérés minden esetben:
POST <a te URL-ed>
Content-Type: application/json
Paynance-Signature: <hex HMAC-SHA256>
2. Beállítás
A Paynance felületen: Webshop beállítások → Webhook (fizetési értesítések).
- Webhook URL — csak
https, publikusan elérhető címen. Privát IP-re vagyhttp-re mutató URL-t nem fogadunk el. - Aláírási titok — a beállítás mentésekor automatikusan generáljuk. Maszkolva látszik
(
whsec_550d2a2b…); a Titok felfedése gombbal bármikor megnézheted a teljes értéket. Ha elveszik a szerveredről, nem kell rotálni: itt újra kiolvasható. - Teszt-esemény küldése — azonnal, szinkron küld egy kérést a végpontodra, és a válaszkódot a helyszínen visszamutatja.
- Kézbesítési napló — ugyanezen az oldalon: mely események mikor és milyen válaszkóddal mentek ki, plusz kézi újraküldés.
Két dolgot érdemes tudni a teszt-eseményről:
event_typeértéketest, nem szintetikus fizetési esemény — tehát a kódod ne fizetési eseményt várjon tőle. Azevent_idviszont valódi UUID v7, hogy az idempotencia-tárolód pontosan úgy viselkedjen, ahogy itt le van írva.- A naplóba nem kerül be. Az eredmény ott és akkor látszik a képernyőn; visszamenőleg nem kereshető, és a végpont hibaállapotát sem befolyásolja.
3. Payload
Minden payload ugyanezt a burkot használja:
| Mező | Típus | Leírás |
|---|---|---|
version |
int | Payload séma verzió. Új mező bekerülhet emelés nélkül, átnevezés nem. |
event_id |
string (UUID v7) | Az esemény publikus azonosítója. Erre épül az idempotencia. |
event_type |
string | A mi verziózott eseménynevünk, lásd a táblát lentebb. |
created_at |
string (ISO 8601) | Mikor keletkezett az esemény nálunk. |
data |
object | Az esemény tartalma. |
A data objektum mezői:
| Mező | Típus | Leírás |
|---|---|---|
psp_reference |
string | Az adott művelet feldolgozói referenciája. Egy fizetés capture-je és refundja külön psp_reference-t kap. |
original_psp_reference |
string | null | A művelet mögötti fizetés referenciája. payment.authorised/payment.failed esetén null. |
merchant_reference |
string | A te rendelés-azonosítód, amit a session létrehozásakor küldtél. |
transaction_type |
string | payment, capture, cancellation, refund, chargeback vagy test. |
success |
bool | Az adott művelet sikeres volt-e. |
amount |
object | null | { "value": <minor unit>, "currency": "HUF" } — a value a legkisebb pénzegységben. |
payment_method |
string | null | Pl. visa, mc, applepay. |
reason |
string | null | Hiba vagy elutasítás oka, ha van. |
event_date |
string | null | A művelet időpontja a fizetésfeldolgozásban. |
shopper_email |
string | null | A vásárló e-mail címe, ha megadta. |
store |
string | A Paynance store azonosító. |
Eseménytípusok
event_type |
Mikor |
|---|---|
payment.authorised |
A fizetés sikeresen authorizálódott. |
payment.failed |
Az authorizáció elhasalt (elutasítás, hibás kártya, 3DS-hiba). |
payment.captured |
Az összeg terhelése megtörtént. Részleges capture-nél több esemény jön. |
payment.cancelled |
Az authorizáció visszavonva, terhelés nélkül. |
payment.refunded |
Visszatérítés. Részleges refundnál több esemény jön. |
payment.chargeback |
A vásárló bankja visszahívta az összeget. |
test |
A beállítófelület teszt-gombja. |
Példák
payment.authorised
{
"version": 1,
"event_id": "0192f1a2-3b4c-7d5e-8f60-1a2b3c4d5e6f",
"event_type": "payment.authorised",
"created_at": "2026-09-09T12:04:12+00:00",
"data": {
"psp_reference": "NC6HT9CRT65ZGN82",
"original_psp_reference": null,
"merchant_reference": "ORDER-10231",
"transaction_type": "payment",
"success": true,
"amount": {
"value": 249900,
"currency": "HUF"
},
"payment_method": "visa",
"reason": null,
"event_date": "2026-09-09T12:04:11+02:00",
"shopper_email": "vevo@example.com",
"store": "ST322LJ223223K5FQ4NM3JWLK"
}
}
payment.failed
{
"version": 1,
"event_id": "0192f1a3-77bd-7c11-9a04-8f3d5b21c9aa",
"event_type": "payment.failed",
"created_at": "2026-09-09T12:07:45+00:00",
"data": {
"psp_reference": "LK2P9WQ4RT88ZZ01",
"original_psp_reference": null,
"merchant_reference": "ORDER-10232",
"transaction_type": "payment",
"success": false,
"amount": {
"value": 249900,
"currency": "HUF"
},
"payment_method": "mc",
"reason": "Refused",
"event_date": "2026-09-09T12:07:44+02:00",
"shopper_email": "vevo@example.com",
"store": "ST322LJ223223K5FQ4NM3JWLK"
}
}
payment.captured — figyeld meg, hogy a psp_reference a capture referenciája, a
fizetés a original_psp_reference-ben van. Egy részleges capture-sorozat minden tagja külön
eseményt kap.
{
"version": 1,
"event_id": "0192f1b0-04ce-7a52-b7d1-6e0c9f4471d3",
"event_type": "payment.captured",
"created_at": "2026-09-09T14:31:02+00:00",
"data": {
"psp_reference": "QT4M8ZC2XX10PP77",
"original_psp_reference": "NC6HT9CRT65ZGN82",
"merchant_reference": "ORDER-10231",
"transaction_type": "capture",
"success": true,
"amount": {
"value": 249900,
"currency": "HUF"
},
"payment_method": "visa",
"reason": null,
"event_date": "2026-09-09T14:31:01+02:00",
"shopper_email": "vevo@example.com",
"store": "ST322LJ223223K5FQ4NM3JWLK"
}
}
payment.cancelled
{
"version": 1,
"event_id": "0192f1b4-9a20-7f08-8c33-2d5a7e91b402",
"event_type": "payment.cancelled",
"created_at": "2026-09-09T15:02:20+00:00",
"data": {
"psp_reference": "WW90KL22ZZ41MM08",
"original_psp_reference": "NC6HT9CRT65ZGN82",
"merchant_reference": "ORDER-10231",
"transaction_type": "cancellation",
"success": true,
"amount": {
"value": 249900,
"currency": "HUF"
},
"payment_method": "visa",
"reason": null,
"event_date": "2026-09-09T15:02:19+02:00",
"shopper_email": "vevo@example.com",
"store": "ST322LJ223223K5FQ4NM3JWLK"
}
}
payment.refunded
{
"version": 1,
"event_id": "0192f2c1-51ab-7d90-9e77-4b8c0a2f6611",
"event_type": "payment.refunded",
"created_at": "2026-09-10T09:18:40+00:00",
"data": {
"psp_reference": "RF77TT01YY93QQ12",
"original_psp_reference": "NC6HT9CRT65ZGN82",
"merchant_reference": "ORDER-10231",
"transaction_type": "refund",
"success": true,
"amount": {
"value": 100000,
"currency": "HUF"
},
"payment_method": "visa",
"reason": null,
"event_date": "2026-09-10T09:18:39+02:00",
"shopper_email": "vevo@example.com",
"store": "ST322LJ223223K5FQ4NM3JWLK"
}
}
payment.chargeback
{
"version": 1,
"event_id": "0192f9d4-c8e1-7b33-a5f2-90ab13cd4477",
"event_type": "payment.chargeback",
"created_at": "2026-09-21T06:45:03+00:00",
"data": {
"psp_reference": "CB55QQ88WW22EE31",
"original_psp_reference": "NC6HT9CRT65ZGN82",
"merchant_reference": "ORDER-10231",
"transaction_type": "chargeback",
"success": true,
"amount": {
"value": 249900,
"currency": "HUF"
},
"payment_method": "visa",
"reason": "Fraud",
"event_date": "2026-09-21T06:45:02+02:00",
"shopper_email": "vevo@example.com",
"store": "ST322LJ223223K5FQ4NM3JWLK"
}
}
test — a beállítófelület teszt-gombja. Nincs mögötte fizetés, ezért a data szűkebb.
{
"version": 1,
"event_id": "0192f1a0-1111-7222-8333-444455556666",
"event_type": "test",
"created_at": "2026-09-09T11:59:00+00:00",
"data": {
"transaction_type": "test",
"store": "ST322LJ223223K5FQ4NM3JWLK",
"message": "This is a test event from Paynance. No payment is associated with it."
}
}
4. Aláírás-ellenőrzés
Minden kérésen ott van a Paynance-Signature header: a nyers request body
HMAC-SHA256-a, hex kódolva, az aláírási titkoddal. Timestamp nincs benne — a replay ellen az
idempotencia véd (5. pont).
A header formátuma listás-toleráns: vesszővel elválasztva több érték is állhat benne. Ma egy értéket küldünk, de a példakód mindkettőt kezelje — így egy későbbi dupla aláírás nem lesz breaking change nálad.
A nyers bodyt ellenőrizd, ne az újra-szerializált JSON-t. Ez a leggyakoribb integrációs hiba: a framework előbb parse-olja a JSON-t, a
json_encode()pedig más szóközökkel és más escape-eléssel adja vissza, mint ami a huzalon jött — az aláírás pedig arra a byte-sorozatra érvényes, amit küldtünk.
PHP
<?php
$secret = getenv('PAYNANCE_WEBHOOK_SECRET');
// A NYERS body, parse előtt.
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_PAYNANCE_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $body, $secret);
$valid = false;
foreach (explode(',', $header) as $candidate) {
// hash_equals() és nem ===: az utóbbi időzítéses támadásra sebezhető.
if (hash_equals($expected, trim($candidate))) {
$valid = true;
break;
}
}
if (! $valid) {
// 4xx, SOHA nem 2xx — lásd lentebb. Javasolt kód: 401.
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// ... idempotencia + sorbaállítás, majd:
// 202: átvettük, a feldolgozás háttérben megy. Bármely 2xx megfelel.
http_response_code(202);
Laravelben a nyers body $request->getContent(), a header
$request->header('Paynance-Signature'). A route-ot vedd ki a CSRF-védelem alól.
Node (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const secret = process.env.PAYNANCE_WEBHOOK_SECRET;
// A nyers bodyra van szükség, ezért express.raw() és nem express.json().
app.post('/webhooks/paynance', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const header = req.get('Paynance-Signature') || '';
const valid = header.split(',').some((candidate) => {
const given = Buffer.from(candidate.trim(), 'utf8');
const want = Buffer.from(expected, 'utf8');
// timingSafeEqual azonos hosszú buffert vár.
return given.length === want.length && crypto.timingSafeEqual(given, want);
});
if (!valid) {
return res.sendStatus(401); // 4xx, nem 2xx — javasolt kód: 401
}
const event = JSON.parse(req.body.toString('utf8'));
// ... idempotencia + sorbaállítás
res.sendStatus(202); // átvettük, a feldolgozás háttérben megy
});
Aláírás-hibánál 4xx-et adj vissza (javasolt: 401), ne 2xx-et
Ez nem stílusajánlás, hanem a szerződés része. A 4xx-re újrapróbálunk, tehát egy átmeneti titok-eltérés — tipikusan rotáció után, amíg a szervereden nem frissítettél — magától helyreáll. Ha aláírás-hibára logolsz és 2xx-et adsz, a kézbesítés véglegesen elveszik: nálunk sikeresnek fog látszani, és soha nem próbáljuk újra.
Bármely 4xx jó nekünk, de 401 Unauthorized-ot javaslunk: a kézbesítési naplóban a
válaszkód az egyetlen jelzés arról, miért nem ment át az esemény — a 401 azonnal
megkülönbözteti az aláírás-hibát a hibás payloadtól (400) és a te feldolgozási hibádtól
(5xx).
5. Idempotencia
Követelmény, nem javaslat. Ugyanaz az esemény többször is megérkezhet — ez a rendszer normál működése, nem hiba:
- egy hálózati félrecsúszás (elküldtük, a válasz elveszett) újraküldést okoz;
- a 30 napig tartó újrapróbálkozás mellett ez statisztikailag biztosan elő fog fordulni;
- aláírás szinten nincs replay-védelem (nincs timestamp), az idempotencia veszi át a szerepét.
A megoldás: tárold el az event_id-t, és ha már láttad, hagyd ki a feldolgozást — de
továbbra is adj vissza 2xx-et.
CREATE TABLE paynance_webhook_events (
event_id CHAR(36) PRIMARY KEY,
event_type VARCHAR(64) NOT NULL,
created_at TIMESTAMP NOT NULL
);
try {
$db->prepare('INSERT INTO paynance_webhook_events (event_id, event_type, created_at) VALUES (?, ?, NOW())')
->execute([$event['event_id'], $event['event_type']]);
} catch (PDOException $e) {
// Duplikátum: már feldolgoztuk. 2xx, és kész — ugyanaz a 202, mint a normál ágon.
http_response_code(202);
exit;
}
A event_id UUID v7, azaz időrendezett: a hex reprezentáció lexikografikus sorrendje
megegyezik a keletkezési sorrenddel, tehát primary key-ként is jól viselkedik, és a régi
rekordokat összefüggő tartományként lehet takarítani.
6. Retry-politika
A menetrend. Ha nem 2xx-et adsz vissza (vagy nem érünk el 5 másodpercen belül), a következő időpontokban próbálkozunk újra:
9 s → 18 s → 27 s → 2 m → 5 m → 10 m → 15 m → 30 m → 1 h → 2 h → 4 h → majd 8 óránként
Addig próbálkozunk, amíg az esemény a kézbesítési naplóban van: 30 napig. Nincs külön, rövidebb újrapróbálkozási ablak — egy ki nem kézbesített eseményt a fenti menetrend szerint addig küldünk újra, amíg a napló meg nem szabadul tőle a 30 napos megőrzési idő végén.
Egy esemény akkor és csak akkor nem érkezik meg, ha 30 napon át egyszer sem tudtuk átadni.
A letiltás nem semmisít meg eseményt. Ha a végpontod egy hét folyamatos hiba után letiltásra kerül (lásd lejjebb), az csak annyit jelent, hogy nem kopogtatunk tovább — a közben keletkező események rendben felgyűlnek, és a visszakapcsolás után hiánytalanul kimennek.
Amire viszont neked kell felkészülnöd: egy esemény napokkal a keletkezése után is
megérkezhet. A feldolgozásod ezért ne tételezze fel, hogy az esemény friss — a created_at
mezőből mindig látod a tényleges kort, és egy rég lezárt rendelést ne nyisson újra automatikusan.
A sorrendre ne épüljön logika. A gyakorlatban a sorrend megmarad, garancia nincs rá:
párhuzamosan beérkező eseményeknél az egyik előbb válhat láthatóvá, mint a másik. Az
állapotot a psp_reference / original_psp_reference és a transaction_type alapján rakd
össze, ne a beérkezési sorrendből.
Hibázó és letiltott végpont
| Mikor | Mi történik |
|---|---|
| 1 óra folyamatos hiba után | E-mailt küldünk a partneri kapcsolattartói címre, majd naponta legfeljebb egyet, amíg nincs siker. |
| 7 nap folyamatos hiba után | Letiltjuk a végpontot. Nem küldünk rá további eseményt, amíg vissza nem kapcsolod. |
A letiltás időalapú, nem hibaszám-alapú: ugyanaz a kiesés ugyanannyi türelmi időt jelent alacsony és nagy forgalom mellett is.
Letiltás alatt az események keletkeznek, csak nem mennek ki. Amikor a Paynance felületen az Aktiválás gombbal visszakapcsolsz, a felgyűlt események mind kimennek, keletkezési sorrendben — semmit nem dobunk el. Nagyobb forgalomnál ez egyszerre sok hívást jelent, ezért érdemes előbb teszt eseményt küldeni, és csak működő végponttal visszakapcsolni.
Bármelyik sikeres kézbesítés tiszta lappal indítja a végpontot: a hibaszámláló és a figyelmeztetés-időzítő is nullázódik.
7. Titok-rotáció
A Titok rotálása gombbal bármikor cserélhető. Átfedés nincs: a rotáció pillanatától minden esemény — a még ki nem kézbesítettek és az újrapróbálkozások is — az új titokkal jön ki. Nincs előző titok, nincs kulcsválasztás.
Amit tenned kell:
- Rotálj a felületen, és másold ki az új titkot.
- Frissítsd a szervereden, amint tudod.
- A közben kiküldött események aláírás-hibát adnak. Adj rájuk 4xx-et — akkor automatikusan újrapróbáljuk, és a frissítés után átmennek. 2xx-szel ezek véglegesen elvesznek.
A rés, amit ez nyit, a rotáció és a te deployod közti idő. A 30 napos újrapróbálkozás ezt bőven elfedi — a veszteség késés, nem adat.
8. Hibakeresés
A kézbesítési napló a Webshop beállítások oldalon van, a webhook szekció alatt. Minden kézbesítésről látszik:
| Oszlop | Mit mond |
|---|---|
| Keletkezett | Mikor jött létre az esemény nálunk. |
| Esemény | event_type. |
| pspReference | Az adott művelet feldolgozói referenciája — ezzel kereshető vissza a tranzakció. |
| Állapot | Várakozik / Kézbesítve / Sikertelen. |
| Kísérletek | Hány valódi HTTP-hívás történt. |
| Válasz | A végpontod HTTP válaszkódja. |
| Hiba | Kapcsolódási hiba, timeout, vagy a végpontod hibaüzenete. |
Kézi újraküldés. A napló sorain lévő Újraküldés gombbal egy konkrét esemény újra kiküldhető. A kísérletszámláló nullázódik, és a sor a lista végére kerül. Csak aktív végponton működik — ha a végpont le van tiltva, előbb az Aktiválás gomb kell.
Ha letiltottuk a végpontod. A webhook szekcióban egy figyelmeztető sáv jelenik meg: mióta, miért (automatikus vagy kézi), mi volt az utolsó hiba, és hány esemény vár kézbesítésre. A javasolt sorrend:
- javítsd a végpontot (elérhetőség, 2xx, 5 s alatti válasz);
- küldj teszt-eseményt — szinkron, egy kísérlet, azonnal megmondja, jó-e már;
- csak ezután Aktiválás — így nem vakon kapcsolsz vissza, és tudod, hogy a felgyűlt sor valóban ki fog menni.
Gyakori hibák
| Tünet | Ok |
|---|---|
| Az aláírás soha nem egyezik | Az újra-szerializált JSON-t ellenőrzöd, nem a nyers bodyt (4. pont). |
| Egyes események duplán dolgozódnak fel | Nincs event_id szerinti idempotencia (5. pont). |
| Az események „csendben elvesznek” | Aláírás-hibára 2xx-et adsz vissza (4. pont). |
| Az URL nem mentődik | Csak https és publikusan elérhető cím fogadható el. |
| Sok timeout | A feldolgozást a válasz előtt végzed. Vedd át az eseményt, válaszolj 2xx-szel, dolgozz háttérben. |