> ## Documentation Index
> Fetch the complete documentation index at: https://docs.y.uno/llms.txt
> Use this file to discover all available pages before exploring further.

# Validator: Web SDK v2

> A read-only checklist for AI coding agents that audits a Yuno Web SDK 2.x integration built on SdkPayments.initialize and sdk.transaction.

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`:

```
grep -rn "SdkPayments\|sdk-payments-ready\|window\.Yuno\|yuno-sdk-ready\|YunoSDK\|sdk-web\.[a-z.]*y\.uno\|sdk-static-bundles-ms/sdk-web\|loadScript(\|@yuno-payments/sdk-web\|\.transaction(\|startCheckout\|startSeamlessCheckout\|mountCheckout" --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=build .
```

* **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

```
<script src="https://sdk-web.y.uno/v2.<m>.<p>/main.js">   sets window.SdkPayments, fires 'sdk-payments-ready' once
await SdkPayments.initialize(publicApiKey, applicationSession?, options?)   // once per page, stable arguments
  -> sdk.transaction(config)               // sync; validates config; throws on invalid config
       -> transaction.start({ paymentMethod: { type, vaultedToken?, data? } })   // form, or headless with data
       -> transaction.getPaymentMethodsViews({ types, onPaymentSelected }) -> views.<x>.mount({ elementSelector })
       -> transaction.submit()             // only for a form that start() mounted, with showPayButton:false
       -> await transaction.secureFields(options?) -> .create({ name, options }).mount(selector)
       -> await transaction.unmount()
  -> sdk.transactionStatus({ checkoutSession, elementSelector, ... })
  -> sdk.antifraud({ checkoutSession, countryCode, ... })
  -> onStatus({ status, substatus, message })   // client signal only; confirm by webhook or GET payment
```

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

```
CHECK  1  Script v2, full version     PASS  public/index.html:14 https://sdk-web.y.uno/v2.2.0/main.js
CHECK  8  Mode vs workflow            FAIL  api/session.ts:31 no workflow, front end has no createPayment
                                            (managed on SDK_CHECKOUT -> INTERNAL_ERROR)
SDK: v2.2.0 (script tag). Framework: Next.js 14, React StrictMode on.
```

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.