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 csak a saját tesztfiókodra telepítheted (sandbox).
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. Draft állapotban az app csak olyan bérlőre telepíthető, ahol te tag vagy - ez a sandbox. 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. - 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.
OAuth-folyamat
Szabványos OAuth 2.0 authorization code folyamat, opcionális PKCE (S256). 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_… # bizalmas app
&code_verifier=… # VAGY PKCE (publikus kliens)
&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
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, vagy rossz client_secret (és nincs PKCE). |
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 az appra származtatott titok a beágyazási token helyi ellenőrzéséhez - nem azonos a client_secret-tel; a client_secret újragenerálása ezt is cseréli, a portál az újat egyszer mutatja. 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 |
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 verifyEmbedToken(token, embed_secret)-tel, 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;
}
});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: csak a saját (tag) bérlőidre telepíthető - sandbox. |
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 generáld újra aclient_secret-et a fejlesztői portálon - ez azembed_secret-et is cseréli (kiszivárgott embed_secret-tel bármely bérlőre hamisítható kontextus-token, ezért a helyi ellenőrzés mellé a távoli/api/apps/embed/verifyellenőrzést is használd). - Publikus kliensnél (nincs szerver) PKCE S256 kötelező.
- 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. - 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. - Tenant-izoláció nálad is: minden tárolt adatot az
install_id/tenant_idalá köss, és eltávolításkor (a kulcs 401-et ad) töröld a bérlő adatait. - Kvóta: a kulcsra a v3 API percenkénti és napi keretei érvényesek (
X-RateLimit-*,Retry-After).
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, verifyEmbedToken / verifyEmbedTokenRemote, és a böngészős createAppBridge (@dynex/app-sdk/browser).
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ó. A README lépésről lépésre végigvisz a localhoston futó teszten (a http://localhost:3999 redirect URI regisztrálható).