Validate my Yuno Web SDK headless / Secure Fields integration following this file.It works as a
CLAUDE.md, an AGENTS.md or a pasted prompt, with any coding agent. It audits the Yuno Web SDK v1
(line 1.11) used without Yuno’s checkout UI: headless payment and enrollment, the two-card split, and Secure Fields
(pan, expiration, cvv iframes) with lifecycle, fraud preload and CSP. It reports issues with file and line,
reads code only and changes nothing unless you ask. UI flows belong to the sibling web-sdk-v1-checkout-flows.md.
How to use
First detect which of the five contracts the repository uses, then run the cross-cutting checks (1 to 4) plus the groups that apply, and mark the others N/A:grep -rnE "apiClientPayment|apiClientEnroll|generateToken|generateTokenWithInformation|generateHeadlessToken|getThreeDSecureChallenge|getContinuePaymentAction|continueEnrollment|executeFraudCheck|\.secureFields\(|unmountSync|generateVaultedToken|generateVaultedTokenWithInformation|payment_method: *\[|\.initialize\(|loadScript|sdk-web\.y\.uno" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git --exclude-dir=build .
Run the sibling validator for any
startCheckout or mount* call. Server code (sessions, Create Payment) is in
scope when it is in the repository. Do not pass a check because the code “looks right”: quote the line.
The flows being validated
One instance per page:const yuno = await Yuno.initialize(PUBLIC_API_KEY). Sessions come from your server.
Checks
Severity: (B) blocks or silently corrupts a payment, (H) likely incident, (M) degraded behaviour or security hygiene, (L) hygiene. Report PASS / FAIL / N/A for each. Validation errors are thrown as<key>=<value> is not valid; quote them exactly when you explain a FAIL.
Cross-cutting
1. One Yuno.initialize per page, awaited, instance kept (H1, H)
- Look at
Yuno.initialize(andloadScript(in component bodies, unguarded effects, click handlers, per-attempt helpers, and withoutawait. - FAIL on: more than one call path per page load; no
await; the instance not kept in module scope, context or ref. - Why: without
await,yunois a Promise and the first call throwsTypeError: yuno.secureFields is not a function(orapiClientPayment); eachinitializereplaces the global API client, so host-side calls (fraud, 3DS, headless) of earlier clients switch to the newest key. Secure Fields card calls keep their own client.
2. Only the public API key reaches the browser (H2, B)
- Grep client code for
private-secret-key,PRIVATE_SECRET_KEY, secrets inNEXT_PUBLIC_*,VITE_*,REACT_APP_*, or a secret passed toYuno.initialize(. - FAIL on: any match in browser-reachable code.
- Why: the secret authorizes Create Payment and refunds; anyone who reads the bundle can move money.
3. CSP and network allow the Yuno hosts (H3, H)
- Check
Content-Security-Policy(meta, server,next.config.js, nginx) and ad-block or WAF allowlists. - FAIL on:
frame-src(ordefault-src) withouthttps://sdk-web-card.<env>.y.unoandhttps://sdk-3ds.<env>.y.uno;connect-srcwithout the API host (api.y.uno,api-sandbox.y.uno, EUapi.eu.y.uno) andwss://<env>.y.uno;script-srcwithouthttps://sdk-web.y.unoand its lazy-chunk host; rules that block*.y.uno. Fraud providers load their own scripts and frames, so the hosts of the providers on the account are needed too. - Why: if the mediator cannot connect, a field
render()never settles (an SDK defect, sorender().catchshows nothing; add your own UI timeout) whilegenerateTokenrejects after 30 s withbridge: child did not connect within 30000ms; a blocked 3DS frame looks like a hung challenge. Verify the hang at runtime. - Yuno’s docs publish no CSP allowlist; this list comes from the SDK code. EU, MENA, white-label: verify at runtime.
4. White-label apiUrl / assetUrl only behind a complete proxy (H4, M)
- Look for
apiUrl:orassetUrl:in theYuno.initialize(options. - FAIL on: set without a proxy that forwards every Yuno path, including
/v<card-version>/pages/*and the 3DS host;apiUrlpointing at a host that is not the merchant’s own proxy. - Why: card iframes served from
*.y.unoreject a foreignapiUrl(logged asAPI_URL_REJECTED) and use Yuno’s API anyway; an incomplete proxy leaves fields or icons blank.
Headless payment
5. apiClientPayment is awaited and only real methods are called (H5, B)
- Look at
.apiClientPayment(withoutawait;apiClientEnrollment,getPaymentMethods,api.continuePayment,api.generateHeadlessTokenwithoutaccount_id. - FAIL on: the client used without await, or a method that does not exist. The headless client has exactly
generateToken,getThreeDSecureChallenge,getContinuePaymentActionandgenerateHeadlessToken. - Why:
api.generateTokenisundefinedon a Promise and throwsTypeError: ... is not a function.
6. country_code is snake_case, uppercase ISO, from the enum (H6, B)
- Look at the argument keys of
apiClientPayment({andapiClientEnroll({. - FAIL on:
countryCode(camelCase) here; lowercase'co'; a country name; a value outsideCountryCode. - Why: throws
country_code=undefined is not validorcountry_code=co is not valid; no token is possible.
7. Fraud collection starts when the form renders, not at the Pay click (H7, H)
- Look for
apiClientPayment(andgenerateToken(in the same submit handler, with no earlierexecuteFraudCheck(and no earlierapiClientPayment({ checkout_session }). - FAIL on: neither
executeFraudCheck({ checkoutSession })norapiClientPayment({ checkout_session })runs before the submit handler. A client withoutcheckout_sessionafterexecuteFraudCheckis correct (cached result). - Why: the token waits for a running collection up to the account fraud timeout (3 s by default); when collection starts at the click and takes longer, the token has no device fingerprint and the provider can decline.
8. executeFraudCheck uses camelCase arguments (H8, H)
- Look at
executeFraudCheck({. FAIL oncheckout_session/country_codekeys (as the docs show). - Why: the promise rejects with
checkoutSession=undefined is not valid, so the preload the merchant thinks is running never starts.countryCodeis optional and only the EBANX collector reads it: a missing one is a FAIL only on accounts that route to EBANX.
9. generateToken and getThreeDSecureChallenge are awaited inside try/catch (H9, M)
- Look for these calls outside
try, a.then().catch()chain only, or an emptycatch {}. - FAIL on: any of those.
- Why: argument validation throws synchronously, so a
.catchchain never sees it and the page crashes; API errors carry the useful text inerror.response.data(for exampleINVALID_PARAMS: holder_name value less than allowed (3 chars minimum)). Secure Fields errors exposeerror.dataanderror.codeinstead.
10. A checkout session reaches generateToken (H10, B)
- Look for
apiClientPayment({ country_code })andgenerateToken({ payment_method }), both without a session. - FAIL on: neither call carries the session; the token session differs from the one sent to Create Payment.
- Why:
generateTokenrejects withcheckout session is required.
11. payment_method.type is an uppercase Payment.Type (H11, B)
- FAIL on
type:insidepayment_methodset to'card','credit_card','CREDIT_CARD', empty. - Why: throws
type=card is not valid(split:payment_method[0].type=CARDS is not valid), also in enrollment.
12. No docs-only device_fingerprint / third_party_session_id (H12, H)
- Look for
device_fingerprint:orthird_party_session_id:insidepayment_method. - FAIL on: present without the
device_fingerprints(array of{ provider_id, id }) orthird_party_data(array of{ provider_id, session_id }) forms. - Why: the SDK never reads those two names, so the value is dropped (a lost fingerprint made Mercado Pago decline every payment for one merchant).
13. Raw PAN: full unmasked number, validated by you, never logged (H13, H)
- Look at what feeds
card.detail.number:maskedPan,lastFour,*or bullet characters, spaces or dashes; no Luhn or length check;console.log, analytics, Sentry or server logs of the call argument. - Why (FAIL on any): headless passes the number as received with no Luhn or brand-length check, so a masked or
malformed PAN declines at the provider (
CardNumber: Invalid parameter size); logging writes card data to logs. - Spaces in the PAN or a 2-digit string year: not confirmed in code, verify at runtime. Docs list SAQ A only for Full, Lite and Secure Fields; with Headless the PAN passes through your page (report as information).
14. Create Payment: token only, absolute callback_url, same session (H14, B)
- Read the server code that builds the Create Payment body (also for Secure Fields).
- FAIL on:
vaulted_tokennext totoken;callback_urlmissing, null, relative or withouthttps://;checkout.sessiondifferent from the token’s session; your owncheckout_session_idon a DIRECT-flow create. - Why:
vaulted_tokenoverrides the whole payment method, fingerprint included; acallback_urlwithout protocol broke the 3DS page, and a null one left the challenge blank (incident findings, not SDK code: verify at runtime).
15. 3DS challenge: guard url, keep it intact, check the origin (H15, B)
- Look at
getThreeDSecureChallenge(, the code that sets the iframesrc, andaddEventListener('message'. - FAIL on: (a) no check for
url === nullor the literal'work in progress'; (b) the URL parsed and rebuilt, droppingsession,challenge-urloraccess-token; (c)embedded=truemissing for iframe use; (d) a listener that accepts any origin or matchesincludes('sdk-3ds')on a white-label setup, instead of checkingdata.origin === 'CHALLENGE'anddata.statusfrom the 3DS origin; (e) an object argument ({ checkoutSession }) instead of the string; (f) a framing page on another origin than the one the session was created from. - Why:
{ url: null }is the normal no-challenge answer, sonull&embedded=trueshows an error page; the challenge posts its result only to the origin signed in itssessiontoken (how it is derived: verify at runtime).
16. No bare yuno.continuePayment() in headless (H16, M)
- Look for
yuno.continuePayment(in a repo that usesapiClientPaymentand never callsstartCheckoutorsecureFields. - FAIL on: called without
{ checkoutSession }. Why: headless never sets the SDK state, so a bare call resolvesnulland shows nothing; with a session it renders Yuno’s UI. Use the headless client calls instead.
17. The payment result comes from the webhook or Retrieve Payment (H17, H)
- Look for fulfilment gated on
paymentResult,yunoPaymentResult, the iframemessageevent or the redirect landing page alone. - FAIL on: order status set from the browser only.
- Why: a redirect 3DS navigates away before any in-page callback runs, and a client signal can be forged.
18. No logic on card_data.fingerprint from the token (H18, L)
- Look for
.card_data.fingerprintor.fingerprinton the token response used for dedupe or risk. - Why: null for a first-time, non-vaulted card, set on the payment response (backend rule: verify at runtime).
Headless enrollment
19. Fresh customer_session, awaited client, status check (H19, H)
- Look at
apiClientEnroll(,continueEnrollment(and the server code that creates customer sessions. - FAIL on: one
customer_sessionreused across enrollments;apiClientEnrollwithoutawait; success assumed withoutstatus === 'ENROLLED';ENROLL_IN_PROCESSorPENDINGtreated as failure; no session on either call (throwscustomer_session is required). - Why:
DECLINED,REJECTED,ERRORare not an enrolled card;ENROLL_IN_PROCESSandPENDINGsettle by webhook, so failing them drops a card that enrolls later. A reused session failing the vault call: verify at runtime.
Secure Fields
20. One secureFields instance per form, kept in a ref, used for fields and token (H20, B)
- Look for
.secureFields(in a component body, render function,onSubmit, or an effect without stored result; two call sites; nouseRefor module variable. - FAIL on:
yuno.secureFields()able to run twice per mount;generateTokenon another instance than the one that created the fields. - Why: each instance owns its own hidden mediator and field map, so a token from the second instance sends an empty card that the API rejects (verify the answer at runtime); it also takes over the retry hook and session listener.
21. Each field created once, rendered into its own existing visible element, render awaited (H21, B)
- Look at
.create({ name:repeated for one name;.render('#...')targeting an element the framework creates later or re-creates;renderwithoutawait/ catch; names other thanpan,expiration,cvv,cardPin,cardLoadPreview; one container for two fields; a container (or ancestor) withdisplay:noneor clipped byoverflow:hidden. - FAIL on: any of these, but report
cardPinandcardLoadPreview(supported, undocumented) as information. There is no holder-name field; the name goes incardHolderName(check 23). - Why:
renderrejects withElement #pan doesn't exist in the DOM(an unhandled rejection if not awaited); an unknown name fails withbridge: url is required; the iframe sets its own pixel height from the child, so a zero height container is fine but a hidden or clipping one shows nothing. A secondcreatewith the same name leaving the old iframe in the DOM is code reading: verify at runtime.
Re-render and framework lifecycle (Secure Fields)
22. One mount per component, cleanup on unmount, StrictMode and remount safety (H22, H)
- React:
initializeonce (module-level promise or guarded ref);secureFields()and thecreate/rendersequence in one effect with acancelledguard after eachawait; one field instance per field name; deps limited to[yuno, checkoutSession, countryCode], callbacks read from a ref; cleanup setscancelled = trueand callsawait sf.unmountSync()with no arguments. StrictMode runs the effect twice in development, so the first mount must be fully torn down. Never create fields during render or inonChange. - Re-render: call
f.render(selector)again on the same field (it replaces its iframe); aftersf.unmountSync()the instance is dead, so create a newyuno.secureFields(). - Next.js: the component is
'use client'or loaded withdynamic(..., { ssr: false }); noloadScriptorinitializein server code (document is not defined). Vue / Angular / SPA (guidance, not in Yuno docs): create inonMounted/ngAfterViewInit,unmountSync()inonBeforeUnmount/ngOnDestroyor on route leave. - FAIL on: no teardown; teardown that only calls
field.unmountSync()per field; arenderedFlaginstead of cleanup (the Yuno React demo does this; do not copy it); deps that change each render; nocancelledguard; callbacks closing over stale state;createon a dead instance; a route change that leaves the old form mounted. - Why: a leaked first mount adds a second set of iframes and mediator (competing forms after SPA navigation lost
input and installments);
render()waits for the mediator, so an earlier cleanup can leave an iframe behind; calls aftersf.unmountSync()reject withbridge closed. Per-fieldunmountSyncleaving the mediator alive is code reading: verify at runtime by counting iframes indocument.bodyafter unmount.
Secure Fields (continued)
23. generateToken after render, with cardHolderName, without checkoutSession, form state checked (H23, B)
- Look at
.generateToken(/.generateTokenWithInformation(on the Secure Fields instance. - FAIL on: no
cardHolderName; acheckoutSessionkey when payment retry is on for the account (or a value that can differ from the instance’s current session); no check that the three fields rendered and are valid (onChangeflags orawait field.validate()); a call from another instance (check 20). - Why: no local validation (the Luhn check only logs), so an empty field goes as
''and the API answers 400, e.g.INVALID_PARAMS: holder_name value less than allowed (3 chars minimum); a passedcheckoutSessionwins over the cloned retry session. Without retry the docs allow it ongenerateToken.
24. Field helpers only after render, guarded after unmount (H24, M)
- FAIL on
.focus(,.blur(,.validate(,.setError(,.clearError(,.resetFields(,.setCardType(with no guard: beforeawait render()resolves, in cleanup after unmount, or on a possiblyundefinedgetElement(). - Why: before the field reached the mediator, the mediator indexes its map without a null check and rejects with
Cannot read properties of undefined (reading 'validateElement')(same family as theclearValueElementcrash); after a field or full unmount the call rejects withbridge closed.
25. Teardown uses secureFields.unmountSync(), not unmountSdk (H25, M)
- FAIL on
unmountSdk(as the only Secure Fields cleanup, including after a payment that needs no further action. - Why:
unmountSdkonly unmounts the SDK’s React root (such as onecontinuePaymentrendered), never the Secure Fields iframes or mediator, which stay on the page after payment and can capture a second submit.
26. Payment retry wiring when the retry flag is on (H26, H)
- Look at the handling after Create Payment,
continuePayment(andupdateCheckoutSession(. - FAIL on: a
DECLINED/ERRORcard result not followed byyuno.continuePayment(); thepanfield destroyed before retry;checkoutSessionpassed to the token call. - Why: the form is left open with no next step, or retry runs on a stale session; retry reads the field named
pan. Retry is a server flag per organization (whether sandbox has it: verify at runtime).
27. Styling and options respect the iframe (H27, L)
- Look at
options.styles,designType,validateOnBlur,validationTypeandonChangedata reads. - FAIL on:
designTypeoutsidefloat-label,float-label-static,label-placeholder; page CSS expected to reach inside the iframe;onChangedata read without?..validateOnBlurstill works (treated ason_blur): report it as information, since the payment docs page calls it deprecated in favour ofvalidationType. - Why:
designType=<value> is not validis thrown;stylesis a CSS string injected only in the iframe; style the wrapper via itsfocus,hover,errorclasses (the SDK setsfont-size:0,margin:-1px).
28. Secure Fields enrollment: customerSession only, cardHolderName passed (H28, H)
- Look at
secureFields({withcustomerSessionand atgenerateVaultedToken(. - FAIL on: both
checkoutSessionandcustomerSession(the SDK runs in payment mode); no customer session at all (acustomerSessionpassed togenerateVaultedTokenis an accepted fallback); nocardHolderName. - Why:
secureFields()validates nothing, so a missing session fails only later inside the iframes.
Two-card split (headless)
29. Composite token: shape, totals and order (H29, B)
- Look at
generateToken({ ... payment_method: [. - FAIL on: fewer than two items; an item with neither
cardnorvaulted_token;amount.valuemissing, zero or a string ("60"); emptyamount.currency; a currency different fromtotal.currency; amounts that do not add up exactly; an unknowntype. Report nototaland a third item as warnings: the docs requiretotaland exactly two items, the SDK does not. - Why: the SDK throws before any request:
payment_method must have at least two items; use an object for one payment method,payment_method[1] needs card or vaulted_token,payment_method[0].amount.value=0 is not valid,payment_method[0].amount.currency= is not valid,payment_method[0].type=CARDS is not valid,payment_method[1].amount.currency=EUR does not match total.currency=USD,payment_method amounts add up to 60 + 30 but total.value=100,payment_method must be an object or an array of payment methods,total.currency=<v> is not valid,total.value=<v> is not valid.totalis optional in SDK and core; the docs sayPOST /v2/paymentsrejects a payment without it: verify at runtime, as for a third item. Per-itemdevice_fingerprintsandthird_party_dataare dropped by the v2 token route today, so split tokens carry no merchant fraud data.
30. Composite token redeemed on v2, challenges run one by one (H30, B)
- Read the server code that receives the composite token and the loop around
getThreeDSecureChallenge. - FAIL on: the token sent to
/v1/payments; nocallback_url; the next challenge requested before the previous card’sTHREE_D_SECUREtransaction leftPENDING(webhook orGET /v2/payments/{id}) or before itsCHALLENGEmessage; a fixed-count loop instead of stopping onurl === null; the other card offered afterstatus === 'ERROR';card_dataread from the composite result. - Why: an early call returns the same card again (first card still waiting), and after
ERRORthe SDK still hands out the next card, so stopping is your job. The docs say the token works only onPOST /v2/paymentsand the other card is reversed after a failure: verify at runtime. Split must be enabled on the account by Yuno.
31. SDK version supports split and the fixes referenced here (H31, M)
- Read
@yuno-payments/sdk-webinpackage.jsonand the lockfile, and script URLs (sdk-web.y.uno/v1.xx/main.js,loadScript). - FAIL on: split used on a line older than 1.11.43; a pinned older patch where a referenced fix landed (for example
the Secure Fields
clearValueElementguard). - Why: on 1.11.42 and older the array goes through the single-card validator and throws
type=undefined is not validbefore any request. Thev1.11URL already serves 1.11.43 or later.
Report format
Diagnostics to capture
- The SDK version (script URL or
@yuno-payments/sdk-web) and the sdk-web-card version from the iframesrc(sdk-web-card.<env>.y.uno/v<version>/pages/...); the card version follows the loadedmain.js. - The checkout session id (customer session for enrollment) and the UTC time.
- Browser console errors, especially
bridge: child did not connect within 30000ms,bridge closed,Element #<id> doesn't exist in the DOMand any<key>=<value> is not valid. - CSP violation reports, blocked
*.y.unorequests, a fieldrender()that never settles, and thesdk-web-cardiframe count after a remount. - The failing call without card data or keys, and
error.response.dataorerror.code/error.data.
Docs versus code
Where the public docs and the code disagree, follow the code and tell the merchant the docs are being fixed.
Sources: the Web SDK 1.11.47 source code (served
v1.11 bundle 1.11.45), the card form 1.93.0,
@yuno-payments/sdk-web-types 5.8.1 and the public Yuno docs. Research 2026-10-29.