Validate my Yuno VTEX headless Web integration following this file.It works the same with any coding agent (Cursor, Codex, Copilot): paste the file as the prompt. It audits the code against the public guide “Headless SDK integration” (docs.y.uno/docs/plugins/vtex/direct-sdk-integration) and reports what is wrong, with file and line. It only reads code and changes nothing unless you ask.
The flow being validated
onPaymentDone.
Statements marked “support cases” or “public guide” are context: they explain a symptom but are never a FAIL
condition on their own. Every FAIL below is something you can see in the code.
Checks
Report each one as PASS / FAIL / N/A with the evidence (file:line). Do not pass a check because the code “looks right”: quote the line that proves it. Start by finding every use of the SDK:grep -rn "sdk-web-vtex\|YunoVTEX\|loadScript\|\.mount(\|unmount(\|domainVTEX\|proxyUrlVTEX" --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=dist --exclude-dir=build .
1. Mount runs only after the connector payload exists
- Trace where
payloadcomes from. Per the public guide it is the payload the Yuno connector returns during the authorization step of this order, and it must exist beforemount. Support cases show VTEX delivering it as theappPayloadof a 428 answer togatewayCallback(or your backend relayingpaymentAppData.payload). - FAIL if
mountcan run before the code has received that payload (page load,useEffectwith no payload guard, a timer), with a hardcoded or sample payload (the README/guide sample"{\"isVTEXCard\":true,...}"), or with a payload cached from a previous order. - Why: the bundle has no payment context of its own. An empty payload throws
payload = ... is not valid; a stale one renders a checkout session that belongs to another order.
2. The payload is passed as the unparsed string
MountProps.payloadis typedstring; the bundle callsJSON.parse(payload)itself.- FAIL if the code passes an object, or re-serializes a modified copy. Passing an object throws
value.trim is not a functionfrom the mount validation, beforeonErrorcan run.
3. elementRoot is an element id that exists and stays
elementRootis passed todocument.getElementById, so it is a bare id:'yuno-sdk-root', not'#yuno-sdk-root'(the npm README shows'#payment-container', which does not work).- The element must be in the DOM when
mountis awaited and must not be replaced while the shopper pays (Reactkeychange, conditional render, SPA route swap). - FAIL on a
#or.prefix, a mount before the element renders, or a container that re-renders mid-payment. - Why: a missing element calls
onError('Element with id <x> not found')and throws; a replaced element leaves the checkout detached and the shopper on a blank area.
4. One mount per order, and unmount on teardown
window.YunoVTEXis a single instance with one React root. A secondmountsilently unmounts the first, whatever element it targets. Check thatmountis called once per order: not in a render body, not in an effect without a run-once guard, not in awindow.addEventListener('message', ...)handler that accepts any message (the Yuno WebView demo does this; filter by atypeof your own bridge contract and ignore the rest).await yunoVTEX.unmount()must run when the page or component is torn down (route change, component destroy, WebView closed). The Payment App does it in the effect cleanup.- FAIL if mount can run twice for one order, or nothing calls
unmount.unmountonly drops the React root:window.YunoVTEX,window.Yunoand both script tags stay, which is expected.
5. domainVTEX is the exact store origin: scheme and host, no trailing slash, no path
- Quote the value (and where it comes from: env var, config, string). It must look like
https://<account>.myvtex.comor the store’s ownhttps://host, lower case is fine (the code also acceptshttp://, which a production store should not use). - FAIL on: a trailing slash (
https://mystore.myvtex.com/), any path (https://store.com/br), a missing scheme (store.myvtex.com), or a value built by concatenation that can end in/. - Why: the bundle lowercases the value and prepends it as is (
yuno-api.ts), so a trailing slash produceshttps://<host>//api/io/_v/yunopartnerbr.yuno/v4/payments/yuno. In a reported incident (a WhatsApp checkout flow), VTEX answered the CORS preflight on that URL with 405 and no CORS headers, the browser blocked the call (Network Error, no status), no payment was created and the orders were cancelled after 24 h. At the time of writing the production bundle does not strip the slash. A value withouthttp(s)://raisesonError('Domain must be https. It is <value>'). - Stores served under a sub-path use the connector’s
Store Base Pathaffiliation field, never a path indomainVTEX. If the page is served from the store origin itself,domainVTEXmay be omitted.
6. proxyUrlVTEX, only if used, reaches the create-payment route
- When set, the bundle POSTs the create payment to this value verbatim: no
domainVTEX, no store base path and no/api/io/...suffix are added (yuno-api.ts). The value is not validated. - Two forms are seen. A bare host, as in the public guide’s own example (
https://proxy.mystore.com): the POST goes to the proxy’s root, so the proxy must forward its root to/api/io/_v/yunopartnerbr.yuno/v4/payments/yunoof the store account. A full path: the POST goes to that path, which the proxy must forward the same way. Both work when the proxy routes them; report which form is used. - FAIL only if the proxy configuration is in this repository and shows that the value does not reach the create-payment route. Otherwise report “needs confirmation” with the value.
- Note in the report:
proxyUrlVTEXis not applied to wallet installments (/installments/apmusesdomainVTEXonly), so a store that reaches VTEX only through a proxy gets no wallet installment selector.
7. One VTEX account host per order, and no direct connector calls
- The host used for the checkout calls (
orderForm,transaction,gatewayCallback) and the host indomainVTEXmust be the same VTEX account, and that account must have the Yuno connector affiliation configured. The affiliation itself cannot be checked in code. FAIL only if the two hosts visibly name different accounts; report “needs confirmation” if they come from separate config keys that can diverge. - FAIL if your code calls
/payments/yuno,/payments/yuno/statusor builds any_v/yunopartnerbr.yunoURL itself. The bundle creates the payment with the one-time token; a second call creates a second payment or fails. - The Payment App installation check does not apply to headless.
8. Only one Yuno SDK on the page
grep -rn "sdk-web\.y\.uno\|sdk-web\.sandbox\.y\.uno\|@yuno-payments/sdk-web['\"]\|window\.Yuno\b\|yuno-script\|webpackChunksdk_web\|sdk-web-vtex\.y\.uno"over source, HTML templates and tag-manager snippets, and ask about third-party scripts (VTEX IO express buttons, wallet snippets).- FAIL if the page also loads sdk-web directly, or adds the
sdk-web-vtexbundle with its own<script>tag next toloadScript(the loader only dedupes its own#yuno-vtex-script). - Why: every sdk-web build shares the
webpackChunksdk_webglobal. A second sdk-web version corrupts the chunk registry andstartCheckoutcrashes withCannot read properties of undefined (reading 'call'): endless spinner. If another sdk-web was loaded with the idyuno-script, the bundle reuses thatwindow.Yunoinstead of its own v1.6. The Payment App works around this withresetForeignYunoSdk(); a headless page has no such reset. In a reported incident, two copies of the bundle on one page lost payment confirmations.
9. loadScript options
version: leave it unset (default'v1', the rolling folder every 1.x release replaces). FAIL on any other value unless Yuno gave it to you in writing;v1.11,v2andv2.0return 404 today.env: leave it unset (default'prod') in production, and in sandbox too: the environment of the payment comes from thepublicApiKeysprefix inside the payload, not from the bundle host. FAIL on'dev'or'staging'in shipped config, and report any'sandbox':sdk-web-vtex.sandbox.y.unohas no DNS record (at the time of writing), soloadScriptrejects withFailed to load script.sdkWebEnvinMountPropsis a different option: it picks the sdk-web host the bundle loads ('sandbox'loadshttps://sdk-web.sandbox.y.uno/v1.6/main.js, which resolves). Leave it unset in production; FAIL on'dev'or'staging'in shipped config.sdkWebVersionhas no effect since bundle 1.5.0 (sdk-web is pinned to v1.6 by the bundle). Report it as N/A noise.- Retry: if a first
loadScriptfailed, the#yuno-vtex-scripttag stays in<head>and a second call waits forever for an event that already fired. FAIL if retry logic callsloadScriptagain without removing that tag (or reloading the page).
10. The outcome comes from onPaymentDone, and errors are handled
onPaymentDone({ success, payments: [{ status, orderId, paymentId }] })fires once per mount: after every checkout session succeeded, or as soon as one fails. Any error also ends the flow:onErrorruns and is followed byonPaymentDone({ success: false })(sdk.main.tsx), possibly before later sessions of a split order run. It is the only signal to continue: confirmation page, native bridge message, reading the VTEX order. FAIL if the result is inferred from the DOM, a URL, a timer oronLoading, or if the code assumesonErrorandonPaymentDoneare exclusive (for example two failure screens, or a retry started fromonErrorthat the followingonPaymentDoneoverrides).successistrueforSUCCEEDED,PENDING,READY_TO_PAYandVERIFIED. FAIL ifsuccess === trueis shown as “approved” without readingpayments[].statusor the VTEX order status (pending payments resolve later through the webhook).onError(message, error)must be passed and must surface something to the shopper or the bridge. FAIL if absent or empty.mountmust be awaited insidetry/catch: validation failures callonErrorand also throw.- Recommended in a WebView: post the result to the native bridge first, before any other UI work, and guard
every
JSON.parseof bridge messages (support cases: a loader that did not awaitmountand posted late lost confirmations). - Report, not FAIL: the bundle calls
window.location.reload()when the shopper cancels a wallet sheet (CANCELED_BY_USER) or taps its close button, and stays silent (noonError, noonLoading(false)) if sdk-web cannot load. A watchdog may offer a retry, but must never decide the payment result.
11. language and deviceFingerprints
languageis required. The bundle has strings foren,es,pt(it also accepts region variants by their first two letters and falls back toen), and forwards the value unchanged to sdk-web, whose type listspt, notpt-BR. Recommendpt,esoren. FAIL if empty or computed toundefined.deviceFingerprintsis an array of{ provider_id, session_id }, withprovider_idas configured in Yuno (RISKIFIED,SIGNIFYD,CYBERSOURCE,CIELO_CYBERSOURCE_FRAUD).session_idshould be the VTEXorderFormIdon every order: the public guide says the connector sends that same id server side, so the provider can only match the device data when the browser uses it too (and split orders require it). FAIL on an object instead of an array, other key names (providerId,sessionId), or asession_idthat is not theorderFormIdfor a configured provider.
12. WebView settings (only if the page runs inside an app)
Check the native code (Kotlin/JavaWebView, Swift WKWebView, react-native-webview, Flutter webview):
- JavaScript enabled, DOM storage enabled, third-party or shared cookies enabled, and a two-way bridge.
- Google Pay on Android: Payment Request API enabled (
WebSettingsCompat.setPaymentRequestEnabled), new windows allowed to open in the same WebView (window.open; a blocked one showsOR_BIBED_15), and the manifest<queries>withorg.chromium.intent.action.PAY,IS_READY_TO_PAYandUPDATE_PAYMENT_DETAILS. - Apple Pay on iOS: the page must be a hosted URL on a domain verified in Apple Developer and registered in Yuno,
never a local file (
file://, bundled assets). - FAIL on each missing setting that applies to the payment methods the store offers.
13. No custom CSS or JS depends on SDK internal names
grep -rn "[yY]uno-" --include=*.{css,scss,less,js,ts,jsx,tsx,html,vue}, excluding your own ids.- The SDK’s class and id names are not a contract. The bundle loads sdk-web v1.6, which uses
yuno-*/Yuno-*; since sdk-web 1.9 they aresdk-payments-*, and the/v1/bundle is replaced without any deploy on your side. The VTEX layer keeps its own names, which are safe:yuno-checkout-root,yuno-form-root,yuno-external-button-root,yuno-vtex-script, theyuno-sdk-vtex-readyevent. - FAIL on any selector or
querySelectoron an sdk-web name. Run the scanner and rename map in the 1.11 upgrade check (node yuno-upgrade-check.mjs <dir>) and attach its output.
Report format
Diagnostics to capture from a failing run
- The VTEX
orderId/orderGroupand the UTC time of the attempt (the bundle’s events are keyed byorderId). - The 428 body from
gatewayCallback(theappPayloadstring), with personal data removed. - The network entry of the create-payment POST and its
OPTIONSpreflight: full URL, status, response headers. A missing request after the form submit, orNetwork Errorwith no status, points to checks 5 to 7. - The browser console: the bundle logs
SET_ERROR,SET_STATUSandSEND_STATUSactions. - What
onPaymentDone/onErrorreceived, the page origin (orfile://path), and for WebViews the device, OS and Android System WebView version.
Versions
Bundlesdk-web-vtex 1.11.1 is served at https://sdk-web-vtex.y.uno/v1/main.js and loads sdk-web v1.6.
Loader @yuno-payments/sdk-web-vtex 0.6.1. Open at the time of writing: trailing-slash stripping for
domainVTEX. Until it ships, check 5 is the only protection.
What this validator does not cover: wallets through the preflight endpoints in native apps (see the Flutter and
React Native validators in this folder), the VTEX Payment App itself, and the connector configuration.
Sources: the public guide Headless SDK integration and
Advanced features, the loader @yuno-payments/sdk-web-vtex 0.6.1, the SDK Web
for VTEX bundle 1.11.1, the Payment App, the Yuno WebView demo and the
1.11 rename map; CDN and DNS probes on 2026-10-29.
Incident details and the 428 / appPayload contract come from support cases and were not re-verified in code.