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

JavaScript
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.

JavaScript
// 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_wallet cannot be combined with payment_token, a gateway override, the ach rail, 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 wallet and email, 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_wallet combined 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 as three_ds on the charge to get the liability shift. Omitting it still charges, just unauthenticated. three_ds is 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_reason names 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 usual three_ds_* fields instead.
  • Driving it yourself: POST /wallets/prepare returns { prepared, three_ds_required, three_ds_reason, card_id, card }. Authenticate card_id with the normal 3DS endpoints, then charge paysio_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.

  1. Download the association file from https://paysio.com/.well-known/apple-developer-merchantid-domain-association
  2. Host it unmodified on your domain at exactly /.well-known/apple-developer-merchantid-domain-association (over HTTPS).
  3. Register the domain (Paysio verifies the file, then registers it with Apple):
JavaScript
// 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.com

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

  1. 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.
  2. 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).
  3. Add Apple's universal test cards to Wallet from developer.apple.com/apple-pay/sandbox-testing.
  4. 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.
NetworkNumberExpirySecurity code
Visa4622 9431 2318 928512/2028096
Mastercard5204 2452 5046 004901/30111
Amex (US)3727 3572 3032 00012/287777
Discover6011 0009 9446 278001/30111

HTML layout

HTML
<!-- 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.

JavaScript
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.

TypeApple rendersGoogle rendersPersonalizes?
plainApple logo + “Pay” (the Apple Pay mark alone)The G Pay mark aloneYes
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

JavaScript
// 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

OptionValues
applePay.localear-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.buttonLocaleen, 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 buttonRadius caps at half the button height (max 20 on a 40px button).
  • Matching radii: set applePay.borderRadius and googlePay.buttonRadius to your card button's radius for a consistent row.
  • buttonColor: 'default' lets Google match the user's theme; Apple has no equivalent — pick buttonStyle per your background (white-outline for 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 isReadyToPay is false).