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éteg | Hol fut | Mit csinál |
|---|---|---|
| Az app szervere | Nálad (bármilyen stack) | OAuth callback, token-csere, API-hívások, webhook-fogadó, a beágyazott oldalak kiszolgálása. |
| Dynex v3 API | prod.dynex.hu/api/v3 | REST, Authorization: Bearer dxk_…, hatókör-ellenőrzés, kvóta, idempotencia. |
| OAuth | prod.dynex.hu/api/apps/oauth/* | Telepítés hozzájárulási képernyővel; authorization code → kulcs. |
| Beágyazás | A Dynex bal menüje → iframe a te URL-edre | Aláírt kontextus-token a query-ben, kétirányú postMessage híd. |
| Webhook | Dynex → a te webhook_url-ed | HMAC-SHA256 aláírt POST az eseményekről, újrapróbálással. |
| Számlázás | Dynex | Havidíj a bérlő számláján, havi elszámolás a fejlesztői portálon. |
Gyorsindítás 10 lépésben
- 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).
- Ú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 egyclient_id-t és egyclient_secret-et - az utóbbi csak egyszer látszik, tárold biztonságosan. - 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. - OAuth-indítás. Egy „Telepítés a Dynexbe” gomb a
buildAuthorizeUrl()által épített címre visz. - Callback. A redirect URI-don a
code-otexchangeAuthorizationCode()-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 codestatenélkül, az ELSŐ regisztrált redirect URI-dra érkezik -readMarketplaceCallbackParams()-szal fogadd, és a választenant_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. - 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. - API-hívás.
new DynexClient({ accessToken })- próbáld ame()-t, majd a valódi végpontokat. - Beágyazás. Az
app_url + embeds[].patholdalad olvassa adynex_tokenparamétert, ellenőrzi (verifyEmbedToken), a böngészőben pedig acreateAppBridge()hívja aready()-t és azautoResize()-t. - Webhook. A telepítéskor a Dynex automatikusan feliratkozik az app
default_webhook_eventseseményeire a webhook URL-eden; a fogadóbanparseWebhookEvent()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 aparseAppEvent()fogadja, másik titokkal. - 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 aGET /mesandbox: truemezője, valamint a válaszokX-Dynex-Sandbox: truefejlé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 (statené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.uninstalledeseményt kapreason: "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ód | Mikor |
|---|---|
invalid_request | Hiányzó vagy rossz paraméter; a redirect_uri nincs regisztrálva; nem S256 PKCE. |
invalid_client | Ismeretlen client_id, hiányzó vagy rossz client_secret. |
invalid_grant | Ismeretlen, 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_client | Az app fel van függesztve. |
invalid_scope | A kért hatókör ismeretlen, vagy nem része az app jóváhagyott hatóköreinek. |
access_denied | A bérlő elutasította a hozzájárulást (a redirect_uri-n érkezik). |
server_error | Belső 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ör | Mit enged a bérlőnek megjelenítve | Típus |
|---|---|---|
reservations.readonly | Foglalások megtekintése | olvasás |
reservations.write | Foglalások létrehozása és módosítása | írás |
reservations.manage | Foglalások teljes kezelése (törlés, státusz) | írás |
contacts.readonly | Kapcsolatok megtekintése | olvasás |
contacts.write | Kapcsolatok létrehozása és módosítása | írás |
contacts.delete | Kapcsolatok törlése | írás |
deals.readonly | Üzletek (pipeline) megtekintése | olvasás |
deals.write | Üzletek létrehozása és mozgatása | írás |
orders.readonly | Rendelések megtekintése | olvasás |
menu.readonly | Étlap megtekintése | olvasás |
menu.write | Étlap szerkesztése | írás |
loyalty.readonly | Hűségprogram-egyenlegek megtekintése | olvasás |
reviews.readonly | Értékelések megtekintése | olvasás |
webhooks.manage | Webhook-feliratkozások kezelése | írás |
content.readonly | Tartalmak (blog) megtekintése | olvasás |
content.write | Tartalmak (blog) írása | írás |
cameras.write | Kamerás vendégszámláló adatok beküldése | írás |
conversions.write | Konverziós események küldése | írás |
calendar.readonly | Naptárak és időpontok megtekintése | olvasás |
calendar.write | Időpontok foglalása és lemondása | írás |
webshop.readonly | Webshop-termékek és -rendelések megtekintése | olvasás |
webshop.write | Webshop-termékek szerkesztése, rendelések állapotának léptetése | írás |
ticketing.readonly | Rendezvények, jegyrendelések és jegyek megtekintése | olvasás |
ticketing.write | Jegyek érvényesítése (beléptetés) | írás |
attendance.readonly | Munkaidő-adatok megtekintése (dolgozók, beosztás, jelenlét) | olvasás |
attendance.absences.readonly | Dolgozói távollétek megtekintése (betegszabadság is - egészségügyi adat) | olvasás |
erp.readonly | Vállalatirányítási adatok megtekintése | olvasás |
erp.bank.readonly | Banki tranzakciók megtekintése (bérutalások és magánszemélyek adatai is) | olvasás |
emails.readonly | E-mail sablonok és kampányok megtekintése | olvasás |
emails.send | E-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 villanjonKé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 |
|---|---|
installId | A telepítés azonosítója (= a token-csere install_id-ja). |
tenantId | A bérlő azonosítója. |
tenantName | A bérlő neve. A query-tokenben üres (a hídon és a verify-válaszban kitöltve). |
userId | A Dynex-felhasználó azonosítója, aki az oldalt nézi. |
userEmail | A felhasználó e-mail címe. A query-tokenben üres (csak a hídon). |
userRole | A felhasználó szerepe a bérlőnél (pl. owner, admin, staff). A query-tokenben üres (csak a hídon). |
locale | Nyelv (jelenleg hu). |
theme | light | dark - kövesd, hogy az iframe ne üssön el a kerettől. |
appSlug | Az app slugja. |
iat | Kiadás (unix mp). |
exp | Lejárat (unix mp): a kiadástól 5 perc. Ellenőrzéskor 30 mp óraeltérés megengedett. |
nonce | Egyszeri 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 adynex_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
nonceellenő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 | Üzenet | Mezők | Hatás |
|---|---|---|---|
| app → Dynex | dynex:ready | - | Az app betöltött; a Dynex válaszul dynex:context-et küld. |
| app → Dynex | dynex:resize | height | Az iframe magasságának beállítása (px). |
| app → Dynex | dynex:navigate | path | Navigálás a Dynexen belül (pl. /app/reservations); válasz: dynex:navigate-result. |
| app → Dynex | dynex:toast | kind, message | Értesítés a keretben (success | error | info | warning). |
| app → Dynex | dynex:getContext | - | Kontextus újrakérése; válasz: dynex:context. |
| app → Dynex | dynex:openExternal | url | Külső URL megnyitása új lapon. |
| Dynex → app | dynex:context | context, token | A kontextus (a fenti payload mezői) és egy friss token. |
| Dynex → app | dynex:theme | theme | Téma-váltás (light | dark). |
| Dynex → app | dynex:navigate-result | ok, 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
deliveryIdritká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é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 |
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
| 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áldapp.*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örzschannelmezőjén és az esemény-titkon múlik. Az SDK ezt betartatja: aparseAppEvent()csak az esemény-titokkal aláírt, app-csatornás törzset fogadja el, aparseWebhookEvent()pedig mindenapp.*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
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.
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:
- 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.
- 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). - 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, azembed_secret-et és awebhook_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éc | Mikor jön | Jelentés |
|---|---|---|
X-Request-Id | Minden 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-Reset | Minden 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-Sandbox | Ha a kulcs teszt-fiókhoz tartozik. | true |
Retry-After | A 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/v3végpontjai küldik; a leíró végpontok (/api/v3gyökér,openapi.json,postman.json) és a régi/api/v1nem.
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
| Állapot | Jelentés |
|---|---|
draft | Munkapéldány: a teszt-fiókodra és a saját (tag) fiókodra telepíthető, másra nem. |
submitted | Bekü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. |
rejected | Elutasítva megjegyzéssel; javítás után újra beküldhető. |
suspended | Felfü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://localhostengedett. - A redirect_uri pontosan egyezik a regisztrált listával - nincs minta, nincs prefix, nincs query-eltérés.
- A
client_secret, adxk_kulcs és azembed_secretcsak 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 aclient_secret-et a fejlesztői portálon (lásd: Több titkos kulcs). Azembed_secrettelepí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/verifyellenő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
stateparamé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 SDKreadCallbackParams-a üres elvárt state-nélnull-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-ancestorsCSP-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éswebhook_secret-jével soha. AzX-Dynex-Channelfejlé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
forbiddenválasz hiányzó hatókört jelent - hogy melyiket, azt azerror.required_scopemező mondja meg. - Tenant-izoláció nálad is: minden tárolt adatot az
install_id/tenant_idalá köss, és eltávolításkor (app.uninstalledesemé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 a429státusz jelzi, a várakozás hosszát aRetry-Afterfejléc.
SDK és példa-app
npm-csomag (ESM, TypeScript-típusokkal). Telepítés a projektedben: npm install ./dynex-app-sdk.tgz
Express-app: OAuth-callback, beágyazott oldal a híddal, webhook-fogadó. npm install és npm start, a README szerint.
@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ó).