Use the payment request button
The Payment Request Button is a single frame-js element that gives customers a one-click checkout through their device's stored wallet — Apple Pay on Apple devices, Google Pay on Chrome. Frame.js handles the wallet detection automatically: if the customer's device + browser supports a wallet, the button appears; if not, you can fall back to the standard Card element.
The customer flow: they tap the button, the wallet UI pops up with their stored cards and shipping addresses, they confirm via biometric (Face ID, fingerprint, etc.), and Frame produces a payment method record on your backend. No card-data entry, no autofill friction, materially higher conversion on mobile.
Prerequisites
| Requirement | Details |
|---|---|
| HTTPS | Apple Pay and Google Pay both require HTTPS. Use ngrok or similar for local dev. |
| Domain registered with Frame | Each domain (and subdomain) where the button appears must be registered with Frame support in both sandbox and live modes. |
| Apple Pay enabled | Apple Pay isn't on by default — contact Frame support to enable it for your account. |
| Apple domain verification | Apple requires a one-time domain verification file at /.well-known/apple-developer-merchantid-domain-association. Frame provides the file; you host it. |
| frame-js loaded | Required for the button to render. |
Domain registration + Apple verification is the most painful part of the setup. Loop in Frame support early — it's a one-time setup but has a few back-and-forth steps.
Customer requirements
The button only renders when the customer's device supports a wallet:
| Wallet | Supported on |
|---|---|
| Apple Pay | Safari on macOS 10.13+ or iOS 11.3+; cardholder must have a card in Wallet |
| Google Pay | Chrome 61+ on Android, macOS, Windows, or Linux; cardholder must have a card in Google Pay |
Some regions (notably India) restrict one or both wallets — check device + region eligibility before assuming the button will appear.
Verify your domain with Apple
Before Apple Pay can be displayed to customers on your website, Apple requires you to verify that you own the domain. This is a one-time setup step per domain.
Every domain where you want to display Apple Pay or Google Pay must be registered with Frame and verified with the respective payment provider. This includes subdomains (e.g., checkout.example.com and example.com are treated as separate domains). If you change or add domains such as moving your checkout from app.example.com to example.com - you must notify Frame support before the change so we can register the new domain. Apple Pay and Google Pay buttons will not appear on unregistered domains.
To verify your domain:
- Download the
apple-developer-merchantid-domain-associationfile. - Host the file at the following URL on each domain and subdomain where you want to use Apple Pay:The file must be served with no file extension and accessible without redirects.
https://<yourdomain>/.well-known/apple-developer-merchantid-domain-association - Notify Frame support once the file is in place so your domain can be registered with Apple.
Once a domain is verified, you must ensure that the validation token is always available at the specified URL. If the validation token is not available when Apple periodically attempts to retrieve it, Apple will not be able to verify the domain, and Apple Pay will not work on your website.
1. Load frame-js
Same as any frame-js integration: load from Frame's CDN, never a local copy.
<script src="https://js.framepayments.com/v1/index.js"></script>
const frame = await Frame.init('pk_sandbox_your_publishable_key');
2. Define the payment request
A paymentRequest describes what the customer is being charged for — currency, amount, what info to collect. Create one before the button:
const paymentRequest = frame.paymentRequest({
country: 'US',
currency: 'usd',
total: {
label: 'Order #1042',
amount: 5000 // $50.00 in cents
},
requestPayerName: true,
requestPayerEmail: true,
requestShipping: false
});
A few options worth noting:
total.amountis the charge amount in the smallest currency unit (cents for USD). Frame uses this as the charge amount unless you override at confirmation time.requestPayerNamecollects the cardholder name from the wallet — useful for AVS + dispute defense.requestPayerEmailcollects the email (most customers' wallets have this on file).requestShipping: trueopens the wallet's shipping selector with whatever addresses the customer has stored. The selected address comes back on the event.
3. Detect support + mount the button
The button only renders if the wallet can be used. Check first:
<div id="payment-request-button"></div>
const prButton = await frame.createElement('paymentRequestButton', { paymentRequest });
const supportResult = await paymentRequest.canMakePayment();
if (supportResult?.applePay || supportResult?.googlePay) {
prButton.mount('#payment-request-button');
} else {
// No wallet available — fall back to Card element or hide entirely
document.getElementById('payment-request-button').style.display = 'none';
}
This pattern is the right shape for mixed customer bases: the wallet button shows for customers whose devices support it; everyone else sees your standard checkout.
4. Handle the payment-confirmation event
When the customer taps the button and authenticates, the paymentRequest fires a paymentmethod event with the customer's selected card + the wallet-provided details. Handle it to create the charge on your backend:
paymentRequest.on('paymentmethod', async (event) => {
// event.paymentMethod is the Frame PaymentMethod created from the wallet credential
// event.payerName, event.payerEmail are the customer's wallet-provided info
const response = await fetch('/checkout', {
method: 'POST',
body: JSON.stringify({
paymentMethodId: event.paymentMethod.id,
payerName: event.payerName,
payerEmail: event.payerEmail,
sonarSessionId: localStorage.getItem('frame_charge_session_id')
})
});
const { success, requires3ds, clientSecret } = await response.json();
if (success) {
event.complete('success');
} else if (requires3ds) {
event.complete('success'); // close the wallet UI
await frame.confirmCardPayment(clientSecret);
} else {
event.complete('fail');
}
});
Here clientSecret is the client_secret your backend returned from the transfer it created — on a Transfer flow that value is the underlying ChargeIntent's ci_ secret, which is exactly what confirmCardPayment expects. See Handle 3D Secure for why the SDK helper is named after the ChargeIntent rather than the Transfer.
event.complete() is what closes the wallet UI on the customer's device — call it with 'success' to dismiss the loading state with a checkmark, 'fail' to dismiss with an error. The customer sees this transition immediately; latency between complete() and your post-payment UI matters for perceived performance.
5. Create the charge server-side
Your backend creates the transfer normally, using the payment method ID from the wallet event:
curl --request POST \
--url https://api.framepayments.com/v1/transfers \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"amount": 5000,
"currency": "USD",
"account_id": "<account_id>",
"source_payment_method_id": "<payment_method_id from the wallet event>",
"sonar_session_id": "<sonar session>",
"description": "Order #1042"
}'
Wallet-sourced charges run through the same Sonar + 3DS evaluation as any other card charge. Apple Pay and Google Pay use device-bound tokens (not the underlying card number), which means dispute defense is materially stronger — the cryptogram is built into the wallet's authorization.
Common variations
Wallet + Card together. Render both the Payment Request Button and the Card element on the same page. Customers with wallet support see the button; everyone else uses the Card element. Don't make customers choose between paths.
Dynamic amounts. Update the paymentRequest.total if the cart changes between page load and the customer tapping the button. Call paymentRequest.update({ total: { ... } }) to reflect the new amount in the wallet UI.
Shipping address from wallet. Set requestShipping: true and Frame returns the selected shipping address on the paymentmethod event. You can also update available shipping methods dynamically based on the address via the shippingaddresschange event.
Multiple buttons per page. If you have multiple "pay" actions on a page (e.g., one per cart item), create a separate paymentRequest and button per action. They don't share state.
Testing
Use Apple Pay's sandbox test cards in iCloud Wallet to test in development, and Google Pay's test environment for Google Pay. Frame's sandbox accepts wallet-sourced payment methods normally.
For browsers without wallet support during dev, use the Card element instead — the button won't render anyway.
Gotchas
Symptom: the Payment Request Button doesn't appear in production despite working in development. Why: the production domain isn't registered with Frame and/or Apple. Domain registration is per-domain (subdomains count as separate domains). Fix: check Frame's dashboard for registered domains; loop in Frame support to register any new ones. Verify the Apple apple-developer-merchantid-domain-association file is hosted at the right URL with no redirects.
Symptom: the button appears but tapping it shows "Payment not supported." Why: the customer has no card in their wallet, or the card is from a region where Apple Pay / Google Pay isn't supported. Fix: this is expected — surface a fallback to the Card element.
Symptom: paymentmethod event fires but the wallet UI doesn't close after event.complete('success'). Why: you didn't call complete(), or you called it with a status the wallet rejects. Fix: complete() must be called exactly once per event, with 'success' or 'fail'. Calling it twice or with an invalid status leaves the wallet in a stuck state.
Symptom: the payment succeeded but the Sonar session ID was empty on the charge. Why: the wallet flow bypassed your normal page load, so localStorage may not have a session yet. Fix: initialize frame-js (and therefore Sonar) early in the page lifecycle, not lazily right before the wallet event.
Next steps
- Build a custom payment page — pair the Payment Request Button with the Card element for full coverage
- Handle 3D Secure — completing 3DS when Frame's engine triggers it on a wallet-sourced charge
- Sonar — fraud signal coverage on wallet-sourced charges