Skip to main content
The Challenge frame handles verification steps required by another flow. Three upstream flows mount it today:
  • The Buy Button frame emits a challenge event with a URL. Use that URL to mount the frame. For card and bank transfer quotes, including headless mode, the challenge frame finishes the purchase itself.
  • The identity flow mounts the frame directly to capture identity data (for example, document capture or liveness checks).
  • A buy quote returns a challenge object when a guest checkout customer can raise their spending limit. Mount the frame at challenge.url to run the limit upgrade.
The frame is self-driving: after the handshake, there are no further parent-to-child messages. It sequences through all required verification steps and emits complete when the pipeline finishes. The complete and cancelled payloads are discriminated by flow so you can branch on the originating flow. Opened with resume, an Apple Pay or Google Pay buy challenge emits resolved instead of complete. The customer then pays on your buy button. See Finish on your buy button.

URL

For the buy flow, the URL is provided by the buy button’s challenge event payload. For the identity flow, the URL is provided by the upstream identity verification call. For the guest_checkout_limit_upgrade flow, the URL is challenge.url on the buy quote response. In every case, do not construct the URL yourself — only append the query parameters below before setting it as the frame src.

Requirements

Size

Render the frame in a modal or full sheet so the customer can complete verification. The frame adapts to any size, but a full-screen or large modal works best for flows like 3D Secure that load bank-hosted pages.

Initialization parameters

Events

All events are dispatched using the message pattern described in the frames protocol. Below are the event payloads specific to the Challenge frame.

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

The challenge UI is rendered and visible to the customer.

buttonPressed

The customer tapped the native Apple Pay or Google Pay button inside the challenge frame’s payment sheet. The frame emits this the instant the button is tapped, before the OS presents the wallet payment sheet. 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. This event carries no payload. Route it by channelId. buttonPressed is only emitted by the flows that present a wallet payment sheet — the buy step-up and the guest_checkout_limit_upgrade flow. The identity flow does not render a payment sheet and never emits it. 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 result, listen for complete, which reports the outcome once the pipeline resolves.

complete

The challenge finished. Remove the challenge frame and branch on the discriminated payload:
  • flow: "buy" — the transaction pipeline finished. Remove the buy button too. If transaction.status is failed, the payment didn’t go through: read failureCode and failureReason, as for the Buy Button frame. Otherwise navigate to the confirmation screen. After a guest checkout recovery, the payload also carries sessionUpgraded: true: create a new session and check the connection again. See Recover the connection after a step-up.
  • flow: "identity" — the customer finished the challenge. Check status first. When it is absent, the customer was verified: use identityId to continue your onboarding flow. pending means MoonPay is still verifying them, so don’t treat them as verified: see Pending customers. unavailable means they can’t be verified (for example, an unsupported region or a closed account), so show your own screen. See Unavailable customers.
  • flow: "guest_checkout_limit_upgrade" — the upgrade reached a terminal outcome. Read status. upgraded means the limit is raised, so request the quote again to get an executable one. rejected means verification failed, and running the upgrade again does not change that.

resolved

The buy challenges are done and the frame handed the purchase back to your buy button. The frame sends this only when its URL carries resume=true, and only for an Apple Pay or Google Pay buy. Remove the Challenge frame, keep the buy button, and show the buy button again. The customer taps it to pay, and the buy button emits complete. The frame sends resolved or complete for a purchase, never both. If it can’t hand the purchase back, it shows its own confirm screen and emits complete. So keep your complete handler when you pass resume. resolved carries no transaction and no sessionUpgraded. After a guest checkout recovery, sessionUpgraded arrives on the buy button’s complete. See Finish on your buy button for every event in order.

cancelled

The customer dismissed the challenge. Remove the challenge frame and act on the discriminated payload:
  • flow: "buy" — remove the buy button too, then offer a retry path. The payload carries transactionId and challengeToken (when known) so you can resume without restarting the pipeline.
  • flow: "identity" — offer a retry path or exit. No extra payload is emitted.
  • flow: "guest_checkout_limit_upgrade" — the customer dismissed the upgrade. Nothing was submitted and the limit is unchanged. Offer a retry path.

error

The challenge failed. Remove the challenge frame (and the buy button, if the buy flow was driving it), then surface the message to the developer.
Common codes:

Pending customers

A customer can have a check in flight or under review, such as a liveness check. It is not a failure. The customer can’t finish this challenge until the check clears, so don’t mark the transaction or the customer as failed. How the frame reports it depends on the challenge:
  • Transaction challenges (buy, Apple Pay, Google Pay): an error with customer_status_pending, because the transaction can’t continue.
  • Customer API challenge (kyc.challenge): a complete with flow: "identity" and status: "pending". While a check is running, the frame can stay open on a waiting screen first and sends nothing. Don’t dispose it until it sends an event.
When you receive the event:
  1. Dispose the frame and tell the customer their verification is in progress.
  2. Poll Get a customer with GET /customers/{id}, using the customer id from the connection result. Authenticate with your secret key, or with the customer’s accessToken from the decrypted credentials. See the Customer API prerequisites for which to use. Back off between requests. Checks usually clear within a few minutes but can take longer.
  3. When kyc.status is active, resume what the customer was doing: continue your onboarding flow, or start the transaction flow again. If it is unavailable, stop and send the customer to an alternate flow.
For checks that stay pending for a longer time, offer the customer a way to contact support instead of polling indefinitely.

Unavailable customers

A customer can have a constraint that blocks verification entirely, such as an unsupported region or a closed account. How the frame reports it depends on the challenge:
  • Transaction challenges (buy, Apple Pay, Google Pay): an error with customer_status_unavailable, because the transaction can’t continue.
  • Customer API challenge (kyc.challenge): a complete with flow: "identity" and status: "unavailable", the same way the connect frame reports an unavailable customer. Don’t treat this complete as a verified customer: check status first.
When you receive the event, don’t retry and don’t poll, since neither changes the outcome. Dispose the frame and send the customer to your own screen or an alternate flow.

Inbound events

parent->frame These events are sent from the parent window to this frame.

ack

Acknowledge the handshake. This is the only message you send to the challenge frame — it is self-driving after the handshake.