Validate my Yuno Web SDK v2 integration following this fileIt 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 andpackage.json:
- v2: a
/v2.<minor>.<patch>/main.jsURL,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.SdkPaymentsand firessdk-payments-ready. v2 hastypeof sdk.transaction === 'function'; v1 hasstartCheckoutinstead.
The flow being validated
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; onloadScript()withoutbaseUrl(it loads/v1.11/); onloadScript({ sri: true })(a hard-coded v1.11.x SRI URL, the 1.11 build pinned by the npm package). For npm, passbaseUrl: 'https://sdk-web.y.uno/v2.x.y'. Av2.0.0pin 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 hostsdk-web.prod.eu.y.unomay lag behind the latest version (verify at runtime for the pinned version).
- 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, oryuno-sdk-readynever fires.
- Grep
sdk-payments-readyandSdkPayments. PASS:if (window.SdkPayments) start(); else window.addEventListener('sdk-payments-ready', start), or an awaitedloadScript/ own load promise. - FAIL on a bare
addEventListener(the event fires once, at load), orinitializebefore the script ran. - Why: checkout never starts on pages where the script loaded first;
SdkPayments is not defined.
initialize is awaited, client-side, with the public key only (V4, B)
- Grep
SdkPayments\.initialize\(; checkawaitor.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.
initialize arguments are stable and one instance is reused (V5, W)
- Look for
initializein render functions, in effects without a module-level cache, or with an argument that changes per call (applicationSessionper checkout,Date.now(), random values). - FAIL on more than one distinct argument set per page lifetime, or one
initializeper 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()andsecureFields()on it throwThis SdkPayments instance is no longer active: a later initialize() replaced it. Use the instance returned by the most recent initialize().A supersededantifraud()resolves{ status: 'error' }andisPaymentMethodAvailable()resolvesfalse, silently.
Transaction config and result
6. The transaction config is valid (V6, B)- For each
.transaction(call. PASS: exactly one ofcheckoutSession/customerSession;countryCode;onStatusis a function;createPayment, if present, is a function. TypeScript repos must also passlanguageandshowStatusScreen(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.WithoutonStatusthe console warnsSdkPayments 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:
onLoadingis still called (check 19);paymentMethodSelected/yunoPaymentMethodSelectedfire on every list selection, so next toonPaymentSelectedeach selection calls the merchant twice;yunoCreatePaymentis merged intocreatePayment. Flag every key not in the v2 type and every v1 callback name.
onStatus handles the full status contract (V7, B)
- Read the handler. PASS: reads
result.status,result.substatus,result.message; handlesINTERNAL_ERROR(branch onmessage.code;substatusisnullfor SDK errors) andUSER_CANCEL; treatsINTERNAL_ERRORwithmessage.codeFORM_INVALIDorCOLLECT_FAILED, and in manual mode a retryableDECLINED/ERROR, as non-final. - FAIL on v1 values (
'FAIL','REJECT','PROCESSING','READY'), two positional arguments, branching onsubstatusfor SDK error codes, a redirect or teardown on every call, or the order marked paid client-side onSUCCEEDED. - Why: branches never match, a first decline ends the retry loop, or orders are fulfilled without payment.
With
showStatusScreen: true(default)onStatusfires when the shopper dismisses the SDK result screen.
workflow (V8, B)
- Front end:
createPaymentpresent? Backend: grepcheckout/sessionsandworkflow. PASS:createPaymentwith a default-workflow session; nocreatePaymentwithworkflow: "SDK_SEAMLESS". - Why: a mismatch ends in
onStatusINTERNAL_ERROR; the backend rejects the SDK’s payment creation.
createPayment follows the one-argument Promise contract (V9, B)
- Grep
createPayment\s*[:=(]. PASS: one parametertoken; sendstoken.tokenandtoken.checkout_sessionto 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 configcheckoutSessionused instead oftoken.checkout_session;catch {}that resolves; a path that never settles; acontinuePayment()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_ERRORwithmessage.codeMERCHANT_CREATE_PAYMENT_FAILED, form still mounted. - Verify at runtime: the mode is chosen from
createPaymentonly, so a config with only the v1 nameyunoCreatePayment(merged into state with the v1 three-argument signature) may run managed, not manual.
autoContinuePayment: false is deliberate (V10, W)
- Grep
autoContinuePayment. PASS: absent, orfalsewith status owned by webhook and a new session for any laterstart(). - Why: with
falsethe SDK runs no status query, no 3DS and noonStatusaftercreatePaymentresolves, and reusing the session throws the “already finished” error (check 18). Classic PayPal and PayPal enrollment ignorefalse;paypalBraintreehonours it.
Mounting and paying
11.elementSelector resolves at call time (V11, B)
- Check
elementSelectorin the transaction config, in each view.mount({ elementSelector })and intransactionStatus. The node must exist and be connected whenstart()/mount()runs. - Only the transaction config (the form
start()mounts, and enrollment) accepts a string or anHTMLElement. View mounts andtransactionStatustake a string only. - FAIL on an
HTMLElementpassed to a view mount ortransactionStatus; 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(formstart()with no selector),elementSelector "<sel>" was not found in the document.,elementSelector is detached from the document.,Element <sel> doesn't exist in the DOMwithonStatusINTERNAL_ERRORandmessage.codeELEMENT_NOT_FOUND.
start() is awaited with error handling and is not payment completion (V12, B)
- Grep
\.start\(. PASS: inside try/catch or.catch;paymentMethod.typealways set; result read only inonStatus. A formstart()resolves when the form is on the page, not when paid. - FAIL on no catch;
.start()withoutpaymentMethodon a payment; “success” UI afterawait start(); a secondstart()on the same mounted form instead ofsubmit(). - 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.
- Check
showPayButtonand the click handler. Form:showPayButton:false,await start({ paymentMethod }), thentransaction.submit(). List: the button callsstart({ paymentMethod: selected })with the storedonPaymentSelectedvalue;selectionSource === 'auto'(SDK preselection) does not enable it. - FAIL on
submit()beforestart()resolved, or on a list, wallet or enrollment; Pay enabled onauto; the button not disabled while a payment runs; a v1 selection callback kept next toonPaymentSelected. - 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.Asubmit()whilestart()is still mounting does not throw: like an invalid form, it delivers a non-finalINTERNAL_ERRORwithmessage.codeFORM_INVALID.
- Grep
getPaymentMethodsViewsand\.mount\(. PASS:typesonly frompaymentMethodList,applePay,googlePay,samsungPay,paypal,paypalBraintree,revolutPay; only requested views are built, so a view used without being intypesis guarded (?.or a check); eachmountawaited or.catch-ed (typedvoid, returns a Promise); wallets pay on tap, nostart(). - FAIL on v1 names (
GOOGLE_PAY), unguardedviews.x.mount, no catch, views on an enrollment transaction. - Why:
Cannot read properties of undefined (reading 'mount'); unhandled rejectionThere are not valid payment methods; thrown`getPaymentMethodsViews` is not available for enrollment transactions. Use `secureFields()` to collect card data.Wallet views stay mounted aftertransaction.unmount(); there is no per-view unmount. Verify at runtime: a list plus wallets from one transaction.
- Grep
data:\s*\{\s*card. PASS:data.card.{holderName, number, securityCode, expirationMonth, expirationYear}in camelCase; onestart()per transaction; card data never logged or stored. A saved card with a fresh CVV isvaultedTokenplusdata.card.securityCode. - FAIL on snake_case (
holder_name,security_code,payment_method.card.detail), a second headlessstart()(single-shot), ordatawhile an inline list form is mounted. - Why: tokenization fails or fields arrive empty;
`start()` has already been called on this transaction.
- Grep
secureFields(,\.create\(,\.render\(,\.mount\(,generateToken,isFieldComplete. - PASS:
const fields = await transaction.secureFields({ installmentEnable? });fields.create({ name, options }).mount('#sel'); Pay gated on!error && isDirtyper field; payment bytransaction.start({ paymentMethod: { type: 'CARD', data: { card: { holderName } } } }). - FAIL on
.render((v1),generateToken*orcontinuePayment,isFieldCompleteas the gate, PCI keys indata.card. - Why:
render is not a function; a throw that startsSecure fields collect the card data inside the mounted iframesand continuesremove <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.
- Grep
customerSession. PASS: nocheckoutSession,createPayment,showPayButtonorautoContinuePaymentwith it;start()with no arguments for the UI,start({ paymentMethod: { type, data } })for headless; success isENROLLED(UI) orCREATED(headless and Secure Fields). - FAIL on reading
result.vaultedToken(never delivered; fetch it server-side),submit()or views. - Why: an
undefinedvaulted 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. Sameinitialize 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:
initializebehind a module-level promise (or always the same arguments); onetransaction()per mounted checkout, created where the container already exists; one live card surface per page; a new checkout session means a new transaction. FAIL oninitializeortransaction()in a render body, or two transactions mounted at once. - React: the transaction is held in a
useRefand created in theuseEffectthat owns the container;start()runs once per transaction (guard with the ref); cleanup callsawait 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 secondstart()on the first one. FAIL onstart()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 callinginitializeinside an effect, or a component loaded withdynamic(..., { ssr: false }). FAIL onSdkPaymentsin 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 inonBeforeUnmount/ngOnDestroy, and keep it out of reactive state (shallowRefor a plain variable). - SPA teardown: leaving the checkout route calls
await transaction.unmount(); theUSER_CANCELit 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
showLoadingandonLoading. PASS:showLoading: (isLoading) => voidthat shows and hides the merchant’s loader; a registered handler replaces the SDK overlay. - FAIL on
showLoading: true|false, ononLoading(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: truethrows a TypeError on the first loading change, naming a minified variable, notshowLoading.showLoading: falseis treated as no handler: the SDK overlay comes back silently.
- Grep
showPaymentStatus|showStatusScreen|transactionStatus. PASS:showStatusScreen;transactionStatusreceiveselementSelector(required). FAIL onshowPaymentStatus(ignored, the result screen still shows). - Why: duplicated result UI, and
onStatusdelayed until the shopper dismisses the SDK screen.
- Grep
card:\s*\{. PASS:saveCardEnabled,styles,type,vaultOnSuccess,texts,documentEnable,isCreditCardProcessingOnly,fonts. FAIL oncardSaveEnableorrequiredFieldsOrderedEnabled. - Why:
cardSaveEnableis silently ignored and the save-card checkbox disappears. ReportexternalButtons.applePay.topLevelDomainas ignored (I): v2 detects the Apple Pay domain itself.
- Grep
\.antifraud\(. PASS: checksresult.status === 'success'before usingresult.deviceFingerprints. FAIL on no check, or on relying on a rejection (it never rejects). Why:undefinedfingerprints are sent.
- Always run this part: grep every stylesheet and component style (
*.css,*.scss,*.less, styled blocks in*.ts,*.tsx,*.vue,*.svelte) foryuno-andYuno-. Then checkapiUrl,assetUrl, CSP headers and proxy rules, when present. - SDK form classes are
sdk-payments-*on v1.11 and v2; the fewyuno-*strings left on v1.11 are ids, event names, chunk names and theYuno-Interfont fallback (the rename map lists them as unchanged), not element classes. v2 itself rendersyuno-container-spinner-contentandyuno-container-spinner-ring(the spinner of every mount), plus<style id="yuno-container-spinner-style">anddata-yuno-placeholder. Selectors on those are legitimate. - PASS: overrides target
sdk-payments-*or the two spinner classes; a proxy forwards/sdk-static-bundles-ms/*;apiUrlandassetUrlset together; the v2 host is in CSPscript-src; on a white-label page anHTMLElementselector has its ownid(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.tsforwindow.SdkPayments. PASS: fulfilment from webhook orGET payment; a local.d.tscopied from the v2 API report. - FAIL on fulfilment from
onStatus(orders paid without a payment), or@yuno-payments/sdk-web-typesfor v2 calls (v1 shape, notransaction: compile errors or silentany; this part alone is (W)).
Report format
Diagnostics to capture
- The served script URL,
typeof sdk.transaction, the checkout session id, itsworkflow, the UTC time. - Every
onStatuspayload received (status,substatus,message.code,message.reason). - Console warnings and errors from the SDK, quoted verbatim (initialize, superseded, onStatus missing).
- The Create Payment request and response (no card data), framework, StrictMode, route change or re-render.