PayPal Express Checkout — orchestrated via Braintree
detecting mode…

PayPal via Braintree for SpaceX — product brief

Braintree guidance (Alex Dantoft) + architecture outline (Romel, #yuno-paypal-spacex-internal) · PayPal is at the table · July 2026
DRAFT Working draft — pending team confirmation. This outline consolidates the Braintree integration guidance, the SpaceX requirement calls, and the internal alignment discussion. It has not been validated by the implementing teams — refer to the PRD for the source of truth on agreed and open decisions, and raise corrections in #yuno-paypal-spacex-internal.
SpaceX wants to move its PayPal volume from Adyen to Braintree (PayPal owns Braintree — better economics for them). The full use case: the first payment vaults the shopper’s PayPal account, and monthly renewals charge without the shopper present. Yuno is Braintree’s merchant of record, so every step is ours to build — and once built, we keep this capability for any merchant with a Braintree contract.
MerchantSpaceX — big, subscription business, currently on Adyen Components
DriverPayPal recommended the technical path and is in the conversations
AskProduct prioritization + staffing (Carolina / Filipe / Aleksandra) so we can commit dates

The defined flow — six steps

From the PayPal alignment call · full sequence diagrams in the Workflow diagram tab, running live in the Checkout demo tab
  1. New payment method: “PayPal via Braintree”A distinct method — the SDK knows from the start which stack to load.
  2. createClientToken — new Integrations serviceToken delivered in the method config; SDK loads Braintree JS (+ PayPal v6) and renders the button.
  3. Buyer approves — price can changeAddress change in the PayPal window → SDK notifies the merchant, receives the new amount, shows it before approval. Net-new mechanism.
  4. Tokenize — nonce travels inside the OTTLike any other credential. No service is called at this step.
  5. POST /payments · vault_on_success: truePayments pulls the nonce from the OTT → Integrations charges it → Braintree returns the multi-use token → core persists it for renewals.
  6. Everything else on existing infraStatuses, webhooks, refunds and the recurring renewals.

Build scope by team

Owners from Romel’s message · PRD with resolved + open decisions is on Confluence
Checkout / SDKChauca · Daniel Vega
  • New PayPal-Braintree moduleBraintree client SDK + PayPal JS v6 in the web SDK
  • Client token in method configconsume it to boot the stack
  • Nonce + multi-use token in OTTcarried like any credential
  • Price-update mechanismnet-new merchant communication on address change
IntegrationsBeta
  • createClientToken serviceproven in the sandbox prototype
  • Nonce → multi-use exchangeexact mutation TBC — need vault without charge for $0 flows
  • braintree-webhookvalidate PayPal event mapping
Payments / CoreAle
  • Nonce from OTT → chargepass it in the charge call
  • vault_on_successsupport on payment creation
  • Wallet persistence modelcurrent model is card-centric — define wallets as customer payment methods

Prototype status & open items

This page runs the defined flow against the real Braintree sandbox — badge top-right shows the mode · use “Pop out log” to demo
Proven live in sandboxCheckout demo tab
  • createClientToken via GQL (Integrations service shape)
  • Client token → PayPal window — Braintree JS 3.143 + PayPal button
  • Live re-pricing — real onShippingAddressChange → updatePayment; non-US rejected
  • Nonce wrapped in an OTT — no Braintree call at tokenize
  • chargePayPalAccount — real sandbox transactions, verifiable in the Control Panel
Openowners · targets
  • vault_on_success mutation — vault without charge for $0 flows; TBC with Braintree (Beta)
  • Production credentials from PayPal/Braintree (Jarrett/Justo — target end of week)
  • PayPal verify flow — zero-dollar / nominal with auto-cancel, like cards (Jarrett/Daniel)
  • Prod test setup replicating SpaceX flows: $0, sign-up, tokenized one-off
  • Server-side shipping callbacks — available through Braintree? To confirm
  • NuPay — explicitly deprioritized in favor of PayPal
SpaceX’s asks from the customer calls (context alongside the internal plan): merchant-controlled method rendering — they keep their own selector and mount our SDK per method, like Adyen’s checkout.create('paypal') — and the front-end data contract: name, email, phone, address, PayPal transaction ID. See the Adyen comparison tab.
How to run this demo
1Pop out the logClick “Pop out log” on the right panel so the flow stays visible behind the PayPal window
2Click the PayPal buttonThe real PayPal sandbox window opens
3Log in with the demo buyerClick to copy:sb-yunoDemo@personal.example.comYunoDemo1
4Change the shipping addressWatch step 2 in the log fire the real callback and re-price the order live
▶  Step 1 — Pop out the live log before you pay Pop out log ↗

Demo Store checkout

Generic merchant checkout page
Starlite Router
Qty 1
$120.00
Monthly Service
First month
$80.00
Subtotal$200.00
Shipping$0.00
Tax$16.50
Total$216.50
Step 1 — open the live flow log,
so you can watch it behind the PayPal window
skip, pay without the log
payments orchestrated by yuno.

✓ Payment complete — data returned to merchant

Customer name Email Phone Address PayPal txn ID Vault token (multi-use)

Behind the scenes

Yuno is the merchant of record toward Braintree — the merchant just mounts our button; Yuno makes every Braintree call
Yuno server Browser (Yuno SDK wrapping Braintree JS) Braintree GraphQL API
1
createClientToken
yuno serverGQL

The merchant opens a checkout session with Yuno. Yuno's server calls the Braintree GraphQL API to mint a short-lived client token that authorizes the browser SDK — the merchant never touches Braintree credentials.


      
2
Create checkout + vault session
yuno sdk

The merchant mounts the Yuno SDK, which renders the PayPal button and initializes Braintree JS under the hood. It calls createCheckoutWithVaultSession, wiring up onShippingAddressChange / onShippingOptionsChange so the merchant page can re-price live. The PayPal window opens.


      
3
Tokenize
yuno sdk

After the customer approves in the PayPal window, the Yuno SDK tokenizes the approval into a single-use token and surfaces the customer details to the merchant's front end.


        
Front-end data requirement: name, email, phone, and address arrive here in the SDK response — this is exactly the data the merchant asked us to expose client-side.
4
chargePayPalAccount
yuno serverGQL

The merchant creates the payment with Yuno (POST /payments). Yuno's server calls chargePayPalAccount: consumes the nonce, creates the transaction, and receives a multi-use payment method token that Yuno vaults for future charges.


        
Two outputs: the PayPal transaction ID (returned to the merchant) and the vault token (held by Yuno — future payments skip the PayPal window entirely).

Callback timing — when can the amount still change?

The callbacks fire inside the PayPal window, before approval — after tokenize the OTT goes back to the merchant, who creates the payment; we charge Braintree and store the vaulted token
RE-PRICING WINDOW — amount can change AMOUNT LOCKED — buyer approved the exact final total 1 PayPal window opens createCheckoutWithVaultSession 2 Callbacks fire (0–n) onShippingAddressChange / Options merchant re-prices → patch or reject buyer edits address / option 3 “Agree & Pay” onApprove — amount locked 4 Tokenize → OTT nonce wrapped inside the OTT, returned to the merchant page 5 Merchant creates payment POST /payments OTT · vault_on_success: true 6 chargePayPalAccount txn ID + multi-use token → core stores the vaulted token
Why it matters: re-pricing happens only at node 2, live in the PayPal window. From node 3 on the amount is fixed — the OTT (4), the payment creation (5) and the Braintree charge (6) all carry the approved total. The multi-use token from node 6 is what powers the next purchase with no PayPal window.

Session creation — what gets passed in

Fields at each hop when the session is generated · lime = the optional shipping-address levers that decide whether the re-pricing callbacks fire
Merchant → YunoPOST /checkout/sessions
  • amountorder total at session start
  • currencye.g. USD, BRL, MXN
  • countrydrives available payment methods
  • customer_idYuno customer reference
  • merchant_order_idSpaceX order reference
  • shipping_addressoptional — only if SpaceX already collected it
Yuno maps to
Braintree session
Yuno SDK → Braintree JScreateCheckoutWithVaultSession
  • amountfrom the Yuno session
  • currencyfrom the Yuno session
  • commit: true“Pay Now” button (vs. “Continue”)
  • enableShippingAddressPayPal collects + returns an address
  • shippingAddressOverridepre-fill with SpaceX's address (name, line1, city, state, zip, country, phone)
  • shippingAddressEditablefalse = locked, callbacks never fire · true = customer can change it
  • contactPreferencecontrols email / phone collection
  • onShippingAddressChangecallback → SpaceX re-prices
  • onShippingOptionsChangecallback → SpaceX re-prices
server-side,
step 1
Integrations service → Braintree GQLcreateClientToken (new service)
  • merchantAccountIdroutes to the right Braintree account / currency
  • clientToken← returned; authorizes the browser SDK
Two configurations:
Pure Express — send no address; PayPal supplies it from the customer's account; callbacks re-price live.

Address-first — SpaceX collects the address, passes shippingAddressOverride + editable: false; priced upfront, no callbacks needed.

Flow 1 — First payment: Express Checkout + vault

Buyer → SpaceX → Yuno → Braintree → PayPal · the buyer drives the events: selects PayPal on SpaceX's page, changes address in the PayPal window (firing the callbacks), and approves once
Buyer SpaceX merchant page + server yuno. SDK wraps the PayPal session Braintree PayPal CALLBACK LOOP — repeats every time the buyer changes address or shipping option 1. Buyer selects PayPal new method “PayPal via Braintree” — SDK knows which stack to load 2. Create checkout session POST /checkout/sessions 3. createClientToken GraphQL clientToken 4. clientToken delivered in the method config — SDK loads Braintree JS + PayPal v6, renders the button 5. PayPal window opens — buyer logs in createCheckoutWithVaultSession 6. Buyer changes address / shipping option in the PayPal window onShippingAddressChange (city, state, zip, country) — via the Yuno SDK 7. SpaceX re-prices (their rates + tax) → patch new total, or reject the address buyer sees the updated total (or inline error) live in the window 8. Buyer clicks “Agree & Pay” — amount now locked 9. Tokenize → nonce wrapped inside the OTT + name, email, phone, final address (no service call) 10. Create payment POST /payments (OTT · vault_on_success: true) 11. chargePayPalAccount nonce from the OTT · vault-on-success 12. Capture 13. SUCCEEDED + txn ID to SpaceX · core persists the multi-use token as the vaulted method ① NONCE CREATED ② NONCE TRAVELS INSIDE THE OTT ③ NONCE CONSUMED (single use) → txn

Re-pricing detail — address change → tax recalculation → PayPal window update

Zoom into one iteration of the callback loop · SpaceX owns the math; PayPal, Braintree and Yuno only transport the event and the new total
Buyer PayPal window SpaceX page Yuno SDK + callback handler SpaceX server shipping rates + tax engine 1. Buyer picks a different shipping address TX → CA · checkout pauses, “Pay” disabled 2. onShippingAddressChange fires (via Yuno SDK) { city, state: "CA", zip: "90250", country: "US" } — street withheld until approval 3. Recalculate for the new address POST /rates-and-tax → shipping rules + tax engine (Avalara / Vertex / …) 4. shipping $5.00 · CA tax $18.20 → new total $223.20 (was $216.50) 5. Handler resolves → patch order amount to $223.20 Yuno SDK → Braintree JS patches the PayPal order client-side 6. Window refreshes — buyer sees $223.20 “Pay” re-enabled · buyer approves the exact final amount ALT — address fails SpaceX's rules (unserviceable country, PO box, …) 5b. Handler rejects the address reject(ADDRESS_ERROR / COUNTRY_ERROR / ZIP_ERROR) 6b. Inline error in the window “seller doesn't ship to this address” · buyer must pick another — loop repeats from 1
Product requirement for our SDK: steps 2 and 5/5b are the pass-through we must build — surface the event to the merchant's handler, then propagate either the patched amount or the rejection reason down to the PayPal window. The final chargePayPalAccount must use the patched total.

Flow 2 — Returning customer: vaulted payment

No PayPal window — the vaulted token charges the billing agreement directly · runs on existing recurring/renewals infrastructure
SpaceX yuno. Braintree PayPal 1. Create payment with stored token POST /payments (vaulted method) 2. chargePayPalAccount paymentMethodId: vault token 3. Charge billing agreement transaction ID 4. SUCCEEDED — no customer interaction

API call map — who calls what

Every call in the flow: merchant ↔ Yuno endpoints, Yuno internal services, and Braintree/PayPal · client-side calls marked
#From → ToCallReturns / purpose
Setup
1Merchant → YunoPOST /checkout/sessionsSession + method config; “PayPal via Braintree” listed as its own method
2Checkout → Integrations (internal)createClientToken (new service)Requests the provider client token
3Integrations → BraintreeGQL mutation createClientTokenShort-lived clientToken, delivered in the method config
4Yuno SDK (browser)loads Braintree JS + PayPal v6Renders the PayPal button — script load, not an API call
In the PayPal window — all client-side
5Yuno SDK → PayPalcreateCheckoutWithVaultSessionOpens the window; checkout + vault session
6PayPal → SDK → merchant pageonShippingAddressChange / onShippingOptionsChangeCandidate address (city/state/zip/country); buyer is waiting
7Merchant page → SDK → PayPalupdatePayment (new amount) or rejectBuyer sees the new total — or an inline “doesn’t ship here” error
8Yuno SDKtokenizePayment → wrap into OTTSingle-use nonce + payer name, email, phone, address. No server call.
Completion — server-side
9Merchant → YunoPOST /payments { ott, amount, vault_on_success: true }Creates the payment with the approved total
10Payments → Integrations (internal)charge with nonce from the OTTPayments unwraps the OTT; Integrations owns the provider call
11Integrations → BraintreeGQL mutation chargePayPalAccount { nonce, amount, vault-on-success }Transaction (PayPal txn ID) + multi-use payment method token — nonce consumed, single use
12Corepersist multi-use tokenStored as the customer’s vaulted payment method (wallet model — open item)
13Braintree → Yunobraintree-webhook eventsStatuses, refunds, disputes — existing infra; PayPal event mapping to validate
Renewals — no shopper present
14Merchant → YunoPOST /payments { vaulted_token }Monthly renewal / one-click purchase
15Integrations → BraintreeGQL chargePayPalAccount { vault token }Charges the billing agreement — no PayPal window, buyer absent

How Adyen runs the same PayPal flow (Components)

SpaceX's current setup · Adyen connects to PayPal directly (no Braintree hop) · merchant mounts checkout.create('paypal') — a single-method component, the control model SpaceX wants from our full SDK
SpaceX Adyen PayPal RE-PRICING LOOP — repeats on every address / delivery change (rotating paymentData) 1. Create session POST /sessions (amount, countryCode, returnUrl) 2. Merchant mounts checkout.create('paypal') — component loads PayPal JS, renders smart button 3. Shopper clicks PayPal button POST /payments (type: paypal) → action.sdkData 4. PayPal lightbox opens — shopper logs in onShippingAddressChange / onShippingOptionsChange (city, state, zip, country) 5. Re-price → update the lightbox POST /paypal/updateOrder (paymentData, amount, deliveryMethods) new total shown in lightbox 6. Shopper approves → onAdditionalDetails (payload + shopper details) 7. Finalize POST /payments/details 8. Capture with PayPal 9. Authorised + stored payment method (if storePaymentMethod: true) — future charges skip the lightbox ① PAYLOAD CREATED (nonce-equivalent) ② PAYLOAD PASSED → SpaceX → Adyen ③ CONSUMED — Adyen finalizes with PayPal

Side by side — Adyen vs. Yuno→Braintree

Same PayPal JS under the hood, same callbacks, same vault outcome — the differences are in topology and where re-pricing happens
Adyen (Components)Merchant → Adyen → PayPal
  • Topologydirect PSP connection to PayPal — 3 parties
  • Component modelcheckout.create('paypal') — merchant picks the method, mounts one component
  • Re-pricingserver round-trip: /paypal/updateOrder with rotating paymentData on every change
  • ApprovalonAdditionalDetails → /payments/details to finalize
  • VaultstorePaymentMethod → stored method for lightbox-free charges
vs
Yuno via BraintreeMerchant → Yuno → Braintree → PayPal
  • Topologyorchestrated through Braintree — 4 parties, Yuno is Braintree's merchant of record
  • Component modeltoday: full SDK owns the selector — the gap; the ask is Adyen-style mount-per-method
  • Re-pricingclient-side: callback patches the amount in the Braintree JS session — no server round-trip
  • Approvaltokenize → nonce + customer details → POST /payments
  • VaultchargePayPalAccount vault:true → multi-use token held by Yuno
Takeaway for product: SpaceX already has single-method mounting, live re-pricing, address rejection, and vaulting with Adyen. Functionally our Braintree path delivers the same outcomes — the missing piece is the integration surface: letting the merchant choose the payment method and mount our SDK for just that method, like checkout.create('paypal'). Close that, and the switch is UX-neutral for SpaceX with orchestration benefits on top.
Demo buyer account — use in the PayPal window:
sb-yunoDemo@personal.example.com
YunoDemo1  click to copy
PayPal sandbox · drag me
Pay Demo Store$216.50
Jane Astronautjane.astronaut@example.com
+1 (310) 555-0117
Simulated popup — changing the address fires onShippingAddressChange