Payment Request Button
The Payment Request Button gives your customers a fast, single-click checkout experience. It's a single integration that dynamically shows wallet options like Apple Pay, allowing customers to pay using payment details securely stored on their device.
Note: Currently, the Payment Request Button only supports Apple Pay. We'll update you when more options become available.
Prerequisites
Before you can display the Payment Request Button, you must meet the following requirements:
- Your application must be served over HTTPS. This is a browser security requirement for both development and production. Services like ngrok can be used to create a secure tunnel to your local server for testing.
- You must register your domain with us for both test (sandbox) and live modes. Please contact Frame support to complete this step.
- Apple Pay Requirements:
- Your customer must be using a supported device and browser, such as Safari on macOS 10.13+ or iOS 11.3+.
- They must have a card added to their Wallet.
- Note that Apple Pay may not display for users in certain regions, such as India.
Set up Frame Elements
First, add the Frame.js script to your checkout page. Always load it directly from js.framepayments.com
to remain PCI compliant.
<script src="https://js.framepayments.com/v1/index.js"></script>
Next, create a container DOM node for the button on your page and initialize Frame with your publishable key.
<div id="payment-request-button">
<!-- A Frame Element will be inserted here. -->
</div>
const frame = await Frame.init("<PUBLISHABLE_KEY>");
Create a paymentRequest instance
Create a paymentRequest
instance, providing the payment details such as currency and the total amount.
const paymentRequest = frame.paymentRequest({
country: "US",
currency: "USD",
total: {
label: "Demo (Card is not charged)"
amount: 990,
},
requestPayerName: true,
requestPayerEmail: true
});
Use the requestPayerName
parameter to collect the payer's billing address for Apple Pay. You can use the billing address to perform address verification and block fraudulent payments. All other payment methods automatically collect the billing address when one is available.
Create and mount the paymentRequestButton
Check if the customer's device and browser support the Payment Request Button by calling paymentRequest.canMakePayment()
. If it returns wallet is supported, create and mount the paymentRequestButton
Element to the container you created earlier. If not, hide the container and consider showing your standard card input form instead.
(async () => {
const prButton = await frame.createElement("paymentRequestButton", { paymentRequest });
// Check the availability of the Payment Request API first.
const result = await paymentRequest.canMakePayment();
if (result.applePay) {
prButton.mount('#payment-request-button');
} else {
document.getElementById('payment-request-button').style.display = 'none';
}
})();
Create a ChargeIntent
Frame uses a ChargeIntent object to represent your intent to collect payment from a customer, tracking charge attempts and payment state changes throughout the process.
Create a ChargeIntent
on your server with an amount and currency. Always decide how much to charge on the server side, a trusted environment, as opposed to the client. This prevents malicious customers from being able to choose their own prices.
curl --request POST \
--url https://api.framepayments.com/v1/charge_intents \
--header 'Authorization: Bearer API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"amount": 990,
"currency": "usd"
}'
The API response for the ChargeIntent includes a client_secret
. Return this client_secret
to the client-side to securely confirm the payment.
Complete the payment
Listen for the paymentmethod
event on your paymentRequest object. This event fires after the customer authenticates the payment with their device (e.g., using Face ID or Touch ID). The event handler provides a PaymentMethod object.
Use the client_secret
from the ChargeIntent and the id
from the new PaymentMethod
to confirm the payment with frame.confirmCardPayment
. Finally, call ev.complete()
with the status ("success"
or "fail"
) to close the browser's payment interface and provide feedback to the customer.
paymentRequest.on("paymentmethod", async (ev) => {
const { hasError } = await frame.confirmCardPayment(clientSecret, { payment_method: ev.paymentMethod.id });
if (hasError) {
// Report to the browser that the payment failed, prompting it to
// re-show the payment interface, or show an error message and close
// the payment interface.
ev.complete("fail");
} else {
// Report to the browser that the confirmation was successful, prompting
// it to close the browser payment method collection interface.
ev.complete("success");
}
});
Caution: The customer can dismiss the payment interface in some browsers even after they authorize the payment. This means that you might receive a cancel
event on your PaymentRequest object after receiving a paymentmethod
event. If you use the cancel event as a hook for canceling the customer's order, make sure you also refund the payment that you just created.
Test your integration
To test your integration, you must use HTTPS and a supported browser. In addition, payment method and browser has specific requirements:
Safari
- Safari on Mac running macOS Sierra or later
- A compatible device with a card in its Wallet paired to your Mac with Handoff, or a Mac with TouchID. You can find instructions on the Apple Support site.
- A registered domain with Apple Pay.
Mobile Safari
- Mobile Safari on iOS 11.3 or later
- A card in your Wallet (go to Settings > Wallet & Apple Pay).
- A registered domain with Apple Pay.
As of iOS 16, Apple Pay might work in some non-Safari mobile browsers with a card saved in your Wallet.
Collect shipping information
Enable shipping collection to gather customer delivery addresses and offer shipping options directly within the payment interface. This streamlined approach reduces checkout friction while ensuring you collect all necessary information for order fulfillment.
When you enable requestShipping: true
, customers can select from their saved addresses or enter a new one, and you can dynamically calculate shipping costs based on their location.
Set up shipping collection
Begin by including requestShipping: true
when creating the payment request. You can provide initial shipping options if they don't depend on the customer's specific address:
const paymentRequest = frame.paymentRequest({
country: "US",
currency: "USD",
total: {
amount: 2099,
label: "Demo"
},
requestShipping: true,
shippingOptions: [
{
id: "free-shipping",
label: "Free shipping",
detail: "Arrives in 5 to 7 days",
amount: 0,
},
{
id: "express",
label: "Express Shipping",
detail: "Arrives in 2-3 business days",
amount: 1299,
}
];
});
Handle address changes and validation
Listen for the shippingaddresschange
event to validate addresses and update shipping options based on the customer's location. The address data is anonymized by the browser for privacy until the customer completes their purchase.
paymentRequest.on("shippingaddresschange", async (event) => {
const { shippingAddress, updateWith } = event;
if (shippingAddress.country !== "US") {
updateWith({ status: "invalid_shipping_address" });
} else {
// Perform server-side request to fetch shipping options
const response = await fetch("/shipping/calculate", {
data: JSON.stringify({ shippingAddress: shippingAddress })
});
const result = await response.json();
updateWith({
status: "success",
shippingOptions: result.supportedShippingOptions,
});
}
});
Address validation best practices:
- Use the anonymized address data responsibly - only collect what's needed for shipping calculations
- Provide clear error messages when shipping isn't available to specific locations
- Consider offering alternative delivery methods (like local pickup) when standard shipping isn't available
- Always validate shipping costs server-side to prevent manipulation
Handle shipping option selection
When customers change their shipping preference, update the total and display items accordingly:
paymentRequest.on("shippingoptionchange", async (event) => {
const { shippingOption, updateWith } = event;
// Recalculate total with new shipping cost
const subtotal = 1999;
const tax = 160; // Previously calculated tax
const newTotal = subtotal + tax + shippingOption.amount;
updateWith({
status: "success",
total: {
amount: newTotal,
label: "Total"
},
displayItems: [
{ label: "Premium Course", amount: subtotal },
{ label: "Tax", amount: tax },
{ label: shippingOption.label, amount: shippingOption.amount }
]
});
});
Display line items
Use displayItems
to provide customers with a transparent breakdown of their purchase in the browser's payment interface. This creates trust and helps reduce cart abandonment by showing exactly what customers are paying for before they complete their transaction.
Display items appear as a detailed list in the payment sheet, showing individual costs like products, taxes, shipping, and discounts. This transparency is especially important for complex purchases where customers want to understand the total calculation.
const paymentRequest = frame.paymentRequest({
country: "US",
currency: "USD",
total: {
amount: 2000,
label: "Total"
},
displayItems: [
{
label: "Premium Course",
amount: 1000,
},
{
label: "Express Shipping",
amount: 1000,
}
],
});
Best practices for line items:
- Use clear, descriptive labels that customers will recognize
- Keep labels concise but informative (avoid generic terms like "Item 1")
- Ensure all displayItems sum to your total amount for consistency
Updating display items dynamically
You can update display items in real-time when customers make changes, such as selecting different shipping options:
// Update display items when shipping changes
paymentRequest.on("shippingoptionchange", async (event) => {
const { shippingOption, updateWith } = event;
// Calculate new totals based on selected shipping
const shippingCost = getShippingCost(shippingOption.id);
const subtotal = 1999;
const newTotal = subtotal + shippingCost;
updateWith({
status: "success",
total: {
amount: newTotal,
label: "Total"
},
displayItems: [
{
label: "Premium Course Access",
amount: subtotal,
},
{
label: shippingOption.label,
amount: shippingCost,
}
],
});
});
Set up recurring payments
The Payment Request Button supports recurring payments by requesting an Apple Pay MPAN, which enables you to process merchant-initiated transactions (MIT) for subscription-based services.
Note: Currently, the Payment Request Button only supports recurring payments for Apple Pay transactions. Auto-load and deferred payment options are not yet available. We'll update you when more options become available.
Create a recurring paymentRequest instance
When setting up recurring payments, modify your paymentRequest
configuration to include the applePay
object with recurring payment details:
const paymentRequest = frame.paymentRequest({
country: "US",
currency: "USD",
total: {
amount: 1000,
label: "Monthly Subscription"
},
requestPayerName: true,
requestPayerEmail: true,
applePay: {
recurringPaymentRequest: {
paymentDescription: "Monthly subscription to Premium Service",
managementURL: "https://yoursite.com/manage-subscription",
billingAgreement: "You will be charged $10 monthly for access to premium content. This subscription will automatically renew until canceled.",
regularBilling: {
amount: 1000,
label: "Premium Course",
recurringPaymentStartDate: new Date(),
recurringPaymentIntervalUnit: "monthly"
}
}
}
});
The managementURL
must be a valid HTTPS URL where customers can manage their recurring payment. Apple requires this for all recurring payment requests.
Recurring payment parameters
Configure your recurring payment request with these parameters:
Parameters
A description of the recurring payment that Apple Pay displays to the user in the payment sheet.
A URL to a web page where customers can manage their subscription.
A localized billing agreement that the payment sheet displays to the user before the user authorizes the payment.
Object containing the recurring payment details.
The recurring charge amount in cents.
Description of the recurring charge.
The date of the first payment.
Frequency unit daily
, monthly
, weekly
, yearly
, every_3_months
, or every_6_months
.
Handle recurring payment authorization
When processing recurring payments, the payment flow remains the same:
paymentRequest.on("paymentmethod", async (ev) => {
// send the PaymentMethod ID to the backend to create a subscription
// return the ChargeIntent clientSecret and the status
const { clientSecret, status } = await create_subscription(ev.paymentMethod.id)
if (status === "succeeded") {
// subscription was successful, prompting
// it to close the browser payment method collection interface.
ev.complete("success");
} else {
// Check if the ChargeIntent requires any actions. If so, let Frame.js handle the flow.
const { hasError } = await frame.confirmCardPayment(clientSecret);
if (hasError) {
// The payment failed -- ask your customer for a new payment method.
ev.complete("fail");
} else {
// The payment has succeeded -- show a success message to your customer.
ev.complete("success");
}
}
});
Example Implementation
For a complete working example of Frame payment integration, explore our sample application:
We maintain a comprehensive example application that demonstrates best practices for Frame payment integration.