Webhookok és események
Két csatorna érkezik ugyanarra a webhook URL-re: a telepítő fiók eseményei (foglalás, vendég, rendelés) és az app saját életciklus-eseményei. Mindkettő aláírt, és mindkettőt másik titok ellenőrzi.
A telepítéskor a Dynex a te webhook_url-edre feliratkozást hoz létre az app alapértelmezett eseményeire; a feliratkozás titkát (whsec_…) a token-válasz webhook_secret mezője adja vissza (installonként tárold), illetve a webhooks.create végpont. Minden kézbesítés egy JSON POST:
1POST <webhook_url>2Content-Type: application/json3X-Dynex-Event: reservation.created4X-Dynex-Delivery-Id: <uuid>5X-Dynex-Timestamp: <unix mp>6X-Dynex-Signature: sha256=<hex(HMAC-SHA256(secret, "<timestamp>.<nyers törzs>"))>7 8{ "event": "reservation.created", "deliveryId": "…", "timestamp": "2026-09-15T10:00:00.000Z",9 "tenantId": "…", "data": { "reservation": { … } } }- A NYERS törzsből számolj - a JSON újraszerializálása más hash-t ad.
- Utasítsd el az 5 percnél régebbi időbélyeget (visszajátszás), és hasonlíts időzítés-biztosan.
- Válaszolj 2xx-szel 10 mp-en belül; hibánál a Dynex 5, 30, 120 és 720 perc múlva újrapróbálja, 20 egymást követő hiba után a feliratkozás kikapcsol.
- Ugyanaz a
deliveryIdritkán kétszer is érkezhet - a feldolgozás legyen idempotens.
1import { parseWebhookEvent, WebhookSignatureError } from "@dynex/app-sdk";2 3app.post("/webhooks", express.raw({ type: "*/*" }), (req, res) => {4 try {5 const event = parseWebhookEvent(req.body, req.headers, install.webhookSecret);6 // event.event, event.tenantId, event.data7 res.status(200).end();8 } catch (e) {9 if (e instanceof WebhookSignatureError) return res.status(401).end();10 throw e;11 }12});Ez a bérlői csatorna: a parseWebhookEvent() az app. kezdetű eseményeket akkor is elutasítja, ha az aláírás egyezik. Azok külön csatornán, másik titokkal jönnek - lásd az Életciklus-események szakaszt.
További feliratkozást az API-n hozhatsz létre; a minta a szerződésből készül:
1curl -X POST 'https://prod.dynex.hu/api/v3/webhooks' \2 -H 'Authorization: Bearer dxk_A_TELEPITES_KULCSA' \3 -H 'Accept: application/json' \4 -H 'Content-Type: application/json' \5 --data-raw '{6 "url": "https://example.com",7 "events": [8 "reservation.created",9 "reservation.cancelled"10 ],11 "description": "Rendelés-szinkron a raktárrendszerbe",12 "headers": {13 "X-Partner-Id": "abc123"14 },15 "filters": {16 "reservationSources": [17 "api",18 "web"19 ],20 "pipelineIds": [21 "pl_1"22 ],23 "reviewPlatforms": [24 "google"25 ],26 "reviewMinRating": 427 }28}'1// Node.js 18+ (beépített fetch, nem kell külön csomag).2async function main() {3 const payload = {4 "url": "https://example.com",5 "events": [6 "reservation.created",7 "reservation.cancelled"8 ],9 "description": "Rendelés-szinkron a raktárrendszerbe",10 "headers": {11 "X-Partner-Id": "abc123"12 },13 "filters": {14 "reservationSources": [15 "api",16 "web"17 ],18 "pipelineIds": [19 "pl_1"20 ],21 "reviewPlatforms": [22 "google"23 ],24 "reviewMinRating": 425 }26 };27 28 const res = await fetch("https://prod.dynex.hu/api/v3/webhooks", {29 method: "POST",30 headers: {31 "Authorization": "Bearer dxk_A_TELEPITES_KULCSA",32 "Accept": "application/json",33 "Content-Type": "application/json"34 },35 body: JSON.stringify(payload)36 });37 38 const data = await res.json();39 console.log(res.status, data);40}41 42main().catch(console.error);Ha nem az SDK-t használod, az ellenőrzés négy lépés, és mindkét csatornán ugyanaz - csak a titok más:
- Olvasd ki az
X-Dynex-Timestampés azX-Dynex-Signaturefejlécet. Ha bármelyik hiányzik, utasítsd el a kérést. - Utasítsd el, ha az időbélyeg 5 percnél jobban eltér a szervered órájától.
- Számold ki a HMAC-SHA256 értéket a titokkal az
<időbélyeg>.<nyers törzs>szövegre, hexában,sha256=előtaggal. - Hasonlítsd össze a fejléc értékével időzítés-biztosan, és csak egyezés után értelmezd a törzset JSON-ként.
1import { createHmac, timingSafeEqual } from "node:crypto";2 3// rawBody: a kérés NYERS törzse (Buffer vagy string), még a JSON.parse előtt.4function verifyDynexSignature(rawBody, headers, secret) {5 const timestamp = String(headers["x-dynex-timestamp"] ?? "");6 const presented = String(headers["x-dynex-signature"] ?? "").trim();7 const ts = Number(timestamp);8 if (!Number.isInteger(ts) || ts <= 0) return false;9 if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false; // régi időbélyeg: visszajátszás10 11 const expected = "sha256=" + createHmac("sha256", secret).update(timestamp + ".").update(rawBody).digest("hex");12 const a = Buffer.from(expected, "utf8");13 const b = Buffer.from(presented, "utf8");14 return a.length === b.length && timingSafeEqual(a, b); // időzítés-biztos összehasonlítás15}Az aláírás önmagában nem mondja meg a csatornát
Sikeres ellenőrzés után az aláírt törzsből döntsd el, mit kaptál: ha a channel mező értéke app, vagy az esemény neve app. kezdetű, az életciklus-esemény, és csak az app esemény-titkával ellenőrizve fogadható el. Az SDK parseWebhookEvent() és parseAppEvent() függvénye ezt elvégzi helyetted.
| Kézbesítés | Válaszidő | Újrapróbálás | Sorozatos hiba után |
|---|---|---|---|
| Bérlői esemény, éles fiók | 10 mp | 5, 30, 120, 720 perc múlva (összesen 5 kísérlet) | 20 egymást követő hiba után a feliratkozás kikapcsol. |
| Bérlői esemény, teszt-fiók | 5 mp | Összesen 2 kísérlet: az első és egy ismétlés. | A hibás teszt-esemény is beleszámít a teszt-telepítésed hibaszámlálójába. |
Életciklus-esemény (app.*) | 10 mp | 5, 30, 120, 720 perc múlva (összesen 5 kísérlet) | 10 egymást követő hiba után a végpont 30 percre szünetel. |
- Sikeresnek a 2xx válasz számít; minden más státusz és az időtúllépés hiba.
- Válaszolj azonnal, és a munkát utána végezd el (sor, háttérfeladat): a lassú feldolgozás időtúllépésnek látszik, és újrapróbát vált ki.
- A
deliveryIdaz újrapróbák között állandó - ezzel szűrd a duplát. - A kézbesítéseket a portálon látod: a bérlői eseményekét az app „Webhookok” fülén a kézbesítési naplóban, az életciklus-eseményekét a „Napló” fülön.
Az app-szintű események nem egy fiók feliratkozásán jönnek, hanem a telepítésektől függetlenül, az appod webhook_url-jére. Ebből tudod meg például, hogy egy fiók eltávolította az appot - a fiók saját feliratkozása ilyenkor már nem él, a kulcs pedig 401-et ad.
| Esemény | Mikor jön | A data mezői |
|---|---|---|
app.installed | App telepítve. Egy fiók telepítette az appot - éles fiók vagy teszt-fiók. A sandbox mező mutatja, melyik. | installId, tenantId, sandbox, scopes, appVersion, installedAt |
app.uninstalled | App eltávolítva. Egy telepítés megszűnt. A telepítés API-kulcsa ekkor már nem él: állítsd le a hívásokat, és takarítsd el a telepítéshez tartozó adatokat. | installId, tenantId, sandbox, reason, uninstalledAt |
app.scopes_changed | Hatókörök változtak. A telepítés engedélyezett hatókörei változtak: a fiók újra hozzájárult (reconsent), vagy egy újonnan jóváhagyott verzió szűkítette őket (new_version). | installId, tenantId, sandbox, scopes, previousScopes, cause, appVersion |
app.suspended | App felfüggesztve. Az appot vagy a fejlesztői fiókodat felfüggesztettük. A telepítések kulcsai és webhookjai ilyenkor nem működnek. | cause, note, suspendedAt |
app.reinstated | App visszaállítva. A felfüggesztés megszűnt, a telepítések újra működnek. | cause, reinstatedAt |
app.version_approved | Verzió jóváhagyva. A beküldött verziót jóváhagytuk, mostantól ez az élő verzió. A hozzáadott és az elvett hatókörök külön mezőben jönnek. | submissionId, version, note, addedScopes, removedScopes, decidedAt |
app.version_rejected | Verzió elutasítva. A beküldött verziót visszaadtuk vagy elutasítottuk. A különbséget és a teendőt a note mező írja le. | submissionId, version, note, decidedAt |
app.ping | Teszt-ping. Csak teszt-eseményként létezik: ezzel próbálhatod ki a végpontodat és az aláírás ellenőrzését. | message |
1POST <webhook_url>2Content-Type: application/json3X-Dynex-Channel: app4X-Dynex-Event: app.installed5X-Dynex-Delivery-Id: <uuid>6X-Dynex-Timestamp: <unix mp>7X-Dynex-Signature: sha256=<hex(HMAC-SHA256(esemény-titok, "<timestamp>.<nyers törzs>"))>8 9{ "event": "app.installed", "deliveryId": "…", "timestamp": "2026-10-05T12:00:00.000Z",10 "clientId": "dxapp_…", "channel": "app", "test": false,11 "data": { "installId": "…", "tenantId": "…", "sandbox": false,12 "scopes": ["contacts.readonly"], "appVersion": 3, "installedAt": "…" } }- Válaszolj 2xx-szel 10 mp-en belül. Hibánál a Dynex 5, 30, 120, 720 perc múlva újrapróbálja (összesen 5 kísérlet). 10 egymást követő hiba után a végpontot 30 percre szüneteltetjük; a szünet alatt keletkező eseményt nem küldjük ki, de a portál „Napló” fülén látod, és onnan újraküldheted.
- A
deliveryIdaz újrapróbák között állandó - ezzel szűrd a duplát. A kézbesítések 30 napig látszanak a naplóban. - Az
app.uninstalledreasonmezője:tenant(a fiók távolította el),developer(te, a portálról),sandbox_reset(a teszt-fiókod visszaállítása) vagyplatform(minden más, a Dynex oldalán történt ok). - Teszt-eseményt a portál „Napló” fülén, az életciklus-események nézetéből küldhetsz. A törzsében
"test": trueáll és kitalált minta-azonosítókat hordoz: naplózd, de adatot ne módosíts rá. Teszt-eseményt egyszer próbálunk kézbesíteni, újrapróba nincs. - Esemény csak nyilvános
httpscímre megy. Ha a webhook URL-edlocalhost, az eseményt kihagyjuk - helyi fejlesztéshez alagút (pl. cloudflared, ngrok) kell. - Új eseménytípus később is jöhet: az ismeretlen
app.*eseményre válaszolj 2xx-szel, és hagyd figyelmen kívül.
| Csatorna | Miről ismered fel | Titok | SDK |
|---|---|---|---|
| Bérlői események (reservation.created, …) | Nincs X-Dynex-Channel fejléc; a törzsben tenantId áll. | A telepítés webhook_secret-je (a token-válaszból). | parseWebhookEvent() |
| App-szintű események (app.*) | X-Dynex-Channel: app fejléc, és az aláírt törzsben "channel": "app". | Az app esemény-titka (whsec_…): a portálon az app „Webhookok” fülén, „Esemény-aláíró kulcs” néven jeleníted meg. | parseAppEvent() |
A telepítés webhook_secret-jét soha ne használd app.* eseményre
A csatorna-fejléc nincs aláírva, ezért csak útválasztásra jó (isAppChannelEvent(headers)); a döntés az aláírt törzs channel mezőjén és az esemény-titkon múlik. Az SDK ezt betartatja: a parseAppEvent() csak az esemény-titokkal aláírt, app-csatornás törzset fogadja el, a parseWebhookEvent() pedig minden app.* eseményt elutasít.
1import { isAppChannelEvent, parseAppEvent, parseWebhookEvent, WebhookSignatureError } from "@dynex/app-sdk";2 3app.post("/webhooks", express.raw({ type: "*/*" }), async (req, res) => {4 try {5 if (isAppChannelEvent(req.headers)) {6 // App-csatorna: az app ESEMÉNY-TITKÁVAL (nem a telepítés webhook_secret-jével).7 const event = parseAppEvent(req.body, req.headers, process.env.DYNEX_EVENT_SECRET);8 if (!event.test && event.event === "app.uninstalled") {9 await db.installs.remove(event.data.installId); // a kulcs már nem él: takaríts10 }11 } else {12 const event = parseWebhookEvent(req.body, req.headers, install.webhookSecret);13 // event.event, event.tenantId, event.data14 }15 res.status(200).end();16 } catch (e) {17 if (e instanceof WebhookSignatureError) return res.status(401).end();18 throw e;19 }20});Az eseményeket a Dynex szerverei küldik, és azok a te gépedet (localhost) vagy egy belső hálózati címet nem érnek el. A webhook URL ezért csak nyilvános https cím lehet, akkor is, ha az app még nincs jóváhagyva; az ilyen címre szóló feliratkozás telepítéskor kimarad. Helyi géphez egy alagút ad nyilvános címet:
1# 1. Az appod fut a gépeden (a példa-app a 3999-es porton):2npm start3 4# 2. Nyiss rá egy nyilvános https címet - az egyik a kettő közül:5cloudflared tunnel --url http://localhost:39996ngrok http 39997 8# 3. A kapott https címet (a fogadó útvonalával, pl. https://<alagút-cím>/webhooks)9# írd be a portálon az app „Webhookok” fülén a webhook URL mezőbe.- Telepítsd (vagy telepítsd újra) az appot a teszt-fiókodra: a bérlői feliratkozás a telepítéskor jön létre.
- Küldj teszt-eseményt a saját teszt-telepítésedre: bérlői eseményt az app „Webhookok” fülének „Teszt-esemény” részéből, életciklus-eseményt a „Napló” fülről.
- Nézd meg az eredményt a kézbesítési naplóban: ott látod a státuszt és a hibát, és onnan újra is küldheted.
- Az alagút címe újraindításkor megváltozhat. Ilyenkor írd át a webhook URL-t a portálon; ha az események ezután sem az új címre jönnek, telepítsd újra az appot a teszt-fiókra.
- Ha az appnak már van élő (jóváhagyott) verziója, a telepített fiókok az élő verzió címére kapják az eseményeket; a webhook URL módosítása csak jóváhagyás után lép életbe.
- A hibás teszt-esemény is beleszámít a teszt-telepítésed hibaszámlálójába.
- Az alagút mögött is a nyilvános interneten vagy: fejlesztői vagy diagnosztikai végpontot, ami bérlői adatot ad vissza, ne hagyj nyitva.