> ## 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: VTEX headless wallets (backend)

> A read-only checklist for AI coding agents that audits a backend service calling the VTEX connector preflight routes for Google Pay, Apple Pay, Samsung Pay and Click to Pay on behalf of an app or a website.

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](https://docs.y.uno/docs/plugins/vtex/validators/headless-wallets-flutter.md)
or [React Native](https://docs.y.uno/docs/plugins/vtex/validators/headless-wallets-react-native.md) 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

```
App: VTEX cart (Steps 1–5, total final)
Backend → POST /session/preflight          { orderFormId, amount, currency, country, affiliationName }
                                           → { checkoutSession, publicApiKey }
Backend → App: checkoutSession, publicApiKey
App: SDK initialized with publicApiKey → wallet sheet → one-time token
App → Backend: token
Backend → POST /session/preflight/payments { orderFormId, payment (with the token), affiliationName,
                                             createPaymentInAuth: true }
                                           → { deferred: true }
VTEX Step 6 transaction → Step 7 payments → Step 8 gatewayCallback
  → the connector creates the payment in Yuno during authorization and answers VTEX
  → the gatewayCallback response + GET /api/checkout/pub/orders/order-group/{orderGroup} → outcome
```

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

| Route | Answer | Meaning | What the backend does |
| - | - | - | - |
| Session preflight | `400 {"error":"Missing required fields: orderFormId, amount, currency, country"}` | A field is missing, or the amount is `0` | Fix the request; do not retry |
| Both | `404 {"error":"Yuno affiliation not found"}` | The `affiliationName` does not exist in this store. On the preflight payment it can also follow a failed affiliation lookup on the VTEX side | Check the name against the store's affiliations (check 2); on the preflight payment, retry once first |
| Session preflight | `500 {"error":"Failed to create preflight session"}` | The session could not be created | Retry once after a short delay; a second 500 means the affiliation name (check 2) or a failed affiliation lookup on the VTEX side; the body is the same for both |
| Preflight payment | `400 {"error":"Missing required fields: orderFormId, payment"}` | A field is missing | Fix the request; do not retry |
| Preflight payment | `404 {"error":"Preflight session not found for this orderFormId"}` | No session preflight for that order form | Run the session preflight first, with the same `orderFormId` |
| Preflight payment | `409` fingerprint mismatch (check 5) | The amount, currency or country changed | Rerun preflight and restart the wallet with the new session |
| Preflight payment | `500 {"error":"Failed to create preflight payment"}` | The payment body could not be stored | Retry once after a short delay |

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

| `gatewayCallback` answer | Meaning | What the backend does |
| - | - | - |
| **428** | The connector did not use the stored body and answered with the Payment App payload, for example on a payment method mismatch (check 4) | Report an error to the app; log the response |
| **Other non-2xx** (e.g. 500 `CHK0223`) | Payment denied; the error body carries the connector's message, for example the expired or mismatched session of check 5 | Report an error to the app; log the response |
| **2xx** | Not denied: **approved or still pending**; VTEX answers both the same way | Read 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

```
CHECK  1  Session preflight fields      PASS  src/vtex/preflight.ts:41
CHECK  2  Affiliation name              FAIL  src/vtex/preflight.ts:44 — affiliationName not sent; the store has
                                              two Yuno affiliations, the test one listed first
CHECK  3  Preflight payment after token PASS  src/vtex/handoff.ts:88
...
CHECK 11  Outcome from VTEX             FAIL  src/vtex/order.ts:120 — 2xx from gatewayCallback reported as approved
Connector routes on: https://<store-host>, affiliationName "<value>"
```

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](https://docs.y.uno/docs/plugins/vtex/validators/headless-wallets-flutter.md) or
[React Native](https://docs.y.uno/docs/plugins/vtex/validators/headless-wallets-react-native.md) validator on the app.


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