Wallets
Apple Pay and Google Pay integration.
Paysio serves Apple Pay and Google Pay natively — paysio.js renders the official wallet buttons, the encrypted wallet token is decrypted by Paysio server-side, and the charge settles on your card gateway. No Apple/Google merchant accounts, gateway wallet setup, or certificate work on your side. The buyer authorizes in the wallet sheet, Paysio.js emits a walletPayment event, and your server forwards its paysioWallet payload to POST /charges.
Before you start
- Paysio Wallets must be enabled for your workspace (Settings → Checkout), plus the Apple Pay / Google Pay toggles for the wallets you want.
- Apple Pay requires HTTPS. It will not work on localhost — Paysio.js automatically skips Apple Pay on localhost/HTTP.
- Apple Pay live mode requires your domain to be registered — see “Apple Pay domain registration” below. Test mode needs no registration and works on any HTTPS domain (tunnels included). (Apple verifies the top-level page domain, so this cannot be skipped.)
- Google Pay needs no registration at all, on any domain, in any mode: the button renders inside an invisible Paysio-hosted frame, which satisfies Google's own domain checks for you. All styling options still apply, and the frame is sized by your
buttonHeight. - The Apple Pay button renders in every major browser: on Safari/iOS the sheet opens directly; elsewhere Apple shows a code/QR dialog the buyer scans with their iPhone.
Mount wallet buttons
const paysio = Paysio('pk_live_your_key');
const elements = paysio.elements();
// Mount wallet buttons
elements.mountWallets('#wallet-buttons', {
amount: 29.99, // Amount in dollars
currency: 'USD',
country: 'US',
collectShipping: true, // Optional: request shipping address
});
// Listen for wallet payments (Apple Pay / Google Pay)
elements.on('walletPayment', (data) => {
console.log('Wallet type:', data.walletType); // 'apple_pay' or 'google_pay'
console.log('Payload:', data.paysioWallet); // forward VERBATIM to /charges
console.log('Payer:', data.wallet); // buyer contact from the sheet
// Send the wallet payload AND the buyer contact to your server.
// Returning a Promise controls the Apple Pay sheet's success/failure
// state: resolve true = success, false = failure.
return fetch('/your-server/process-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
paysio_wallet: data.paysioWallet,
wallet: data.wallet,
three_ds: data.threeDS, // present when a Google Pay step-up ran
}),
}).then((res) => res.ok);
});
// Update amount dynamically (e.g. after quantity change)
elements.updateAmount(59.98);Charge the payload within a couple of minutes
data.paysioWallet contains a single-use encrypted credential that expires quickly — charge it from your server immediately and never store it.
The wallet button can stand on its own
We recommend letting the wallet button work on its own — like Paysio's hosted checkout, where you click Apple Pay / Google Pay and pay in one step. You generally don't need to require your own name / email / billing-address fields before the wallet button, since the wallet sheet already collects the buyer's name, email, phone, and full billing address (Paysio.js requests them — Apple Pay requiredBillingContactFields + contactFields; Google Pay billingAddressRequired + emailRequired). Those details come back on data.wallet (data.wallet.billingInfo holds the address). Your billing form is typically only needed for the manual card path, so you can keep the two independent.
Charge it
On your server, forward data.paysioWallet verbatim as paysio_wallet, plus data.wallet as wallet. Paysio decrypts the wallet token, charges it on your card gateway, and records the wallet's authentication evidence (ECI, cryptogram) on the transaction. Forwarding wallet is what attaches the buyer: Paysio find-or-creates the customer and fills in the transaction (name, email, billing address, card last-4) — exactly like hosted checkout.
// Charge the wallet payload AND forward the buyer contact
const charge = await fetch('https://paysio.com/api/v1/charges', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_secret_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
paysio_wallet: body.paysio_wallet, // data.paysioWallet, forwarded verbatim
wallet: body.wallet, // data.wallet — buyer contact
three_ds: body.three_ds, // data.threeDS — only when a step-up ran
amount: 2999,
currency: 'USD',
}),
});paysio_walletcannot be combined withpayment_token, agatewayoverride, theachrail, or 3DS fields — the wallet credential is the complete payment instrument and (for device tokens) carries its own authentication.- A reusable Google Pay card is saved to the buyer's payment methods automatically after an approved charge. Apple Pay tokens are single-use by design and are not saved.
- If you omit both
walletandemail, the charge still succeeds but has no buyer attached (no customer created, no card saved).
Dynamic totals and discount codes
The amount lives in two places with different jobs. The wallet sheet shows whatever mountWallets({ amount }) / elements.updateAmount() last set — call updateAmount(newTotal) whenever the cart changes (quantities, a discount code applying, shipping added); both wallets read the live amount at click time, no re-mount needed. The charge takes the amount your server passes to POST /charges — that value is authoritative; the sheet never sets the charge amount.
So a discount flow is: validate the code on your server → updateAmount(discountedTotal) so the sheet shows it → charge the discounted cents server-side. Compute the total on the server both times; never trust a total sent up from the browser. (Paysio's own discount_code handling applies to hosted Checkout Sessions — on direct API charges discounts are your logic.)
Shipping addresses
Pass collectShipping: true to mountWallets() and both wallet sheets collect a shipping address (plus phone). It comes back as data.wallet.shippingInfo, and forwarding wallet to POST /charges stores it on the transaction's shipping fields and the buyer's profile automatically — nothing else to wire. The sheet's billing address (data.wallet.billingInfo) is likewise consumed automatically: AVS, the transaction, the customer, and any card saved to the vault.
3D Secure and wallet payments
- Apple Pay (always) and Google Pay on Android produce a device token with a single-use cryptogram — the buyer already authenticated with Face ID / fingerprint / passcode. That is the wallet's equivalent of a completed 3DS authentication: fraud-dispute liability shifts to the issuer, and Paysio records the evidence on the transaction (wallet type, ECI, cryptogram). Running 3DS on top is neither possible nor needed — the API rejects
paysio_walletcombined with 3DS fields by design. - Google Pay on desktop/iOS (PAN_ONLY) returns the buyer's real card number — an ordinary card-not-present charge with no authentication of its own, like a typed card. Paysio.js steps these up automatically when 3D Secure is on for your workspace: after the Google sheet closes it prepares the payment and, if a step-up applies, runs the standard challenge popup before your handler is called.
- Forward the result. When the popup ran, the event carries
data.threeDS— send it asthree_dson the charge to get the liability shift. Omitting it still charges, just unauthenticated.three_dsis accepted only with a prepared Google Pay payload; device tokens refuse it. - Requires 3D Secure enabled for the workspace and the Paysio vault on (the challenge runs against a vaulted card). If either is off, or the brand has no acquirer profile, the wallet charges normally with no step-up — the prepare response's
three_ds_reasonnames which condition disabled it (three_ds_disabled= turn on 3D Secure in Settings > Checkout). - Which kind you received is visible on the transaction: device tokens carry
wallet_eci+ cryptogram evidence; a stepped-up PAN_ONLY charge carries the usualthree_ds_*fields instead. - Driving it yourself:
POST /wallets/preparereturns{ prepared, three_ds_required, three_ds_reason, card_id, card }. Authenticatecard_idwith the normal 3DS endpoints, then chargepaysio_wallet: { type, prepared }+three_ds. The sealed blob is single-use, workspace-bound and expires in 15 minutes.
Apple Pay domain registration
In live mode, Apple verifies every domain that shows the Apple Pay button. paysio.com and your workspace's custom domain are registered automatically — you only need this when embedding the button on your own site. Test mode skips domain verification entirely.
- Download the association file from
https://paysio.com/.well-known/apple-developer-merchantid-domain-association - Host it unmodified on your domain at exactly
/.well-known/apple-developer-merchantid-domain-association(over HTTPS). - Register the domain (Paysio verifies the file, then registers it with Apple):
// Register (secret key). Paysio checks the association file first.
await fetch('https://paysio.com/api/v1/wallets/apple-pay/domains', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_your_secret_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain: 'shop.example.com' }),
});
// List: GET https://paysio.com/api/v1/wallets/apple-pay/domains
// Remove: DELETE https://paysio.com/api/v1/wallets/apple-pay/domains/shop.example.comDomains can also be managed in the dashboard under Settings → Apple Pay domains (in the Developers group), which includes a download button for the association file. Merchant validation itself (POST /wallets/apple-pay-session) is called by Paysio.js automatically with your publishable key — you never call it directly.
Testing wallets
Mount the buttons exactly as you would in live mode — there is nothing extra to add for sandbox. A test-mode key puts both wallets in their own sandbox, and the payment runs through the same charge pipeline against a test card.
Google Pay: nothing special. In test mode the Google sheet serves Google's canned test cards to any signed-in Google account — click the button and pay.
Apple Pay sandbox setup
Apple's sandbox needs a one-time setup with a sandbox Apple ID:
- In App Store Connect, create a Sandbox Tester (Users and Access → Sandbox → Testers). Any never-used email works — a plus-address like
[email protected]is fine. Any Apple developer account can mint testers. - On the iPhone, sign into iCloud with that sandbox Apple ID in Settings (the top slot — the App Store “Sandbox Account” slot does not apply to Wallet).
- Add Apple's universal test cards to Wallet from
developer.apple.com/apple-pay/sandbox-testing. - Pay on any test-mode page. Non-Safari browsers show Apple's QR code — the iPhone that scans it is the one that needs the sandbox account.
| Network | Number | Expiry | Security code |
|---|---|---|---|
| Visa | 4622 9431 2318 9285 | 12/2028 | 096 |
| Mastercard | 5204 2452 5046 0049 | 01/30 | 111 |
| Amex (US) | 3727 3572 3032 000 | 12/28 | 7777 |
| Discover | 6011 0009 9446 2780 | 01/30 | 111 |
HTML layout
<!-- Wallet buttons (Apple Pay / Google Pay) -->
<div id="wallet-buttons"></div>
<div style="text-align: center; color: #94a3b8; margin: 12px 0;">— or pay with card —</div>
<!-- Your own card form -->
<form id="payment-form">
<input type="text" id="card-number" placeholder="Card number" />
<input type="text" id="card-exp" placeholder="MM / YY" />
<input type="text" id="card-cvv" placeholder="CVV" />
<button type="submit">Pay $29.99</button>
</form>Button appearance
Paysio.js renders the official Apple Pay and Google Pay buttons, and every customization knob Apple and Google offer passes straight through mountWallets(). No overlay or skin tricks needed — both brands' guidelines require the genuine button appearance, and the official variants cover the full sanctioned range: colors, labels, corner radius, locale, and size.
elements.mountWallets('#wallet-buttons', {
amount: 29.99,
currency: 'USD',
country: 'US',
// Layout of the two buttons inside your container
buttonHeight: 48, // px (or CSS string) — both buttons. Default 40.
gap: 8, // px between the buttons. Default 8.
direction: 'column', // 'column' (stacked, default) or 'row' (side by side)
// Apple Pay — the official <apple-pay-button> options
applePay: {
buttonStyle: 'black', // 'black' (default) | 'white' | 'white-outline'
buttonType: 'buy', // label — full list below. Default 'buy'.
locale: 'en-US', // e.g. 'fr-FR' — full list below
borderRadius: 8, // px or CSS string
height: 48, // Apple-only override of buttonHeight. Min 30px.
padding: '0px 0px',
},
// Google Pay — the official createButton() ButtonOptions
googlePay: {
buttonColor: 'black', // 'default' (Google picks) | 'black' | 'white'
buttonType: 'buy', // label — full list below. Default 'buy'.
buttonRadius: 8, // px, 0 to half the height. Google default 4.
buttonLocale: 'en', // ISO 639-1, defaults to the browser language
buttonSizeMode: 'fill', // 'fill' (default) | 'static' (Google's fixed size)
buttonBorderType: 'default_border', // or 'no_border'
personalized: true, // false = never show Google's card preview (see below)
height: 48, // Google-only override (fill mode)
},
});What each buttonType renders
The wallets add their own wording around their logo — pick by the literal label, not the name. “Just logo + Pay” is plain on both wallets.
| Type | Apple renders | Google renders | Personalizes? |
|---|---|---|---|
| plain | Apple logo + “Pay” (the Apple Pay mark alone) | The G Pay mark alone | Yes |
| buy | “Buy with (Apple logo) Pay” | “Buy with G Pay” | Yes |
| pay | “Pay with (Apple logo) Pay” | “Pay with G Pay” | Yes |
| check-out / checkout | “Check out with (Apple logo) Pay” | “Checkout with G Pay” | No |
| order / donate / subscribe / book | “<Verb> with (Apple logo) Pay” | “<Verb> with G Pay” | No |
Apple supports a further set that Google has no equivalent for — continue, contribute, support, tip, rent, reload, add-money, top-up, and set-up. Each renders as “<Verb> with (Apple logo) Pay”, and none of them personalize.
Google's card preview
When the signed-in buyer has an eligible saved card, Google replaces the buy/pay/plain label with a preview of their card (network + last 4). That is Google's behavior, not Paysio's, and Google offers no per-type flag to disable it — but it is driven by a payment-method hint Paysio passes to the button. Set googlePay.personalized: false and Paysio withholds that hint, so those labels always render their generic form. Payment behavior is unchanged. Apple Pay has no personalization concept.
Recipes
// Logo-only pair, side by side, no extra wording, no card preview
elements.mountWallets('#wallet-buttons', {
amount: 29.99, direction: 'row', buttonHeight: 44,
applePay: { buttonType: 'plain', borderRadius: 10 },
googlePay: { buttonType: 'plain', personalized: false, buttonRadius: 10 },
});
// "Buy with ..." pair matching a 12px-radius card button
elements.mountWallets('#wallet-buttons', {
amount: 29.99, buttonHeight: 48,
applePay: { buttonType: 'buy', borderRadius: 12 },
googlePay: { buttonType: 'buy', buttonRadius: 12 },
});
// Keep Google's card preview (higher conversion for returning buyers) — the default
elements.mountWallets('#wallet-buttons', { amount: 29.99 });Supported locales
| Option | Values |
|---|---|
| applePay.locale | ar-AB, ca-ES, cs-CZ, da-DK, de-DE, el-GR, en-AU, en-GB, en-US, es-ES, es-MX, fi-FI, fr-CA, fr-FR, he-IL, hi-IN, hr-HR, hu-HU, id-ID, it-IT, ja-JP, ko-KR, ms-MY, nb-NO, nl-NL, pl-PL, pt-BR, pt-PT, ro-RO, ru-RU, sk-SK, sv-SE, th-TH, tr-TR, uk-UA, vi-VN, zh-CN, zh-HK, zh-TW |
| googlePay.buttonLocale | en, ar, bg, ca, cs, da, de, el, es, et, fi, fr, hr, id, it, ja, ko, ms, nl, no, pl, pt, ru, sk, sl, sr, sv, th, tr, uk, zh |
- Minimums (brand guidelines): Apple requires at least 30px height and 140px width; Google's static button is 40px tall. 40–48px reads most native.
- Google
buttonRadiuscaps at half the button height (max 20 on a 40px button). - Matching radii: set
applePay.borderRadiusandgooglePay.buttonRadiusto your card button's radius for a consistent row. buttonColor: 'default'lets Google match the user's theme; Apple has no equivalent — pickbuttonStyleper your background (white-outlinefor white-on-white).- Both wallets can render on the same device (Apple Pay works beyond Safari via the QR handoff), so
direction: 'row'gives a balanced two-button row. - An unavailable wallet collapses instead of leaving a dead box (e.g. Google Pay hides when
isReadyToPayis false).