Fejlesztői dokumentáció

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.

Kb. 10 perc olvasás

Webhook-aláírás ellenőrzése

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 deliveryId ritkán kétszer is érkezhet - a feldolgozás legyen idempotens.
Fogadás az SDK-val
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:

cURL
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}'
Node.js
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);

Ellenőrzés SDK nélkül

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:

  1. Olvasd ki az X-Dynex-Timestamp és az X-Dynex-Signature fejlécet. Ha bármelyik hiányzik, utasítsd el a kérést.
  2. Utasítsd el, ha az időbélyeg 5 percnél jobban eltér a szervered órájától.
  3. Számold ki a HMAC-SHA256 értéket a titokkal az <időbélyeg>.<nyers törzs> szövegre, hexában, sha256= előtaggal.
  4. 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.
Node.js, függőség nélkül
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.

Időkorlát és újrapróbálás

KézbesítésVálaszidőÚjrapróbálásSorozatos hiba után
Bérlői esemény, éles fiók10 mp5, 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ók5 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 mp5, 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 deliveryId az ú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.

Életciklus-események

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ényMikor jönA data mezői
app.installedApp 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.uninstalledApp 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_changedHató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.suspendedApp 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.reinstatedApp visszaállítva. A felfüggesztés megszűnt, a telepítések újra működnek.cause, reinstatedAt
app.version_approvedVerzió 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_rejectedVerzió 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.pingTeszt-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 deliveryId az ú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.uninstalled reason mező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) vagy platform (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 https címre megy. Ha a webhook URL-ed localhost, 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.

Melyik titok melyik csatornát ellenőrzi

CsatornaMiről ismered felTitokSDK
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.

Egy fogadó, két csatorna
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});

Helyi fejlesztés alagúttal

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.
  1. 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.
  2. 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.
  3. 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.