Skip to main content
Drop this file in the root of your project and ask your coding agent:
Validate my Yuno Web SDK v2 integration following this file
It works as CLAUDE.md, AGENTS.md or a pasted prompt. It only reads code and changes nothing unless you ask. It covers the sdk-web v2 line (“Yuno SDK 2.x”): window.SdkPayments, SdkPayments.initialize(...), then sdk.transaction({...}). The live release is 2.2.0 (2026-10-27; GA 2.0.0), served per full version at https://sdk-web.y.uno/v2.2.0/main.js, documented in the docs.y.uno “SDK” tab (docs/yuno-sdk/*; the “SDKs” tab is 1.x).

How to use

First confirm the repository is on v2. Search source, HTML templates, server templates and package.json:
  • v2: a /v2.<minor>.<patch>/main.js URL, sdk.transaction(...), transaction.start(...), getPaymentMethodsViews(...).
  • v1 (1.x): /v1.11/main.js, window.Yuno, Yuno.initialize, startCheckout, startSeamlessCheckout, mountCheckoutLite, yunoPaymentResult. Stop and use the v1 or migration validator instead.
  • The global name does not decide it: since 1.9.0 v1 also sets window.SdkPayments and fires sdk-payments-ready. v2 has typeof sdk.transaction === 'function'; v1 has startCheckout instead.
Report each check as PASS / FAIL / N/A with file:line, quoting the line that proves it. Severity: (B) blocking, payment fails or the result is wrong; (W) degraded or fragile; (I) information.

The flow being validated

Mode: createPayment with a checkoutSession is manual; without it the SDK creates the payment (managed, session workflow: "SDK_SEAMLESS"). Enrollment is always managed. There is no on() and no close().

Checks

Loading and bootstrap

1. The script is v2 and pinned to a full version (V1, B)
  • Grep sdk-web\.(y\.uno|sandbox\.y\.uno|staging\.y\.uno|prod\.eu\.y\.uno)/[^"' ]*main\.js, sdk-static-bundles-ms/sdk-web/, loadScript( and @yuno-payments/sdk-web.
  • PASS: the URL matches /v2\.\d+\.\d+/main.js, or the SRI URL .../sdk-web/v2.<m>.<p>/main.js.
  • FAIL on /v1.11/, /v2/, /v2.2/, latest; on loadScript() without baseUrl (it loads /v1.11/); on loadScript({ sri: true }) (a hard-coded v1.11.x SRI URL, the 1.11 build pinned by the npm package). For npm, pass baseUrl: 'https://sdk-web.y.uno/v2.x.y'. A v2.0.0 pin is valid: the public docs samples still use it.
  • Why: a v1 bundle gives sdk.transaction is not a function; rolling folders /v2/, /v2.0/, /v2.2/ return 404. There is no rolling v2 folder: bumps are manual. The EU host sdk-web.prod.eu.y.uno may lag behind the latest version (verify at runtime for the pinned version).
2. No v1 globals or events (V2, B)
  • Grep window\.Yuno|\bYuno\.initialize|new Yuno|yuno-sdk-ready|YunoSDK. FAIL on any hit in v2 code.
  • Why: v2 sets only window.SdkPayments: Yuno is not defined, or yuno-sdk-ready never fires.
3. Ready handling covers “script already loaded” (V3, B)
  • Grep sdk-payments-ready and SdkPayments. PASS: if (window.SdkPayments) start(); else window.addEventListener('sdk-payments-ready', start), or an awaited loadScript / own load promise.
  • FAIL on a bare addEventListener (the event fires once, at load), or initialize before the script ran.
  • Why: checkout never starts on pages where the script loaded first; SdkPayments is not defined.
4. initialize is awaited, client-side, with the public key only (V4, B)
  • Grep SdkPayments\.initialize\(; check await or .then; check it is not in server code (getServerSideProps, server components, route handlers); check no private secret key in front-end code.
  • FAIL if the result is used without await, if it runs on the server, or if a private key is bundled.
  • Why: it returns Promise<SdkPayments>: sdk.transaction is not a function; on a server, window is not defined.
5. initialize arguments are stable and one instance is reused (V5, W)
  • Look for initialize in render functions, in effects without a module-level cache, or with an argument that changes per call (applicationSession per checkout, Date.now(), random values).
  • FAIL on more than one distinct argument set per page lifetime, or one initialize per transaction.
  • Why: same arguments return the same instance and log SdkPayments.initialize() was called again with the same arguments. The existing instance was returned; the SDK was not re-initialized. (benign). Different arguments tear the old instance down and the mounted form disappears. transaction(), transactionStatus(), start(), submit() and secureFields() on it throw This SdkPayments instance is no longer active: a later initialize() replaced it. Use the instance returned by the most recent initialize(). A superseded antifraud() resolves { status: 'error' } and isPaymentMethodAvailable() resolves false, silently.

Transaction config and result

6. The transaction config is valid (V6, B)
  • For each .transaction( call. PASS: exactly one of checkoutSession / customerSession; countryCode; onStatus is a function; createPayment, if present, is a function. TypeScript repos must also pass language and showStatusScreen (required by the type although the docs call them optional).
  • Why, verbatim throws: Provide either `checkoutSession` or `customerSession`, not both., A `checkoutSession` or `customerSession` is required., `countryCode` is required., `createPayment` must be a function when provided. Without onStatus the console warns SdkPayments transaction() was created without an `onStatus` function. The payment result will not be delivered to your integration. Pass `onStatus` in the transaction config to receive the result.
  • Unknown and v1-only keys pass through with no error. Most are never read, but three still act: onLoading is still called (check 19); paymentMethodSelected / yunoPaymentMethodSelected fire on every list selection, so next to onPaymentSelected each selection calls the merchant twice; yunoCreatePayment is merged into createPayment. Flag every key not in the v2 type and every v1 callback name.
7. onStatus handles the full status contract (V7, B)
  • Read the handler. PASS: reads result.status, result.substatus, result.message; handles INTERNAL_ERROR (branch on message.code; substatus is null for SDK errors) and USER_CANCEL; treats INTERNAL_ERROR with message.code FORM_INVALID or COLLECT_FAILED, and in manual mode a retryable DECLINED / ERROR, as non-final.
  • FAIL on v1 values ('FAIL', 'REJECT', 'PROCESSING', 'READY'), two positional arguments, branching on substatus for SDK error codes, a redirect or teardown on every call, or the order marked paid client-side on SUCCEEDED.
  • Why: branches never match, a first decline ends the retry loop, or orders are fulfilled without payment. With showStatusScreen: true (default) onStatus fires when the shopper dismisses the SDK result screen.
8. Mode matches the checkout-session workflow (V8, B)
  • Front end: createPayment present? Backend: grep checkout/sessions and workflow. PASS: createPayment with a default-workflow session; no createPayment with workflow: "SDK_SEAMLESS".
  • Why: a mismatch ends in onStatus INTERNAL_ERROR; the backend rejects the SDK’s payment creation.
9. createPayment follows the one-argument Promise contract (V9, B)
  • Grep createPayment\s*[:=(]. PASS: one parameter token; sends token.token and token.checkout_session to the merchant backend; returns a Promise that settles on every path; rejects on backend failure.
  • FAIL on (ott, info, cs); the argument used as the token string; the config checkoutSession used instead of token.checkout_session; catch {} that resolves; a path that never settles; a continuePayment() call.
  • Why: an object sent as the token gives a Create Payment 4xx; the session rotates after a retryable decline, so the config one is stale; no timeout exists, so a hung promise leaves the form loading forever. A rejection ends in INTERNAL_ERROR with message.code MERCHANT_CREATE_PAYMENT_FAILED, form still mounted.
  • Verify at runtime: the mode is chosen from createPayment only, so a config with only the v1 name yunoCreatePayment (merged into state with the v1 three-argument signature) may run managed, not manual.
10. autoContinuePayment: false is deliberate (V10, W)
  • Grep autoContinuePayment. PASS: absent, or false with status owned by webhook and a new session for any later start().
  • Why: with false the SDK runs no status query, no 3DS and no onStatus after createPayment resolves, and reusing the session throws the “already finished” error (check 18). Classic PayPal and PayPal enrollment ignore false; paypalBraintree honours it.

Mounting and paying

11. elementSelector resolves at call time (V11, B)
  • Check elementSelector in the transaction config, in each view .mount({ elementSelector }) and in transactionStatus. The node must exist and be connected when start() / mount() runs.
  • Only the transaction config (the form start() mounts, and enrollment) accepts a string or an HTMLElement. View mounts and transactionStatus take a string only.
  • FAIL on an HTMLElement passed to a view mount or transactionStatus; a node not yet rendered, conditionally rendered or detached; no selector for a form payment.
  • Why, verbatim: elementSelector=[object HTMLDivElement] is not valid (element on the list mount), elementSelector=undefined is not valid (form start() with no selector), elementSelector "<sel>" was not found in the document., elementSelector is detached from the document., Element <sel> doesn't exist in the DOM with onStatus INTERNAL_ERROR and message.code ELEMENT_NOT_FOUND.
12. start() is awaited with error handling and is not payment completion (V12, B)
  • Grep \.start\(. PASS: inside try/catch or .catch; paymentMethod.type always set; result read only in onStatus. A form start() resolves when the form is on the page, not when paid.
  • FAIL on no catch; .start() without paymentMethod on a payment; “success” UI after await start(); a second start() on the same mounted form instead of submit().
  • Why, verbatim rejections: `paymentMethod.type` is required for a payment transaction., `start()` has already been called on this transaction. Call `submit()` to submit the form it mounted., A payment is in progress on this transaction. Wait for it to settle before starting another payment method. A “paid” screen before the shopper pays is the merchant-visible symptom.
13. The merchant’s own Pay button is wired correctly (V13, W)
  • Check showPayButton and the click handler. Form: showPayButton:false, await start({ paymentMethod }), then transaction.submit(). List: the button calls start({ paymentMethod: selected }) with the stored onPaymentSelected value; selectionSource === 'auto' (SDK preselection) does not enable it.
  • FAIL on submit() before start() resolved, or on a list, wallet or enrollment; Pay enabled on auto; the button not disabled while a payment runs; a v1 selection callback kept next to onPaymentSelected.
  • Why, verbatim: `submit()` needs a form mounted by `start({ paymentMethod })`. Call `start()` first and wait for it to resolve., `submit()` does not pay from the payment method list. Call `start({ paymentMethod })` with the selected method., `submit()` cannot pay a wallet. The shopper pays by tapping the wallet button., `submit()` found no form to submit: the form `start()` mounted is no longer on the page. Call `start({ paymentMethod })` again. A submit() while start() is still mounting does not throw: like an invalid form, it delivers a non-final INTERNAL_ERROR with message.code FORM_INVALID.
14. Views are requested and mounted correctly (V14, W)
  • Grep getPaymentMethodsViews and \.mount\(. PASS: types only from paymentMethodList, applePay, googlePay, samsungPay, paypal, paypalBraintree, revolutPay; only requested views are built, so a view used without being in types is guarded (?. or a check); each mount awaited or .catch-ed (typed void, returns a Promise); wallets pay on tap, no start().
  • FAIL on v1 names (GOOGLE_PAY), unguarded views.x.mount, no catch, views on an enrollment transaction.
  • Why: Cannot read properties of undefined (reading 'mount'); unhandled rejection There are not valid payment methods; thrown `getPaymentMethodsViews` is not available for enrollment transactions. Use `secureFields()` to collect card data. Wallet views stay mounted after transaction.unmount(); there is no per-view unmount. Verify at runtime: a list plus wallets from one transaction.
15. Headless data is the v2 envelope and is not logged (V15, B)
  • Grep data:\s*\{\s*card. PASS: data.card.{holderName, number, securityCode, expirationMonth, expirationYear} in camelCase; one start() per transaction; card data never logged or stored. A saved card with a fresh CVV is vaultedToken plus data.card.securityCode.
  • FAIL on snake_case (holder_name, security_code, payment_method.card.detail), a second headless start() (single-shot), or data while an inline list form is mounted.
  • Why: tokenization fails or fields arrive empty; `start()` has already been called on this transaction.
16. Secure Fields use the v2 surface (V16, B)
  • Grep secureFields(, \.create\(, \.render\(, \.mount\(, generateToken, isFieldComplete.
  • PASS: const fields = await transaction.secureFields({ installmentEnable? }); fields.create({ name, options }).mount('#sel'); Pay gated on !error && isDirty per field; payment by transaction.start({ paymentMethod: { type: 'CARD', data: { card: { holderName } } } }).
  • FAIL on .render( (v1), generateToken* or continuePayment, isFieldComplete as the gate, PCI keys in data.card.
  • Why: render is not a function; a throw that starts Secure fields collect the card data inside the mounted iframes and continues remove <keys> from `paymentMethod.data.card`. Only non-PCI metadata (holderName, type, installment, customer, vaultOnSuccess) may be passed here.; a Pay button disabled for valid short PANs.
17. The enrollment flow is correct (V17, B)
  • Grep customerSession. PASS: no checkoutSession, createPayment, showPayButton or autoContinuePayment with it; start() with no arguments for the UI, start({ paymentMethod: { type, data } }) for headless; success is ENROLLED (UI) or CREATED (headless and Secure Fields).
  • FAIL on reading result.vaultedToken (never delivered; fetch it server-side), submit() or views.
  • Why: an undefined vaulted token is saved; thrown `submit()` is not available for enrollment transactions. or `paymentMethod.type` is required for a headless enrollment.

Re-render and framework lifecycle

18. One instance, one transaction per mount, cleanup on teardown, StrictMode-safe (V18, B) No v2 doc covers frameworks; these rules follow the code. Same initialize arguments return the same instance. A second start() of a mounted form throws on purpose (a double-run React effect once created a payment). A new Transaction resets shared instance state, so with two live transactions the last mount wins. unmount() is async, delivers USER_CANCEL for an unfinished attempt, leaves wallets. Settled sessions are final.
  • All frameworks: initialize behind a module-level promise (or always the same arguments); one transaction() per mounted checkout, created where the container already exists; one live card surface per page; a new checkout session means a new transaction. FAIL on initialize or transaction() in a render body, or two transactions mounted at once.
  • React: the transaction is held in a useRef and created in the useEffect that owns the container; start() runs once per transaction (guard with the ref); cleanup calls await transaction.unmount() and clears the ref; callbacks (onStatus, createPayment) read the latest props through a ref so a re-render does not rebuild the transaction. Under StrictMode the effect runs, cleans up and runs again: PASS only if the second run builds a fresh transaction after the cleanup, never a second start() on the first one. FAIL on start() in an effect without cleanup, or effect dependencies that change every render.
  • Next.js: SDK code runs only in the browser: a 'use client' component calling initialize inside an effect, or a component loaded with dynamic(..., { ssr: false }). FAIL on SdkPayments in a server component, getServerSideProps, a route handler, or module top level of a server-rendered file.
  • Vue and Angular (guidance): create the transaction in onMounted / ngAfterViewInit, unmount in onBeforeUnmount / ngOnDestroy, and keep it out of reactive state (shallowRef or a plain variable).
  • SPA teardown: leaving the checkout route calls await transaction.unmount(); the USER_CANCEL it delivers is not shown as an error or treated as abandonment; wallet containers are removed with the route.
  • Why: duplicated or vanishing forms, double payment attempts, `start()` has already been called on this transaction., The payment on this checkout session already finished. Create a new checkout session and transaction to pay again., or an error banner on every route change.
  • Verify at runtime: transaction.unmount() reads the shared root after its awaits, and no test covers a StrictMode remount. Report StrictMode findings as “unverified against a real remount”.

Options and edges

19. showLoading is a function and the merchant owns the loader (V19, W)
  • Grep showLoading and onLoading. PASS: showLoading: (isLoading) => void that shows and hides the merchant’s loader; a registered handler replaces the SDK overlay.
  • FAIL on showLoading: true|false, on onLoading (v1 name, still called by v2 with the v1 { isLoading, type } payload, next to the SDK overlay), or on a handler with no UI behind it.
  • Why: showLoading: true throws a TypeError on the first loading change, naming a minified variable, not showLoading. showLoading: false is treated as no handler: the SDK overlay comes back silently.
20. The status screen option uses the v2 name (V20, W)
  • Grep showPaymentStatus|showStatusScreen|transactionStatus. PASS: showStatusScreen; transactionStatus receives elementSelector (required). FAIL on showPaymentStatus (ignored, the result screen still shows).
  • Why: duplicated result UI, and onStatus delayed until the shopper dismisses the SDK screen.
21. Card options use v2 names (V21, W)
  • Grep card:\s*\{. PASS: saveCardEnabled, styles, type, vaultOnSuccess, texts, documentEnable, isCreditCardProcessingOnly, fonts. FAIL on cardSaveEnable or requiredFieldsOrderedEnabled.
  • Why: cardSaveEnable is silently ignored and the save-card checkbox disappears. Report externalButtons.applePay.topLevelDomain as ignored (I): v2 detects the Apple Pay domain itself.
22. Antifraud reads the result union (V22, W)
  • Grep \.antifraud\(. PASS: checks result.status === 'success' before using result.deviceFingerprints. FAIL on no check, or on relying on a rejection (it never rejects). Why: undefined fingerprints are sent.
23. CSS overrides, white-label, CSP and host assumptions (V23, W)
  • Always run this part: grep every stylesheet and component style (*.css, *.scss, *.less, styled blocks in *.ts, *.tsx, *.vue, *.svelte) for yuno- and Yuno-. Then check apiUrl, assetUrl, CSP headers and proxy rules, when present.
  • SDK form classes are sdk-payments-* on v1.11 and v2; the few yuno-* strings left on v1.11 are ids, event names, chunk names and the Yuno-Inter font fallback (the rename map lists them as unchanged), not element classes. v2 itself renders yuno-container-spinner-content and yuno-container-spinner-ring (the spinner of every mount), plus <style id="yuno-container-spinner-style"> and data-yuno-placeholder. Selectors on those are legitimate.
  • PASS: overrides target sdk-payments-* or the two spinner classes; a proxy forwards /sdk-static-bundles-ms/*; apiUrl and assetUrl set together; the v2 host is in CSP script-src; on a white-label page an HTMLElement selector has its own id (the spinner names always stay in the DOM).
  • FAIL on any other yuno- / Yuno- selector (it matches nothing the SDK renders), a missing CSP host, or an id-less element on a white-label page.
  • Why: overrides stop applying, the script is blocked, or the SDK assigns yuno-view-<random> as the id. Verify at runtime: only three class names were compared with v1, so check each override visually.

Server side

24. Fulfilment is confirmed on the server, and TypeScript types are v2 (V24, B)
  • Find the webhook handler or GET /v1/payments/... check that drives fulfilment, and any .d.ts for window.SdkPayments. PASS: fulfilment from webhook or GET payment; a local .d.ts copied from the v2 API report.
  • FAIL on fulfilment from onStatus (orders paid without a payment), or @yuno-payments/sdk-web-types for v2 calls (v1 shape, no transaction: compile errors or silent any; this part alone is (W)).

Report format

Finish with the failures ordered by impact, (B) then (W) then (I), each with a one-line fix.

Diagnostics to capture

  1. The served script URL, typeof sdk.transaction, the checkout session id, its workflow, the UTC time.
  2. Every onStatus payload received (status, substatus, message.code, message.reason).
  3. Console warnings and errors from the SDK, quoted verbatim (initialize, superseded, onStatus missing).
  4. The Create Payment request and response (no card data), framework, StrictMode, route change or re-render.
Sources: the Web SDK 2.2.0 release and the v2 source code, the v2 API report and the public Yuno docs (the “SDK” tab and its changelog); CDN probes on 2026-10-29. Research date 2026-10-29.