headless=true to run the same purchase with no button. The frame renders
nothing and starts the purchase as soon as it loads, so your confirmation
screen, loading states, and purchase button stay fully under your control. Use
headless mode for card and bank transfer quotes when you draw
your own button.
The Buy Button frame replaces the Apple Pay, Google Pay, and Buy frames. If
you load
/platform/v1/apple-pay or /platform/v1/google-pay, MoonPay serves
this frame. If you load /platform/v1/buy, MoonPay serves this frame in
headless mode. Existing integrations keep working, and the events below apply
to them.URL
Payment methods
The quote’spaymentMethod.type decides which mode to use. Card works in both
modes. SEPA bank transfer has no button to tap, so it runs in headless mode.
Apple Pay and Google Pay open a payment sheet from the customer’s tap, so they
run with the button.
If the quote’s payment method doesn’t run in the mode you load, the frame sends
error with unsupportedPaymentMethod. Guest checkout runs with the
button only. Card and bank transfer quotes need a connected customer, not a
guest. See Pay with card and Pay with bank
transfer.
Requirements
Size
With the button, the frame container height must be 44px. Width is flexible; the button inside the frame uses 100% of the container width. If you set a customheight through
applePayButtonSettings or
googlePayButtonSettings, size the frame
container to match.
In headless mode, the frame renders nothing. Mount it with
zero dimensions so it does not affect your layout:
Example
Permissions
Thepayment permission policy is required for Apple Pay and Google Pay. If you use the MoonPay web SDK to render the frame, it sets this for you; the guidance below applies to manual integrations, where you build the iframe yourself.
Example
window.prompt dialog, for both Apple Pay and Google
Pay. If your iframe uses the
sandbox
attribute, include allow-modals.
Sandboxed iframe (PCI DSS v4)
If you render the frame in a sandboxed iframe (for example, for PCI DSS v4 compliance), includeallow-scripts allow-popups allow-same-origin allow-forms. Sandboxing is one way to isolate the frame when Google Pay’s pay.js cannot provide a stable Subresource Integrity hash. See Google Pay inside sandboxed iframe for PCI DSS v4 compliance for details.
When you use the sandbox attribute, each value serves a specific purpose:
allow-scriptsrunspay.js.allow-popupsopens the payment window on user interaction.allow-same-origingrants the browser storage and cookie access needed for compatibility.allow-formssubmits the Google Pay sign-in form.allow-modalsshows the test-mode payment dialog (window.prompt).
Mobile WebViews
Rendering the frame inside a mobile WebView has additional requirements:- Apple Pay in an iOS
WKWebView: handle the JavaScript dialogs the frame uses. See WKWebView. - Google Pay in an Android WebView: needs the AndroidX WebKit dependency, intent queries, the Payment Request API enabled, and an approved app. See Android WebView.
- Google Pay in an iOS
WKWebView: not available. Offer another payment method. See iOS WKWebView.
WKWebView
In a native iOS app, you render the frame inside aWKWebView. The frame communicates with the host app through the postMessage-based frames protocol. See Events below.
You must handle JavaScript dialogs. The frame uses window.prompt to present the simulated Apple Pay sheet in test mode. By default a WKWebView silently dismisses alert, confirm, and prompt: window.prompt returns null with no UI shown, and the frame reads that as the customer cancelling, so the transaction comes back with status: "failed".
To fix this, conform your view controller to WKUIDelegate, set webView.uiDelegate, and present the text input panel, for example as a UIAlertController with a text field:
nil declines it.
Wire this even if you only ship live mode, since the default behavior applies to any dialog the frame surfaces.
For a complete native walkthrough that covers rendering the frame, wiring the bridge and events, and the full dialog handler, see iOS manual integration.
Initialization parameters
Headless mode
Passheadless=true when you draw your own purchase button. The frame renders
nothing: after the handshake, it sends ready and starts
the purchase straight away. Mount the frame when the customer confirms the
purchase in your UI, not before, because mounting it is what starts the
purchase.
Headless mode runs the same steps as a tap on the card button, and uses the
same events, with these differences:
- It accepts card and SEPA bank transfer quotes only. See Payment methods.
- It needs a connected customer. A guest checkout quote gets
errorwithinvalidQuote. - It never sends
buttonPressedorcancelled. - After
ready, it sends exactly one final event:complete,challenge, orerror. Every quote that decodes getsreadyfirst, including one the frame then rejects withunsupportedPaymentMethodorinvalidQuote. - It reads
setQuoteonly after anerrorwithquoteExpired. The new quote starts a new attempt. At any other time the frame ignoressetQuote, so it never starts a second purchase. - On
challenge, open the challenge frame withoutresume. The challenge frame finishes the purchase and sends its owncomplete.
Bank transfer
For a SEPA bank transfer quote, the frame creates the transaction, but the customer still has to send the money from their bank. MoonPay doesn’t show the bank details, so you do:- Read the transaction
idfromcomplete. - Fetch the transaction with that
idfrom the transaction endpoint, or withclient.getTransaction()in the SDK. - Show the customer the bank details from
bankTransferDepositInfoand the amount to send fromsource.amount.
reference from
bankTransferDepositInfo with their transfer, or it is rejected. See Pay with
bank transfer.
Apple Pay button settings
The Buy Button frame renders the button that matches the payment method in the quote signature.applePayButtonSettings styles the Apple Pay button variant
only. It has no effect when the frame renders the card or Google Pay button,
or runs in headless mode.
Use applePayButtonSettings to style the Apple Pay button without injecting
custom CSS. Build a settings object, pass it through JSON.stringify,
URL-encode the result, and append it to the frame URL.
Every property is optional. Any property you omit keeps the button’s default.
Dimensions accept pixels only. A bare number is treated as pixels and normalized to a
px value.
Validation
Validation applies each property on its own:- A property with an invalid value is ignored, and the remaining valid properties still apply. Invalid values include a non-pixel dimension such as
50%, and astyle,type, orlocaleoutside its allowed set. - If the whole value is not valid JSON, the frame ignores
applePayButtonSettingsentirely and uses the defaults.
Example
This object sets a 52px-tall white button with a 26px corner radius, thebuy label, and French text:
JSON.stringify, URL-encode the result, and append it to the frame URL:
height, size the frame container to match.
Google Pay button settings
googlePayButtonSettings styles the Google Pay button variant only. It has
no effect when the frame renders the card or Apple Pay button, or runs in
headless mode. You can pass both applePayButtonSettings and
googlePayButtonSettings on the same frame URL. The frame applies the one that
matches the button it renders.
Build a settings object, pass it through JSON.stringify, URL-encode the
result, and append it to the frame URL. Every property is optional. Any
property you omit keeps the button’s default.
Unlike Apple Pay, the Google Pay button has no
type (label) or locale
setting.
Validation
Validation applies each property on its own:- A property with an invalid value is ignored, and the remaining valid
properties still apply. Dimensions must match
^\d+(\.\d+)?(px)?$, so a value in another unit, such as50%or2rem, is ignored. Astyleoutside its allowed set is ignored. - The frame ignores
googlePayButtonSettingsentirely and uses the defaults if the value is not valid JSON, is longer than 4,096 characters, or is not a JSON object.
Example
This object sets a 48px-tall black button with a 28px corner radius:JSON.stringify, URL-encode the result, and append it to the frame URL:
height, size the frame container to match.
Events
All events are dispatched using the message pattern described in the frames protocol. Headless mode uses the same events; see Headless mode for which ones it sends.Outbound events
frame->parent These events are sent from this frame to the parent window.handshake
The frame requests that you open a message channel.
ready
With the button, the frame finished loading and the payment button is rendered.
Use this to coordinate UI transitions if needed. The frame sends ready again
after a setQuote with a different currency has been checked.
In headless mode, the purchase is starting. Use this to show
a loading indicator in your UI. After ready, the frame sends exactly one final
event: complete, challenge, or error.
buttonPressed
The customer tapped the pay button. The frame emits this the instant the button
is tapped. For Apple Pay and Google Pay, it fires before the OS presents the
payment sheet (PassKit on iOS, the Google Pay sheet on Android). For card, it
fires before the card pipeline runs. Treat it as an intent-to-buy signal: use it
to react to the tap, for example to show a loading state or fire analytics.
Headless mode does not send this event.
This event carries no payload. Route it by channelId.
buttonPressed is not a purchase outcome. It still fires if the customer opens
the payment sheet and then cancels, and it fires before any authorization
happens. For the transaction result, listen for complete, which
reports the outcome once the payment resolves.
complete
The purchase finished. Check transaction.status before treating it as a
success:
- Not
failed: the transaction was created and payment is processing. Use the transactionidto track final status, for example by polling or via webhooks. For a bank transfer quote, also use theidto fetch the bank details to show the customer. See Bank transfer. failed: the payment did not go through.failureCodesays why andfailureReasongives developer-facing details. Ifidis present, the transaction exists and you can fetch it. Ifidis missing, the transaction was never created, so there is nothing to poll.
resume, the
payload also carries sessionUpgraded: true: create a new session and check
the connection again. See Finish on your buy
button.
Failure codes
Branch onfailureCode, not failureReason, for programmatic handling.
challenge
Verification is required before the transaction can proceed. Render the
challenge frame at the provided URL. Do not
construct the URL yourself — use it as-is.
For an Apple Pay or Google Pay quote, open the challenge frame with resume to
have the customer pay on this button once the challenge is done. Keep this
button mounted. The challenge frame then emits
resolved, and the customer taps this
button again. See Finish on your buy
button.
For a card or bank transfer quote, including headless
mode, open the challenge frame without resume. It finishes
the purchase and sends complete.
cancelled
The customer dismissed the Apple Pay or Google Pay sheet without paying. This
is a cancellation, not a failure, so handle it separately from
error. The button stays on screen and the customer can tap it
again. Card payments and headless mode do not send this event.
The payload carries code: "payment_sheet_dismissed" and a developer-facing
message. The message is not intended to be rendered in UI.
error
The purchase could not run. Remove the frame and log the message for
debugging, except after quoteExpired: keep the frame and send a new quote with
setQuote. error covers setup problems, an expired quote, and
unexpected errors (generic). A payment that fails arrives as a
complete event with status: "failed".
Inbound events
parent->frame These events are sent from the parent window to this frame.ack
Acknowledge the handshake.
setQuote
Provide a new quote to the frame. Pass signature as returned by the quote
endpoint.
With the button, send this when the current quote expires before the customer
completes the purchase.
In headless mode, send this only after an error
with quoteExpired. The frame retries the purchase with the new quote. At any
other time it ignores setQuote, so it never starts a second purchase.