Skip to main content
Drop this file in the root of your backend service and ask Claude Code:
Validate my Yuno VTEX headless wallet backend following CLAUDE.md.
It audits the server code against the deferred preflight contract of the VTEX connector, described in the flow below, and reports what is wrong, with file and line. It only reads code: it changes nothing unless you ask.

When to use this validator

Use it when a server of yours calls the two preflight routes on behalf of a mobile app or a website: a backend service, or a Node, Python or Java client that calls these routes on https://<store-host>:
  • POST /_v/yunopartnerbr.yuno/v4/session/preflight
  • POST /_v/yunopartnerbr.yuno/v4/session/preflight/payments
Your backend receives the wallet token from the app and hands it to the connector. The app or the storefront never calls the connector itself. When the app calls these routes directly, use the Flutter or React Native validator instead. Run that one on the app as well when your backend only relays: the SDK initialization and the token listener live in the app.

The flow being validated

The VTEX order (Steps 6–8) is placed only after /session/preflight/payments has answered { deferred: true }. The payment does not exist in Yuno until Step 8. The outcome comes from VTEX (check 11). Both routes are public routes of the store: they take no VTEX app key or token. The behaviour below is the one of VTEX connector 4.2.378.

Checks

Report each one as PASS / FAIL / N/A with the evidence (file:line). Do not assume a check passes because the code “looks right”: quote the line that proves it.

1. Session preflight runs before the wallet, with every field

  • The body carries orderFormId, amount, currency, country and affiliationName (check 2).
  • orderFormId is the VTEX order form the order is placed from. At authorization the connector finds the stored session by the order form of the VTEX order, so a different id leaves the stored payment unused.
  • amount is a JSON number in decimal units (845.90), not cents (84590) and not a string. A missing field, or an amount of 0, answers 400 {"error":"Missing required fields: orderFormId, amount, currency, country"}. A string amount passes that check and fails later with 500 {"error":"Failed to create preflight session"}.
  • country is the alpha-2 (BR) or alpha-3 (BRA) code; the connector normalizes both to alpha-2.
  • The answer is 200 { checkoutSession, publicApiKey }.
  • FAIL if any field can be missing, if the amount is a string or in cents, or if preflight can run before shipping, coupons and installments are final.
  • A second preflight for the same orderFormId replaces the stored session. When the cart total changes, the backend reruns preflight and the app starts the wallet again with the new checkoutSession. FAIL if the cart can change while a session is live without rerunning preflight.

2. Both preflight calls name the production affiliation

affiliationName is optional in both bodies, but the backend must send it in both.
  • Without it, or with an empty string, the connector takes the first affiliation of the store whose implementation name contains “Yuno” (any case), in the order VTEX lists them. A VTEX affiliation carries no test or production flag, so a test affiliation (status “Teste” in the VTEX admin) listed before the production one is the one used. The session is then opened with the test affiliation’s credentials and fails when they are not valid for production (for example an Account ID that does not exist), and preflight answers 500 {"error":"Failed to create preflight session"}.
  • With a name, the connector looks for an affiliation with exactly that name, same spelling and case. A name the store does not have answers 404 {"error":"Yuno affiliation not found"} on both routes.
  • The name is per store. The backend must know the name of each store’s production Yuno affiliation, as shown in the VTEX admin under Payments > Affiliations.
  • Send the same name in both calls. The payment is bound to the session the first call opened and to the Yuno account of the affiliation the second call resolves, so two names pair a session of one account with another account.
  • FAIL if either call omits affiliationName or sends '', null or None.
  • FAIL if the name is hard-coded to one value while the backend serves stores whose production affiliation has a different name.
  • FAIL if the two calls can send different names for one order.
  • Report the value sent in each call, where the backend reads it from, and whether the store has more than one affiliation of the Yuno provider. When the code cannot tell, say so and ask for the store’s affiliation list.
  • Symptom: the wallet button fails in some stores and works in others, preflight answers the 500 above, and card payments in the same stores work. The same 500 also appears when the store’s affiliation lookup fails on the VTEX side, whatever name is sent, so a 500 alone does not prove a wrong name.

3. The preflight payment runs only after a non-empty token

  • The body is { orderFormId, payment, affiliationName, createPaymentInAuth: true }, with the same orderFormId and affiliationName as the session preflight. payment carries payment_method.type, payment_method.token, amount.value, amount.currency and country.
  • The call is made only when the app has delivered a non-empty token, never before the wallet sheet closes and never on a timer. FAIL if the call is reachable with token empty, null or missing.
  • createPaymentInAuth: true is in the body. Without it the connector creates the payment in Yuno immediately, before a VTEX order exists. FAIL if it is missing or can be false.
  • The answer is 200 {"deferred":true}. FAIL if the VTEX order can be placed before this answer arrives.
  • Why: the connector accepts a token-less body and answers { deferred: true }. The failure only surfaces at authorization, as a denied order with Deferred preflight payment creation failed.
  • Deferred creation works only for GOOGLE_PAY, APPLE_PAY, SAMSUNG_PAY and CLICK_TO_PAY. Any other payment_method.type answers 400 {"error":"Deferred payment creation is only supported for GOOGLE_PAY, APPLE_PAY, SAMSUNG_PAY, CLICK_TO_PAY"}.

4. Payment method type matches what VTEX authorizes

payment_method.type in the preflight payment must correspond to the VTEX payment method the order is placed with (Apple Pay / Google Pay / Samsung Pay / C2P). Check the paymentSystem used in Step 5 and Step 7 against the type the backend sends. A mismatch makes the connector ignore the stored payment.

5. Amount, currency and country are the same in both calls and in the order

  • The preflight payment’s payment.amount.value, payment.amount.currency and payment.country must be the amount, currency and country of the session preflight. The connector stores a SHA-256 fingerprint of amount:currency:country with the session and recomputes it from the preflight payment.
  • A mismatch answers 409 {"error":"Preflight session does not match the current payment payload. Please re-run preflight."}. A missing or non-numeric amount, currency or country answers 400 {"error":"Missing required fields for preflight validation: amount, currency, country"}.
  • At authorization the connector recomputes the fingerprint from the VTEX order’s value, currency and country (the order’s billing country or the country sent in the preflight payment), and checks that the session is less than 5 hours old. When either fails, the order is denied with Preflight session expired or does not match the order. Please retry.
  • FAIL if the two calls read the amount from different sources (cart total in one, order total in the other), or if the amount sent can differ from the total of the VTEX order.

6. One store host per order

Session preflight, preflight payment and the order placement use the same VTEX store host. The connector reads the affiliations of the store it is called on. FAIL if the connector base URL and the checkout base URL can point at different VTEX accounts, for example a single configured host for several stores.

7. Split orders are not offered with wallets

A cart fulfilled by more than one seller becomes several VTEX orders, and one wallet token cannot cover them. FAIL if wallets are offered without checking the cart can produce a single order.

8. The backend never calls the Payment App routes

  • The token goes only to /session/preflight/payments. FAIL if the backend sends it to /payments/yuno, the Payment App route of the standard checkout.
  • FAIL if the backend polls /payments/yuno/status. That route needs the encrypted session of the standard checkout; without it the connector answers 200 with a timestamp and no payment status. The outcome comes from VTEX (check 11).

9. Errors are handled, logged and retried at most once

  • A rerun of the session preflight for the same orderFormId replaces the stored session, so one retry is safe.
  • Log the store host, the orderFormId, the route, the HTTP status and the response body of every failed call. FAIL if a failure is swallowed or turned into a generic error with no status.
  • FAIL if the backend logs the wallet token, the full public API key or any Yuno private key.
  • FAIL if the backend retries in a loop, or keeps a failing order alive without rerunning preflight.

10. The public API key goes to the app unchanged and is not kept

  • The publicApiKey from the session preflight answer is the key of the affiliation that call resolved. The app’s SDK must be initialized with exactly that value.
  • FAIL if the backend replaces it with a key from its own configuration, caches it per store or per app install, or reuses it for another order. Read it from each preflight answer.
  • The backend needs no Yuno private key: the connector holds the affiliation’s credentials. FAIL if the backend is configured with a Yuno private secret key for this flow.

11. The outcome is read from VTEX, not from the preflight answers

{ deferred: true } only means the body was stored. The result comes from Step 8 and the order: After a 2xx, GET /api/checkout/pub/orders/order-group/{orderGroup} and decide from each order’s status: payment-approved (or any later state) is success, payment-pending means the payment is still being processed.
  • FAIL if the backend reports success to the app on { deferred: true }, or treats a 2xx as approved without reading the order.

Report format

Finish with the failures ordered by impact, each with the one-line fix.

Diagnostics to capture from a failing order

When the checks pass and the wallet still does not complete, capture and send:
  1. The store host and the orderFormId.
  2. The affiliationName sent in each call.
  3. For each call, the UTC timestamp, the HTTP status and the response body. Leave out the token and the keys.
  4. The checkoutSession from the session preflight answer.
  5. The VTEX orderGroup, the gatewayCallback HTTP status and body, and the order status read afterwards.
  6. The VTEX connector version installed in the store, from the VTEX admin. The connector sets no version header on these routes.
That is enough to line your backend’s calls up with the server trace for the session on the Yuno side.

Versions

The checks describe VTEX connector 4.2.378 and later. For the app side of the flow (SDK initialization, token listener, native setup), run the Flutter or React Native validator on the app.