Fejlesztőknek

Dynex App Marketplace - fejlesztői útmutató

Építs saját, a te szervereden futó alkalmazást, amelyet a Dynex-ügyfelek egy kattintással telepítenek: az app a bérlő adataihoz a publikus v3 API-n és webhookokon fér hozzá, saját felülete a Dynex bal menüjében jelenik meg, és ingyenes vagy havidíjas lehet - a havidíj 80 %-a a tiéd.

Az API végpontjainak teljes referenciája külön oldalon van: API dokumentáció. A fejlesztői portál (regisztráció, appok, elszámolás): /developer - előfizetés nem kell hozzá.

Mi az a Dynex-app?

Egy Dynex-app három dolgot kap a platformtól, és mindhármat a te kódod használja a saját szervereden - idegen kód soha nem fut a Dynex szerverein:

  • Adathozzáférés: a bérlő a telepítéskor hozzájárul a kért hatókörökhöz, és az app egy dxk_ API-kulcsot kap, amely pontosan ezekre a hatókörökre jogosít a v3 API-n. Az eseményekről webhookon értesülsz.
  • Saját felület: az app egy vagy több menüpontot kap a Dynex bal menüjének „Alkalmazások” szekciójában; a menüpont a te URL-edet tölti be iframe-ben, aláírt kontextussal (ki a bérlő, ki a felhasználó, milyen téma).
  • Értékesítés: ingyenes vagy havidíjas app. A havidíj a bérlő meglévő Dynex-számlájára kerül, a bevételt havonta osztjuk: 80 % a fejlesztőé, 20 % a Dynexé.

A marketplace-re csak a Dynex tulajdonosának jóváhagyása után kerül ki egy app, és minden új verzió, valamint minden hatókör-bővítés újra jóváhagyásra megy. Amíg nincs jóváhagyva, az appot a teszt-fiókodra vagy a saját Dynex-fiókodra telepítheted.

Architektúra

RétegHol futMit csinál
Az app szervereNálad (bármilyen stack)OAuth callback, token-csere, API-hívások, webhook-fogadó, a beágyazott oldalak kiszolgálása.
Dynex v3 APIprod.dynex.hu/api/v3REST, Authorization: Bearer dxk_…, hatókör-ellenőrzés, kvóta, idempotencia.
OAuthprod.dynex.hu/api/apps/oauth/*Telepítés hozzájárulási képernyővel; authorization code → kulcs.
BeágyazásA Dynex bal menüje → iframe a te URL-edreAláírt kontextus-token a query-ben, kétirányú postMessage híd.
WebhookDynex → a te webhook_url-edHMAC-SHA256 aláírt POST az eseményekről, újrapróbálással.
SzámlázásDynexHavidíj a bérlő számláján, havi elszámolás a fejlesztői portálon.

Gyorsindítás 10 lépésben

  1. Fejlesztői profil. A fejlesztői portál külön réteg: nem kell hozzá Dynex-előfizetés. Regisztrálj a /developer/register oldalon (cégnév, weboldal, Fejlesztői szerződés), vagy ha már van Dynex-fiókod, lépj be a /developer portálra és hozd létre ott a profilod (kapcsolati e-mail, számlázási adatok a profilban).
  2. Új app. Add meg a nevet, slugot, leírást, az app_url-t (https), a redirect URI-kat (pontos egyezés!), a webhook URL-t, a kért hatóköröket, a beágyazott menüpontokat (path + title) és az árat. Kapsz egy client_id-t és egy client_secret-et - az utóbbi csak egyszer látszik, tárold biztonságosan.
  3. SDK. Töltsd le az SDK-csomagot (dynex-app-sdk.tgz) és telepítsd: npm install ./dynex-app-sdk.tgz (vagy indulj a példa-appból). Töltsd ki a .env-t a client_id / client_secret párral.
  4. OAuth-indítás. Egy „Telepítés a Dynexbe” gomb a buildAuthorizeUrl() által épített címre visz.
  5. Callback. A redirect URI-don a code-ot exchangeAuthorizationCode()-dal kulcsra cseréled, és a választ (kulcs, install_id, tenant_id, embed_secret, webhook_secret) bérlőnként elmented. A Dynex piacteréről indított telepítésnél a code state nélkül, az ELSŐ regisztrált redirect URI-dra érkezik - readMarketplaceCallbackParams()-szal fogadd, és a válasz tenant_id-ját mutasd meg a felhasználónak megerősítésre. A sikeres csere után küldd vissza a bérlőt a Dynexbe: buildMarketplaceReturnUrl({ slug }) = /app/marketplace/<slug>?telepitve=1 - a piactéri oldal ott a „telepítve” visszajelzést mutatja.
  6. Teszt-telepítés. A legegyszerűbb út a teszt-fiók: a portálon az app „Teszt-fiók” fülén egy kattintással telepíted, mintaadatokra, jóváhagyás nélkül. Ha van saját Dynex-fiókod, a régi út is megmaradt: draft állapotban az app olyan fiókra telepíthető, ahol te tag vagy. A piactér listája csak jóváhagyott appot mutat: a draft appot a szerkesztő „Telepítés a tesztfiókra” gombjával (a közvetlen /app/marketplace/<slug> címen) vagy az OAuth-linkről telepítsd a saját fiókodra.
  7. API-hívás. new DynexClient({ accessToken }) - próbáld a me()-t, majd a valódi végpontokat.
  8. Beágyazás. Az app_url + embeds[].path oldalad olvassa a dynex_token paramétert, ellenőrzi (verifyEmbedToken), a böngészőben pedig a createAppBridge() hívja a ready()-t és az autoResize()-t.
  9. Webhook. A telepítéskor a Dynex automatikusan feliratkozik az app default_webhook_events eseményeire a webhook URL-eden; a fogadóban parseWebhookEvent() ellenőrzi az aláírást. Ugyanerre a címre jönnek az app életciklus-eseményei is (telepítés, eltávolítás, jóváhagyás) - azokat a parseAppEvent() fogadja, másik titokkal.
  10. Beküldés. A fejlesztői portálon „Beküldés jóváhagyásra”. A tulajdonos jóváhagyja, visszaküldi megjegyzéssel vagy elutasítja; jóváhagyás után az app megjelenik a marketplace-en.

Teszt-fiók

A teszt-fiók ezen a környezeten még nincs bekapcsolva. Addig a saját Dynex-fiókodra telepítve tesztelhetsz (gyorsindítás, 6. lépés); az alábbiak a bekapcsolás után érvényesek.

A teszt-fiók a saját próbafiókod: egy mintaadatokkal feltöltött étterem (vendégek, foglalások, asztalok, étlap, rendelések, értékelések), amelyen az appod ugyanazt a v3 API-t, OAuth-folyamatot és webhookokat használja, mint élesben. Fejlesztőnként egy van, előfizetés nem kell hozzá, és a fejlesztői portálon az app „Teszt-fiók” fülén hozod létre.

  • Miről ismered fel: a teszt-fiók bérlő-azonosítója mindig sbx_ előtaggal kezdődik, éles fióké soha. Ugyanezt jelzi a token-válasz és a GET /me sandbox: true mezője, valamint a válaszok X-Dynex-Sandbox: true fejléce. Az SDK-ban: isSandboxTenantId(tenantId).
  • Semmi nem megy ki belőle: a teszt-fiókból nem megy ki e-mail, SMS és push, és automatizálás sem indul el. Az API ilyenkor is sikeres választ ad, a levelet pedig a Dynex elfogja: a „Teszt-fiók” fülön 7 napig megnézheted. Ne a postafiókodban keresd.
  • Telepítés: a „Teszt-fiók” fülön „Telepítés a teszt-fiókra”. A Dynex valódi code-dal küld az első regisztrált redirect URI-dra (state nélkül, mint a piactéri telepítésnél), a szervered pedig a szokásos token-cserével kap kulcsot. Jóváhagyás nem kell hozzá: jóváhagyott appnál a jóváhagyott verzió települ, egyébként a munkapéldányod. Havidíjas appért a teszt-fiók nem fizet.
  • Visszaállítás: a „Visszaállítás” gomb friss mintaadatokat ad, új bérlő-azonosítóval. A telepítéseid megszűnnek, a kulcsuk érvénytelen lesz, az appod pedig app.uninstalled eseményt kap reason: "sandbox_reset" okkal - utána telepítsd újra. Naponta legfeljebb 10 visszaállítás megy, kettő között 5 perc szünettel.
  • Keretek: kulcsonként percenként 60 kérés (élesben az alap 120), a teszt-fiók összes kulcsára együtt percenként 120, telepítésenként pedig napi 20 000 kérés (a számláló 00:00 UTC-kor indul újra).
  • Nincs felülete: a teszt-fiókba a Dynex alkalmazásban nem lehet belépni. Az adatait az API-n éred el, a portálon pedig az „API-próba” fülön a böngészőből hívhatod a végpontokat, a „Beágyazás-próba” fülön a beágyazott oldalaidat nézheted meg a Dynex keretében.

OAuth-folyamat

Szabványos OAuth 2.0 authorization code folyamat. A token-cseréhez mindig kell a client_secret; a PKCE (S256) ajánlott kiegészítő védelem. A kulcs nem jár le, de a bérlő bármikor eltávolíthatja az appot (akkor a kulcs visszavonódik), és hatókör-bővítés csak a bérlő újabb hozzájárulásával lehetséges.

1. Hozzájárulás

GET https://prod.dynex.hu/api/apps/oauth/authorize
  ?response_type=code
  &client_id=dxapp_…
  &redirect_uri=https://app.example.com/oauth/callback   # pontosan a regisztrált
  &state=<véletlen>                                       # CSRF-védelem
  &scope=contacts.readonly reservations.write             # opcionális; kihagyva az app összes hatóköre
  &code_challenge=<base64url(sha256(verifier))>&code_challenge_method=S256   # opcionális PKCE

→ a bérlő bejelentkezik (ha kell), látja a hozzájárulási képernyőt (hatókörök emberi néven, havidíj), megerősít
→ 302 https://app.example.com/oauth/callback?code=dxac_…&state=<ugyanaz>
  vagy elutasításnál: ?error=access_denied&state=…

2. Token-csere

POST https://prod.dynex.hu/api/apps/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=dxac_…&client_id=dxapp_…
&client_secret=dxas_…            # KÖTELEZŐ
&code_verifier=…                 # ha az authorize-ban code_challenge-et küldtél (PKCE)
&redirect_uri=https://app.example.com/oauth/callback   # KÖTELEZŐ, pontosan az authorize-beli

200 { "access_token": "dxk_…", "token_type": "Bearer", "install_id": "…", "tenant_id": "…",
      "scopes": ["contacts.readonly", "reservations.write"], "embed_secret": "…",
      "webhook_secret": "whsec_…",    # null, ha az appnak nincs webhook-feliratkozása
      "sandbox": false }              # true, ha a telepítés a teszt-fiókodra történt
4xx { "error": "invalid_grant", "error_description": "Authorization code expired." }
HibakódMikor
invalid_requestHiányzó vagy rossz paraméter; a redirect_uri nincs regisztrálva; nem S256 PKCE.
invalid_clientIsmeretlen client_id, hiányzó vagy rossz client_secret.
invalid_grantIsmeretlen, már felhasznált vagy lejárt code (10 perc); eltérő redirect_uri; hibás code_verifier; a telepítés már nem aktív.
unauthorized_clientAz app fel van függesztve.
invalid_scopeA kért hatókör ismeretlen, vagy nem része az app jóváhagyott hatóköreinek.
access_deniedA bérlő elutasította a hozzájárulást (a redirect_uri-n érkezik).
server_errorBelső hiba; próbáld újra az engedélyezést.

A code egyszer használatos; párhuzamos beváltásnál csak az egyik nyer. A redirect_uri a token-kérésben is kötelező (nélküle invalid_request). Az embed_secret a telepítés saját titka a beágyazási token helyi ellenőrzéséhez: telepítésenként más, csak ebben a válaszban kapod meg, és a client_secret újragenerálása nem változtatja meg. Tárold a telepítés mellett. A webhook_secret az install webhook-feliratkozásának titka: installonként állandó, újra-engedélyezéskor ugyanaz jön vissza.

Hatókörök

Az app csak azokat a hatóköröket kérheti, amelyeket a beküldéskor megadott és a tulajdonos jóváhagyott; a bérlő a hozzájárulási képernyőn ezeket az emberi neveket látja. Az .readonly végű hatókörök csak olvasnak.

HatókörMit enged a bérlőnek megjelenítveTípus
reservations.readonlyFoglalások megtekintéseolvasás
reservations.writeFoglalások létrehozása és módosításaírás
reservations.manageFoglalások teljes kezelése (törlés, státusz)írás
contacts.readonlyKapcsolatok megtekintéseolvasás
contacts.writeKapcsolatok létrehozása és módosításaírás
contacts.deleteKapcsolatok törléseírás
deals.readonlyÜzletek (pipeline) megtekintéseolvasás
deals.writeÜzletek létrehozása és mozgatásaírás
orders.readonlyRendelések megtekintéseolvasás
menu.readonlyÉtlap megtekintéseolvasás
menu.writeÉtlap szerkesztéseírás
loyalty.readonlyHűségprogram-egyenlegek megtekintéseolvasás
reviews.readonlyÉrtékelések megtekintéseolvasás
webhooks.manageWebhook-feliratkozások kezeléseírás
content.readonlyTartalmak (blog) megtekintéseolvasás
content.writeTartalmak (blog) írásaírás
cameras.writeKamerás vendégszámláló adatok beküldéseírás
conversions.writeKonverziós események küldéseírás
calendar.readonlyNaptárak és időpontok megtekintéseolvasás
calendar.writeIdőpontok foglalása és lemondásaírás
webshop.readonlyWebshop-termékek és -rendelések megtekintéseolvasás
webshop.writeWebshop-termékek szerkesztése, rendelések állapotának léptetéseírás
ticketing.readonlyRendezvények, jegyrendelések és jegyek megtekintéseolvasás
ticketing.writeJegyek érvényesítése (beléptetés)írás
attendance.readonlyMunkaidő-adatok megtekintése (dolgozók, beosztás, jelenlét)olvasás
attendance.absences.readonlyDolgozói távollétek megtekintése (betegszabadság is - egészségügyi adat)olvasás
erp.readonlyVállalatirányítási adatok megtekintéseolvasás
erp.bank.readonlyBanki tranzakciók megtekintése (bérutalások és magánszemélyek adatai is)olvasás
emails.readonlyE-mail sablonok és kampányok megtekintéseolvasás
emails.sendE-mail küldése a kapcsolataidnak a nevedbenírás

Webhook-események, amelyekre az app alapértelmezetten feliratkozhat (default_webhook_events):

reservation.createdreservation.updatedreservation.cancelledcontact.createdcontact.updatedcontact.deleteddeal.createddeal.stage_changeddeal.wondeal.lostreview.createdreview.replied

Beágyazás és a híd

Az app minden embeds[] bejegyzése egy menüpont a bérlő bal menüjében. Rákattintva a Dynex az /app/apps/<slug>/… oldalon egy teljes magasságú iframe-ben tölti be az app_url + path címet, a query-ben egy aláírt kontextus-tokennel:

https://app.example.com/dashboard
  ?dynex_token=<base64url(payload)>.<base64url(HMAC-SHA256(embed_secret, base64url(payload)))>
  &dynex_theme=light|dark        # az induló téma, hogy az első festés se villanjon

Két token, egy formátum. A query-ben (dynex_token) érkező token PII-mentes: csak az azonosítókat hordozza, a tenantName, userEmail és userRole mezője üres string - az iframe URL az app hozzáférési naplójába és a Refererbe kerül, oda ezek nem valók. A teljes kontextus tokenje a hídon (dynex:context, a dynex:ready után azonnal, majd 4 percenként) és a POST /api/apps/embed/verify válaszában jön - a nevet, e-mailt, szerepet onnan vedd (bridge.onContext).

Payload-mezőJelentés
installIdA telepítés azonosítója (= a token-csere install_id-ja).
tenantIdA bérlő azonosítója.
tenantNameA bérlő neve. A query-tokenben üres (a hídon és a verify-válaszban kitöltve).
userIdA Dynex-felhasználó azonosítója, aki az oldalt nézi.
userEmailA felhasználó e-mail címe. A query-tokenben üres (csak a hídon).
userRoleA felhasználó szerepe a bérlőnél (pl. owner, admin, staff). A query-tokenben üres (csak a hídon).
localeNyelv (jelenleg hu).
themelight | dark - kövesd, hogy az iframe ne üssön el a kerettől.
appSlugAz app slugja.
iatKiadás (unix mp).
expLejárat (unix mp): a kiadástól 5 perc. Ellenőrzéskor 30 mp óraeltérés megengedett.
nonceEgyszeri véletlen - visszajátszás-védelemhez az app oldalán.

Ellenőrzés: helyben a verifyEmbedTokenForInstall(token, getEmbedSecret)-tel (a token telepítésének tárolt embed_secret-jével), vagy a Dynexen a POST /api/apps/embed/verify végponton ({ client_id, client_secret, token } → { ok, context, exp, nonce }), ami azt is megnézi, hogy a telepítés még aktív-e. A helyi ellenőrzés mellé ajánlott a rendszeres távoli ellenőrzés (pl. minden új sessionnél), mert az eltávolítást vagy felfüggesztést csak a Dynex látja. A token rövid életű: a betöltés utáni saját sessionödet te kezeled, ne a tokenre építs.

  • A token a query-ben érkezik: PII-mentes, de azonosítókat hordoz (tenantId, userId, installId) és bemutatásra jogosít: ne naplózd (a hozzáférési naplóból maszkold a dynex_token-t), ellenőrzés után azonnal válts saját sessionre, és 302-vel irányíts a token nélküli URL-re, hogy ne maradjon az előzményekben és a Refererben.
  • A kontextus mezői (tenantName, userEmail, …) bérlői bevitel: HTML-be csak escape-elve írd ki, különben tárolt XSS az app originjén.
  • Az egyszeri nonce ellenőrzése nem elvárás - a védelem az aláírás és a 5 perces lejárat. Ha mégis egyszer fogadod el, tartsd a nonce-t csak a lejáratig, és az élő sessiont ne dobd el: a Dynex keret újratöltése ugyanazzal a tokennel jöhet.
  • Iframe-ben a saját sütid csak SameSite=None; Secure-ral megy; Safari a harmadik-fél sütit alapból blokkolja - ott a Storage Access API vagy a hídon 4 percenként érkező friss token a megoldás.

A postMessage híd

Mindkét irány kizárólag a másik fél originjével megy (targetOrigin és event.origin ellenőrzés), és az app csak a közvetlen szülő ablak (event.source === window.parent) üzenetét fogadja el. Az üzenetek JSON-objektumok, a type mező a diszkriminátor.

IrányÜzenetMezőkHatás
app → Dynexdynex:ready-Az app betöltött; a Dynex válaszul dynex:context-et küld.
app → Dynexdynex:resizeheightAz iframe magasságának beállítása (px).
app → Dynexdynex:navigatepathNavigálás a Dynexen belül (pl. /app/reservations); válasz: dynex:navigate-result.
app → Dynexdynex:toastkind, messageÉrtesítés a keretben (success | error | info | warning).
app → Dynexdynex:getContext-Kontextus újrakérése; válasz: dynex:context.
app → Dynexdynex:openExternalurlKülső URL megnyitása új lapon.
Dynex → appdynex:contextcontext, tokenA kontextus (a fenti payload mezői) és egy friss token.
Dynex → appdynex:themethemeTéma-váltás (light | dark).
Dynex → appdynex:navigate-resultok, path, error?A navigálás eredménye.
import { createAppBridge } from "@dynex/app-sdk/browser";

// A Dynex origin a SZERVER konfigjából (pl. <meta name="dynex-origin">), ne a referrerből:
// egy idegen oldalba ágyazott másolatnál a referrer a támadó originje lenne.
const dynexOrigin = document.querySelector('meta[name="dynex-origin"]')?.content;
const bridge = createAppBridge({ dynexOrigin }); // kihagyva: a dynex_origin query-paraméter, majd a referrer - csak https://*.dynex.hu számít, különben prod.dynex.hu
bridge.onContext(({ context }) => render(context)); // context.tenantName, userEmail: escape-elve jelenítsd meg
bridge.onTheme((theme) => document.documentElement.dataset.theme = theme);
bridge.autoResize();                           // ResizeObserver → dynex:resize
bridge.ready();
saveButton.onclick = () => bridge.toast("success", "Elmentve.");
link.onclick = () => bridge.navigate("/app/reservations");

A Dynex az app-oldalakon frame-src 'self' https: CSP-fejlécet küld (fejlesztői buildben http://localhost:* is engedett), a valódi kapu az alkalmazás-réteg: az iframe csak az aktív, jóváhagyott (vagy saját sandbox-) telepítés app_url-jét tölti be, és a keret minden üzenet előtt ellenőrzi, hogy az iframe originje az appé. Élesben ezért a sandbox-telepítéshez is https app_url kell (pl. ngrok / cloudflared alagút). Felfüggesztett app menüpontja eltűnik, és az iframe nem töltődik be. A másik irány a te dolgod: a beágyazott oldalad küldjön Content-Security-Policy: frame-ancestors https://prod.dynex.hu fejlécet, hogy idegen oldal ne ágyazhassa be.

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:

POST <webhook_url>
Content-Type: application/json
X-Dynex-Event: reservation.created
X-Dynex-Delivery-Id: <uuid>
X-Dynex-Timestamp: <unix mp>
X-Dynex-Signature: sha256=<hex(HMAC-SHA256(secret, "<timestamp>.<nyers törzs>"))>

{ "event": "reservation.created", "deliveryId": "…", "timestamp": "2026-09-15T10:00:00.000Z",
  "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.
import { parseWebhookEvent, WebhookSignatureError } from "@dynex/app-sdk";

app.post("/webhooks", express.raw({ type: "*/*" }), (req, res) => {
  try {
    const event = parseWebhookEvent(req.body, req.headers, install.webhookSecret);
    // event.event, event.tenantId, event.data
    res.status(200).end();
  } catch (e) {
    if (e instanceof WebhookSignatureError) return res.status(401).end();
    throw e;
  }
});

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 a következő szakaszt.

É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
POST <webhook_url>
Content-Type: application/json
X-Dynex-Channel: app
X-Dynex-Event: app.installed
X-Dynex-Delivery-Id: <uuid>
X-Dynex-Timestamp: <unix mp>
X-Dynex-Signature: sha256=<hex(HMAC-SHA256(esemény-titok, "<timestamp>.<nyers törzs>"))>

{ "event": "app.installed", "deliveryId": "…", "timestamp": "2026-10-05T12:00:00.000Z",
  "clientId": "dxapp_…", "channel": "app", "test": false,
  "data": { "installId": "…", "tenantId": "…", "sandbox": false,
            "scopes": ["contacts.readonly"], "appVersion": 3, "installedAt": "…" } }

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.
  • 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.
import { isAppChannelEvent, parseAppEvent, parseWebhookEvent, WebhookSignatureError } from "@dynex/app-sdk";

app.post("/webhooks", express.raw({ type: "*/*" }), async (req, res) => {
  try {
    if (isAppChannelEvent(req.headers)) {
      // App-csatorna: az app ESEMÉNY-TITKÁVAL (nem a telepítés webhook_secret-jével).
      const event = parseAppEvent(req.body, req.headers, process.env.DYNEX_EVENT_SECRET);
      if (!event.test && event.event === "app.uninstalled") {
        await db.installs.remove(event.data.installId); // a kulcs már nem él: takaríts
      }
    } else {
      const event = parseWebhookEvent(req.body, req.headers, install.webhookSecret);
      // event.event, event.tenantId, event.data
    }
    res.status(200).end();
  } catch (e) {
    if (e instanceof WebhookSignatureError) return res.status(401).end();
    throw e;
  }
});

Több titkos kulcs

A több titkos kulcs kezelése ezen a környezeten még nincs bekapcsolva. Addig a „Hitelesítés” fülön az azonnali csere érhető el: az új kulcs kiadásakor a régi rögtön érvényét veszti.

Egy appnak egyszerre több érvényes client_secret-je lehet (legfeljebb 5), így a kulcscsere leállás nélkül megy:

  1. A portálon az app „Hitelesítés” fülén kérj „Új titkos kulcs”-ot, és válaszd ki, mikor járjanak le a régiek (alapból 24 óra múlva, legfeljebb 30 nap). Az új kulcs csak egyszer látszik.
  2. Telepítsd az új kulcsot a szervereden. Az átfedés alatt a régi és az új is érvényes mindenhol, ahol a client_secret-et küldöd (token-csere, /api/apps/embed/verify).
  3. A régi kulcs a megadott időben magától lejár. Ha minden példányod átállt, előbb is visszavonhatod - a listában látod, melyik kulcsot mikor használták utoljára.
  • Kiszivárgás gyanújánál ne várj átfedésre: kérj új kulcsot úgy, hogy a régiek azonnal lejárjanak, vagy vond vissza a kiszivárgott kulcsot.
  • Legalább egy lejárat nélküli kulcs mindig marad: az utolsót nem lehet visszavonni, előbb újat kell létrehoznod.
  • A csere nem érinti a telepítések dxk_ kulcsait, az embed_secret-et és a webhook_secret-et: a futó telepítéseid a csere alatt is működnek.
  • Az esemény-titok külön titok (az életciklus-események aláírásához), és egyszerre egy él belőle. A cseréje azonnali: amíg az újat nem telepíted, az ellenőrzésed elutasítja az eseményeket, és az újrapróbálás hozza őket újra.

Kérés-azonosító és keret-fejlécek

A v3 API minden válasza kap egy kérés-azonosítót, a hitelesített válaszok pedig a percenkénti keret állását is hordozzák.

FejlécMikor jönJelentés
X-Request-IdMinden válaszon, hibánál is.req_ és 24 karakter. Naplózd: a portál „Napló” fülén erre keresve megtalálod a kérést, és hibabejelentésnél is ezt kérjük.
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-ResetMinden hitelesített válaszon, a 429-en is.A percenkénti keret, a hátralévő hívások száma, és hány másodperc múlva indul újra a percablak (nem időbélyeg). A számláló szerver-példányonként él, ezért a hátralévő érték tájékoztató.
X-Dynex-SandboxHa a kulcs teszt-fiókhoz tartozik.true
Retry-AfterA 429-es válaszon.Ennyi másodperc múlva próbáld újra.

A hiba-törzsben is ott az azonosító (error.request_id), a hiányzó hatókör miatti 403-nál pedig az is, melyik hatókör kellett volna (error.required_scope):

403 { "error": { "code": "forbidden", "message": "Token lacks required scope: contacts.write.",
                 "required_scope": "contacts.write", "request_id": "req_…" } }
try {
  await dynex.contacts.create({ email: "anna@example.com" });
} catch (e) {
  if (e instanceof DynexApiError) {
    console.error(e.status, e.code, e.message, "request_id:", e.requestId); // mindig naplózd
    if (e.requiredScope) console.error("hiányzó hatókör:", e.requiredScope);
    if (e.isRateLimited) await sleep((e.retryAfterSec ?? 1) * 1000);
  }
}
dynex.lastRateLimit; // { limit, remaining, resetSec } az utolsó válaszból
dynex.lastRequestId; // az utolsó válasz azonosítója, sikeres hívásnál is
  • Mit látsz a „Napló” fülön: teszt-fiókos telepítésnél a kéréseket a kérés és a válasz törzsével együtt (a titkok kitakarva), 7 napig. Éles telepítésnél csak a hibás kéréseket, törzs és fiók-azonosító nélkül, 14 napig. A 429-es válaszok nem kerülnek a naplóba.
  • Ezeket a fejléceket a /api/v3 végpontjai küldik; a leíró végpontok (/api/v3 gyökér, openapi.json, postman.json) és a régi /api/v1 nem.

Számlázás és elszámolás

  • Ingyenes app: nincs számlázás; a bérlő a hozzájárulással telepít.
  • Havidíjas app: a telepítéskor a bérlő meglévő Dynex-előfizetéséhez egy „Alkalmazás: <név>” tétel kerül a megadott nettó havi díjjal (Ft). A bérlő a Számlázás oldalán látja; a fizetés a Dynex meglévő motorján megy. Eltávolításnál a tétel a hónap végéig jár. Az app (vagy a fejlesztő) felfüggesztésekor a Dynex a bérlők tételét a folyó hónap végére lemondja - felfüggesztett appért a bérlő nem fizet tovább; visszaállításkor a tétel az eredeti (telepítéskori) árral újraéled. A felfüggesztés hónapjára a fejlesztőnek nem jár részesedés.
  • Elszámolás: minden hónap 1-jén az előző hónapra minden fizetős telepítéshez egy elszámolási sor készül: gross = a hónapra ténylegesen terhelt díj (ha a bérlő nem fizetett vagy lemondott: 0), fejlesztő = gross × 80 % (egész Ft, lefelé kerekítve), a maradék a Dynexé. A részesedést a tulajdonos appra egyedileg módosíthatja.
  • Kifizetés: a fejlesztői portálon látod a havi tételeket; a Dynex a fejlesztői profilban megadott számlázási adatok alapján kéri a számlát, és a kifizetés után „kifizetve” jelöléssel zár. Nincs automatikus utalás.

Beküldés és jóváhagyás

ÁllapotJelentés
draftMunkapéldány: a teszt-fiókodra és a saját (tag) fiókodra telepíthető, másra nem.
submittedBeküldve; a tulajdonos látja a diffet az előző élő verzióhoz (hatókörök, beágyazások, URL-ek, ár).
approvedÉlő a marketplace-en. A beküldött pillanatkép lett az élő verzió; a további szerkesztés a munkapéldányt írja, újabb beküldésig nem látszik.
rejectedElutasítva megjegyzéssel; javítás után újra beküldhető.
suspendedFelfüggesztve: minden telepítés kulcsa érvénytelen, a menüpontok eltűnnek, az OAuth nem működik, a bérlők havidíja a hónap végére lemondva.

Egyszerre egy függő beküldés lehet. Élő appnál a beküldés nem szünteti meg az élő verziót. Hatókör-bővítés jóváhagyása után a meglévő telepítések a régi hatókörökkel futnak tovább, az új hatókört a bérlő újbóli hozzájárulása (az OAuth-link ismételt megnyitása) adja.

Biztonsági követelmények

  • Minden URL (app_url, redirect_uri, webhook_url) https; fejlesztéshez kizárólag http://localhost engedett.
  • A redirect_uri pontosan egyezik a regisztrált listával - nincs minta, nincs prefix, nincs query-eltérés.
  • A client_secret, a dxk_ kulcs és az embed_secret csak a szervereden él; soha ne kerüljön böngészőbe, mobil-appba vagy repóba. A Dynex a secretet és a kulcsot hashelve tárolja. Kiszivárgás gyanújánál cseréld le a client_secret-et a fejlesztői portálon (lásd: Több titkos kulcs). Az embed_secret telepítésenként más, így egy kiszivárgott érték csak azt az egy telepítést érinti; a helyi ellenőrzés mellé a távoli /api/apps/embed/verify ellenőrzést is érdemes használni.
  • Az appod bizalmas kliens: a token-csere a szervereden fut a client_secret-tel. A PKCE S256 kiegészítő védelem, nem helyettesíti.
  • A state paramétert véletlenből képezd, a felhasználó sessionjében (HttpOnly süti) tárold, és a callbackben ezzel vesd össze; hiányzó session-értéknél utasítsd el a callbacket (az SDK readCallbackParams-a üres elvárt state-nél null-t ad). A függő state-eket 10 perc után takarítsd.
  • A beágyazási tokent minden betöltéskor ellenőrizd (aláírás + lejárat), ne naplózd, és váltsd saját sessionre; a híd üzeneteit csak a Dynex originről és csak a szülő ablaktól fogadd el; a beágyazott oldal küldjön frame-ancestors CSP-t.
  • A kontextus mezői (tenantName, userEmail, …) és a callback paraméterei idegen bevitel: HTML-be csak escape-elve.
  • Fejlesztői/diagnosztikai végpontot, ami bérlői adatot ad vissza, ne hagyj nyitva (tunnel mögött sem).
  • Webhookot csak érvényes aláírással dolgozz fel; a bérlőt az aláírt törzs tenantId-jából vedd, ne a URL-ből.
  • Két webhook-csatorna, két titok: app.* eseményt csak az app esemény-titkával ellenőrizve (parseAppEvent) fogadj el, a telepítés webhook_secret-jével soha. Az X-Dynex-Channel fejléc nincs aláírva, önmagában semmit nem bizonyít.
  • A kulcs a telepítés hatóköreire korlátozott; ami nem kell, ne kérd. Egy 403 forbidden válasz hiányzó hatókört jelent - hogy melyiket, azt az error.required_scope mező mondja meg.
  • Tenant-izoláció nálad is: minden tárolt adatot az install_id / tenant_id alá köss, és eltávolításkor (app.uninstalled esemény; a kulcs ekkor már 401-et ad) töröld a bérlő adatait.
  • Kvóta: a kulcsra a v3 API percenkénti és napi keretei érvényesek. A percenkénti keret állását az X-RateLimit-* fejlécek mutatják (tájékoztató érték), a túllépést a 429 státusz jelzi, a várakozás hosszát a Retry-After fejléc.

SDK és példa-app

@dynex/app-sdk (TypeScript, Node 18+): DynexClient (contacts, reservations, availability, loyalty, orders, reviews, webhooks, me; Idempotency-Key támogatás), exchangeAuthorizationCode, buildAuthorizeUrl, createPkcePair, verifyWebhookSignature / parseWebhookEvent (bérlői események), isAppChannelEvent / parseAppEvent (életciklus-események), isSandboxTenantId, verifyEmbedToken / verifyEmbedTokenRemote, és a böngészős createAppBridge (@dynex/app-sdk/browser). A hibák a DynexApiError-ban jönnek, a kérés-azonosítóval (requestId) és a hiányzó hatókörrel (requiredScope).

import { DynexClient, DynexApiError } from "@dynex/app-sdk";

const dynex = new DynexClient({ accessToken: install.accessToken });
const me = await dynex.me();
const { contacts } = await dynex.contacts.list({ updatedSince: lastSyncIso, limit: 100 });
const { reservation, reference } = await dynex.reservations.create(
  { date: "2026-10-01", startTime: "19:00", partySize: 4, customerName: "Kiss Anna", customerPhone: "+36301234567" },
  { idempotencyKey: "order-1234" },
);

A examples/dynex-app-hello egy minimális Express-app: OAuth callback token-cserével (sütihez kötött state + PKCE), beágyazott oldal a híddal (token → saját session → tiszta URL, frame-ancestors CSP, toast és navigálás gombok, a kontextus escape-elve kiírva), aláírás-ellenőrző webhook-fogadó mindkét csatornára (az app.uninstalled eseményre törli a telepítést), a hibáknál a kérés-azonosító naplózásával. A README lépésről lépésre végigvisz a localhoston futó teszten (a http://localhost:3999 redirect URI regisztrálható).