# Using the Platform API
Source: https://dev.moonpay.com/api-reference/platform/documentation/using-the-api
Get started with the MoonPay Platform API
Looking for the widget API? Take a look at the [reference
here](/api-reference/widget).
## Capability enablement
**Capability access is enabled per partner.** Some Platform API capabilities
must be enabled on your account by MoonPay before you can call them. Requests
to an endpoint for a capability that is not enabled fail with a plain `404`
(`Cannot POST /...`) rather than a permissions error. If you get an unexpected
`404` on an endpoint you expect access to, contact your MoonPay account team
or [team@moonpay.com](mailto:team@moonpay.com) to have the capability enabled.
## Base URL
```bash theme={null}
https://api.moonpay.com
```
Test mode vs live mode is determined by the API key you use, not the URL. Use
test API key (`sk_test_...`) for test mode and live API keys (`sk_live_...`)
for production.
## Authentication
Authentication depends on whether you’re calling an endpoint from your server or from the client.
### Server-side Authentication
For server-side requests, send your [secret key](/platform/guides/api-and-sdk-credentials#secret-key) in the `X-Api-Key` header.
```ts fetch theme={null}
const URL = "https://api.moonpay.com/platform/v1/sessions";
const res = await fetch(URL, {
headers: {
"Content-Type": "application/json",
"X-Api-Key": "sk_test_123",
},
method: "POST",
body: JSON.stringify({
externalCustomerId: "your_user_id",
deviceIp: "203.0.113.1",
}),
});
```
```sh curl theme={null}
curl -X POST "https://api.moonpay.com/platform/v1/sessions" \
-H "Content-Type: application/json" \
-H "X-Api-Key: sk_test_123" \
-d '{
"externalCustomerId": "customer1",
"deviceIp": "203.0.113.1"
}'
```
### Client-side Authentication
For client-side API requests, use the [`accessToken`](/platform/guides/api-and-sdk-credentials#access-token) returned from a connection as a [Bearer token](https://swagger.io/docs/specification/v3_0/authentication/bearer-authentication/).
```ts fetch theme={null}
await fetch("https://api.moonpay.com/platform/v1/quotes", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
// ... request body ...
}),
});
```
## Response Format
Responses are returned as `JSON` with the `content-type: application/json` header.
## Pagination
Some requests, like [listing transactions](/api-reference/platform/endpoints/transactions/list), return paginated results using cursor-based pagination. Each response includes a cursor string that you pass to the next request to fetch the next page.
The response includes a `pageInfo` object. If `pageInfo.nextCursor` is `null`, there are no more pages. For example:
```json theme={null}
{
"data": [],
"pageInfo": {
"nextCursor": "tr_123e4567-e89b-12d3-a456-426614174000"
}
}
```
## Rate limits
Currently, requests for this integration are limited to 30 per second.
## Debugging
Each API response includes a request ID header. Use this ID when working with support:
```bash Example theme={null}
X-Request-Id: some-value
```
## Error Handling
When a request fails (4xx), the API returns an error object with details:
```json Example error response theme={null}
{
"code": 400,
"type": "Invalid request",
"message": "Invalid request. sourceAmount must be greater than 0."
}
```
## OpenAPI
The API follows the [OpenAPI 3.1](https://swagger.io/specification/) specification. You can use the spec to generate typed clients for any language.
See the [OpenAPI Spec](/platform/guides/openapi-codegen) page for the full specification and code generation instructions.
# List assets
Source: https://dev.moonpay.com/api-reference/platform/endpoints/assets/list
GET /platform/v1/assets
List the crypto assets available for purchase, including DeFi tokens
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Export customer data
Source: https://dev.moonpay.com/api-reference/platform/endpoints/customers/export
POST /platform/v1/customers/export
Export MoonPay-verified customer data using a consent token
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Get a customer
Source: https://dev.moonpay.com/api-reference/platform/endpoints/customers/get
GET /platform/v1/customers/{id}
Get a customer's details and KYC standing
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Get a file upload URL
Source: https://dev.moonpay.com/api-reference/platform/endpoints/customers/get-upload-url
POST /platform/v1/customers/{id}/files/upload-url
Get a single-use presigned URL to upload a customer's identity documents
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Confirm uploaded files
Source: https://dev.moonpay.com/api-reference/platform/endpoints/customers/submit-files
POST /platform/v1/customers/{id}/files
Confirm uploaded files and attach them to the customer's KYC session
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Submit KYC data
Source: https://dev.moonpay.com/api-reference/platform/endpoints/customers/submit-kyc
PATCH /platform/v1/customers/{id}/kyc
Submit outstanding KYC requirements for a customer
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Delete a payment method
Source: https://dev.moonpay.com/api-reference/platform/endpoints/payment-methods/delete
DELETE /platform/v1/payment-methods/{paymentMethodId}
Remove a stored payment method for a customer
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# List payment methods
Source: https://dev.moonpay.com/api-reference/platform/endpoints/payment-methods/list
GET /platform/v1/payment-methods
Get available payment method configurations for a user
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Get a quote
Source: https://dev.moonpay.com/api-reference/platform/endpoints/quotes/get
POST /platform/v1/quotes/buy
Build quotes for fiat->crypto transactions
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
## Identifying the destination token
The destination crypto asset is identified differently depending on whether it
is a centralised (CeFi) or DeFi token:
* **CeFi assets** — pass `destination.code` (e.g. `ETH`, `USDC_SOL`).
* **DeFi tokens** — pass `destination.caip19`, the
[CAIP-19](https://chainagnostic.org/CAIPs/caip-19) identifier returned by the
[list assets](/api-reference/platform/endpoints/assets/list) endpoint. A DeFi
token's `code` is not unique, so `caip19` is required to disambiguate it.
Provide exactly one. If both are sent, `caip19` takes precedence.
# Create a session
Source: https://dev.moonpay.com/api-reference/platform/endpoints/sessions/create
POST /platform/v1/sessions
Create a session token to initialize a connection
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Revoke a session
Source: https://dev.moonpay.com/api-reference/platform/endpoints/sessions/revoke
DELETE /platform/v1/sessions
Revoke an active session token
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Get a transaction
Source: https://dev.moonpay.com/api-reference/platform/endpoints/transactions/get
GET /platform/v1/transactions/{id}
Get details for a single transaction by ID
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# List transactions
Source: https://dev.moonpay.com/api-reference/platform/endpoints/transactions/list
GET /platform/v1/transactions
List transactions for the connected user with optional date filtering and pagination
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
# Simulate bank-transfer settlement
Source: https://dev.moonpay.com/api-reference/platform/endpoints/transactions/simulate-bank-transfer
POST /platform/v1/transactions/{id}/simulate-bank-transfer
Sandbox only. Drive a waiting-payment bank-transfer transaction to a terminal outcome without real funds.
Some Platform API capabilities must be enabled on your account by MoonPay
before you can call them. If this endpoint returns an unexpected `404`, see
[Capability
enablement](/api-reference/platform/documentation/using-the-api#capability-enablement).
This endpoint works only in [test
mode](/platform/overview/test-mode#bank-transfers). Live-mode transactions
return `403`. In production, bank transfers settle when the customer's deposit
arrives.
# Asset
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/asset
A fiat currency or crypto token
This is the compact asset shape embedded in a [quote](/api-reference/platform/objects-and-types/quote). For the full catalogue of purchasable tokens — including DeFi tokens and their on-chain metadata — see the [list assets](/api-reference/platform/endpoints/assets/list) endpoint.
## Properties
| Property | Type | Required | Description |
| ----------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `code` | string | Yes | The currency code or token symbol (e.g., `USD`, `ETH`, `BTC`) |
| `name` | string | No | The human-readable name (e.g., `US Dollar`, `Ethereum`) |
| `precision` | integer | No | The number of supported decimal places |
| `caip19` | string | No | The [CAIP-19](https://chainagnostic.org/CAIPs/caip-19) asset identifier. Present for DeFi tokens, whose `code` is not unique. |
## Example
```json theme={null}
{
"code": "ETH",
"name": "Ethereum",
"precision": 18
}
```
A DeFi token additionally carries its `caip19`:
```json theme={null}
{
"code": "PENGU",
"name": "Pudgy Penguins",
"precision": 0,
"caip19": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv"
}
```
## Common Assets
### Fiat Currencies
| Code | Name | Precision |
| ----- | ------------- | --------- |
| `USD` | US Dollar | 2 |
| `EUR` | Euro | 2 |
| `GBP` | British Pound | 2 |
### Cryptocurrencies
| Code | Name | Precision |
| ------ | -------- | --------- |
| `BTC` | Bitcoin | 8 |
| `ETH` | Ethereum | 18 |
| `USDC` | USD Coin | 6 |
# Customer
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/customer
A customer and their KYC standing
Returned by the [Customer API](/platform/guides/customer-api) endpoints. The `kyc` object tells you whether the customer can transact, what's outstanding, and how to submit it.
## Properties
| Property | Type | Required | Description |
| -------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------- |
| `id` | string (uuid) | Yes | The customer's unique MoonPay identifier |
| `externalCustomerId` | string | Yes | Your identifier for this customer, if one was supplied. `null` when the customer connected without an external identifier |
| `kyc` | object | Yes | KYC details for the customer |
## KYC
| Property | Type | Required | Description |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------- |
| `status` | string | Yes | The customer's KYC standing with you. See [KYC status](#kyc-status) |
| `requirements` | object | Yes | Outstanding and completed KYC requirements, keyed by category. See [Requirements](#requirements) |
| `challenge` | object | No | Hosted challenge you must surface to the customer to complete verification. See [Challenge](#challenge) |
## KYC status
| Value | Description |
| ------------- | ----------------------------------------------------------------------------------------------- |
| `not_created` | No KYC session exists yet for this customer with your account. |
| `collecting` | The customer has outstanding requirements to submit. |
| `verifying` | MoonPay is processing the submitted data. No action is needed from you or the customer. |
| `active` | KYC is complete. The customer is in good standing. |
| `unavailable` | KYC cannot proceed for this customer (for example, an unsupported region, or a closed account). |
## Requirements
Each key is a requirement category. Only categories that apply to the customer are present. Each entry has the same shape:
| Property | Type | Required | Description |
| ---------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `status` | string | Yes | `incomplete` or `complete` |
| `requiredFields` | array | No | When `status` is `incomplete`, the outstanding fields the customer must provide. Omitted when the category does not surface field-level detail |
| Category | Description |
| -------------------- | ----------------------------------------------------------------------------------------- |
| `basicDetails` | The customer's basic personal details (first name, last name, date of birth, nationality) |
| `residentialAddress` | The customer's residential address |
| `identityDocuments` | Identity document submission (passport, driver's license, residence permit) |
| `selfie` | Selfie photo submission |
| `taxIdentifiers` | Tax identifier submission (SSN, CPF, TIN) |
| `proofOfAddress` | Proof-of-address document submission |
| `phoneNumber` | The customer's phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format |
| `questionnaires` | Due-diligence questionnaires the customer must complete |
For the fields and documents each country requires, see [KYC data requirements](/platform/guides/kyc-data-requirements).
## Challenge
| Property | Type | Required | Description |
| ----------- | ------------------ | -------- | -------------------------------------------------------------------------------------- |
| `url` | string | Yes | Fully-formed challenge URL. Render it directly; the URL embeds the challenge token |
| `expiresAt` | string (date-time) | Yes | When the challenge URL expires. Surface the challenge to the customer before this time |
## Example
```json theme={null}
{
"id": "c1a2b3c4-0000-4000-8000-000000000000",
"externalCustomerId": "your_user_id",
"kyc": {
"status": "collecting",
"requirements": {
"basicDetails": { "status": "complete" },
"residentialAddress": {
"status": "incomplete",
"requiredFields": ["street", "locality", "postalCode"]
},
"identityDocuments": { "status": "incomplete" }
}
}
}
```
# Customer export
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/customer-export
MoonPay-verified customer data returned by the export endpoint
Returned by [Export customer data](/api-reference/platform/endpoints/customers/export). All fields are populated from the customer's latest approved verification. Fields MoonPay does not hold are `null`.
## Properties
| Property | Type | Required | Description |
| -------------------- | ------ | -------- | ----------------------------------------------------------------------------- |
| `basicDetails` | object | Yes | The customer's basic personal details |
| `residentialAddress` | object | Yes | The customer's residential address |
| `phoneNumber` | object | Yes | The customer's phone number |
| `taxIdentifiers` | array | Yes | Tax identifiers recorded for the customer. Empty when none are recorded |
| `files` | array | Yes | Identity documents the customer submitted, each with a presigned download URL |
## Basic details
| Property | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `firstName` | string | Yes | The customer's first name. `null` for mononymous customers recorded with only a last name |
| `lastName` | string | Yes | The customer's last name. `null` for mononymous customers recorded with only a first name |
| `dateOfBirth` | string | Yes | The customer's date of birth in `YYYY-MM-DD` format |
| `nationality` | string | Yes | The customer's nationality as an [ISO 3166-1 alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) country code |
## Residential address
| Property | Type | Required | Description |
| -------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `country` | string | Yes | Country as an [ISO 3166-1 alpha-3](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3) country code |
| `administrativeArea` | string | No | State, province, or territory. Present when the country has subdivisions recognized by MoonPay (currently USA and CAN); omitted otherwise |
| `locality` | string | Yes | City, town, or locality |
| `street` | string | Yes | First line of the address (street) |
| `subStreet` | string | No | Second line of the address (apartment, unit, building). Omitted when MoonPay does not hold this value |
| `postalCode` | string | Yes | Postal or ZIP code |
## Phone number
| Property | Type | Required | Description |
| -------- | ------ | -------- | ---------------------------------------------------------------------------------- |
| `number` | string | Yes | The customer's phone number in [E.164](https://en.wikipedia.org/wiki/E.164) format |
## Tax identifiers
| Property | Type | Required | Description |
| --------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `type` | string | Yes | Type of tax identifier: `tin`, `ssn`, or `cpf` |
| `country` | string | No | Issuing country as an ISO 3166-1 alpha-3 code. Present for `tin` entries; omitted for `ssn` and `cpf` |
| `value` | string | Yes | Identifier value as recorded by MoonPay |
## Files
| Property | Type | Required | Description |
| ------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `id` | string (uuid) | Yes | MoonPay's identifier for the file |
| `type` | string | Yes | Type of file. See [File types](#file-types) |
| `side` | string | No | `front` or `back`. Present for two-sided document types; omitted for single-sided |
| `uploadedAt` | string (date-time) | Yes | When MoonPay received the file |
| `downloadUrl` | string (uri) | Yes | Presigned download URL. Expires 60 minutes after the response is returned; do not attach your API key when fetching it |
### File types
| Value | Sides |
| ------------------------ | ------------ |
| `passport` | Single-sided |
| `driving_licence` | Two-sided |
| `national_identity_card` | Two-sided |
| `residence_permit` | Two-sided |
| `selfie` | Single-sided |
| `proof_of_address` | Single-sided |
## Example
```json theme={null}
{
"basicDetails": {
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-01-15",
"nationality": "USA"
},
"residentialAddress": {
"country": "USA",
"administrativeArea": "NY",
"locality": "New York",
"street": "350 Fifth Avenue",
"subStreet": "Apt 1A",
"postalCode": "10118"
},
"phoneNumber": {
"number": "+14155551234"
},
"taxIdentifiers": [{ "type": "ssn", "value": "078-05-1120" }],
"files": [
{
"id": "f1a2b3c4-0000-4000-8000-000000000000",
"type": "passport",
"uploadedAt": "2026-06-30T14:30:50.000Z",
"downloadUrl": "https://files.moonpay.com/exports/f1a2b3c4...?signature=..."
}
]
}
```
# Fees
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/fees
Fee breakdown for an operation
## Properties
| Property | Type | Required | Description |
| ----------- | ------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `network` | [MonetaryAmount](/api-reference/platform/objects-and-types/fees#monetaryamount) | No | The network fee (e.g., gas fee for blockchain transactions) |
| `moonpay` | [MonetaryAmount](/api-reference/platform/objects-and-types/fees#monetaryamount) | No | The MoonPay processing fee |
| `ecosystem` | [MonetaryAmount](/api-reference/platform/objects-and-types/fees#monetaryamount) | No | The ecosystem fee, if applicable |
| `defi` | [MonetaryAmount](/api-reference/platform/objects-and-types/fees#monetaryamount) | No | The DeFi swap fee. Present only for Gateway (DeFi) swaps that carry an itemized swap fee; on-chain fees are reflected in `destination.amount` instead |
| `partner` | [MonetaryAmount](/api-reference/platform/objects-and-types/fees#monetaryamount) | No | **Deprecated.** Use `ecosystem` instead. This field will be removed in a future release. The partner's fee, if applicable |
## MonetaryAmount
Each fee is represented as a monetary amount:
| Property | Type | Required | Description |
| -------------- | ------ | -------- | ---------------------------------------------------- |
| `amount` | string | Yes | The numeric amount as a string to preserve precision |
| `currencyCode` | string | Yes | The currency code (e.g., `USD`) |
## Example
```json theme={null}
{
"network": {
"amount": "2.50",
"currencyCode": "USD"
},
"moonpay": {
"amount": "3.99",
"currencyCode": "USD"
},
"ecosystem": {
"amount": "1.00",
"currencyCode": "USD"
},
"partner": {
"amount": "1.00",
"currencyCode": "USD"
}
}
```
All fee amounts are strings to preserve decimal precision. Parse them
appropriately in your application.
# Payment Method
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/payment-method
A payment method configuration with its capabilities and availability
## Properties
| Property | Type | Required | Description |
| -------------- | ------ | -------- | -------------------------------------------------- |
| `type` | string | Yes | Payment method type (e.g., `apple_pay`) |
| `capabilities` | object | Yes | What this payment method supports |
| `availability` | object | Yes | Whether this payment method is currently available |
## Capabilities
| Property | Type | Required | Description |
| --------------------------- | --------- | -------- | ---------------------------------------------------------- |
| `supportedCurrencies` | string\[] | Yes | Currencies supported (e.g., `["USD", "EUR"]`) |
| `supportedTransactionTypes` | string\[] | Yes | Transaction types supported (e.g., `["buy", "sell"]`) |
| `allowsDeletion` | boolean | Yes | Whether this payment method can be deleted |
| `requiresWidget` | boolean | Yes | Whether completing the payment requires the MoonPay widget |
## Availability
| Property | Type | Required | Description |
| --------- | --------- | -------- | -------------------------------------------------- |
| `active` | boolean | Yes | Whether the payment method is currently available |
| `reasons` | string\[] | No | If unavailable, a list of machine-readable reasons |
## Payment Method Types
| Type | Description |
| ------------ | --------------------------- |
| `apple_pay` | Apple Pay |
| `google_pay` | Google Pay |
| `card` | Stored credit or debit card |
| `sepa` | SEPA bank transfer |
The fiat currencies each method supports come from `capabilities.supportedCurrencies` (see [Capabilities](#capabilities)); `sepa` bank transfers are EUR only.
Bank transfers (`sepa`) are a floating payment method: the quote is an estimate, and the final amount is set when the customer's funds settle. See [Exchange rate type](/api-reference/platform/objects-and-types/quote#exchange-rate-type).
## Example
```json theme={null}
{
"type": "apple_pay",
"capabilities": {
"supportedCurrencies": ["USD", "EUR", "GBP"],
"supportedTransactionTypes": ["buy"],
"allowsDeletion": false,
"requiresWidget": false
},
"availability": {
"active": true
}
}
```
## Checking Availability
Always check `availability.active` before offering a payment method to users. If `active` is `false`, check `reasons` for machine-readable details:
```json theme={null}
{
"type": "apple_pay",
"capabilities": {
"supportedCurrencies": ["USD"],
"supportedTransactionTypes": ["buy"],
"allowsDeletion": false,
"requiresWidget": false
},
"availability": {
"active": false,
"reasons": ["maintenance"]
}
}
```
# Quote
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/quote
A quote for a buy transaction with an exchange rate, fees, and expiry
## Properties
| Property | Type | Required | Description |
| -------------------- | ------------------------------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------- |
| `source` | object | Yes | Source amount and asset (fiat currency) you send |
| `destination` | object | Yes | Destination amount and asset (cryptocurrency) the customer receives |
| `fees` | [Fees](/api-reference/platform/objects-and-types/fees) | Yes | Breakdown of network, MoonPay, and ecosystem fees |
| `wallet` | [Wallet](/api-reference/platform/objects-and-types/wallet) \| null | Yes | Wallet address where crypto will be sent. Null if not yet provided |
| `paymentMethod` | object \| null | Yes | Payment method used for this quote. Null if not specified |
| `expiresAt` | string (date-time) | Yes | ISO 8601 datetime when the quote expires |
| `executable` | boolean | Yes | Whether the quote can be executed |
| `feeBehavior` | string | Yes | Effective fee behavior, `inclusive` or `exclusive`. See [Fee behavior](#fee-behavior). |
| `exchangeRate` | string | Yes | The rate used to convert between fiat and crypto, as the fiat value of one unit of crypto. Pairs with `exchangeRateType`. |
| `exchangeRateType` | string | Yes | Whether the exchange rate is `fixed` or `floating`. See [Exchange rate type](#exchange-rate-type). |
| `signature` | string | Yes | Signature for mounting a payment frame |
| `paymentDisclosures` | array | No | Disclosure(s) to render for this transaction. Empty array means none required. |
| `challenge` | [Challenge](#challenge) | No | A step the customer must complete before this quote can be executed. See [Challenge](#challenge). |
## Source / Destination
Both `source` and `destination` have the same structure:
| Property | Type | Required | Description |
| -------- | -------------------------------------------------------- | -------- | -------------------------------------------- |
| `amount` | string | Yes | The amount as a string to preserve precision |
| `asset` | [Asset](/api-reference/platform/objects-and-types/asset) | Yes | The currency or token |
## Fee behavior
`feeBehavior` describes how fees relate to `source.amount` when you request a quote by `source.amount`:
* `inclusive` (default): the customer pays exactly `source.amount`, and fees are carved out of it.
* `exclusive`: fees are added on top of `source.amount`. The full `source.amount` is converted, so the customer receives more crypto than the inclusive quote for the same input.
Set `feeBehavior` on the buy-quote request to choose the behavior. It applies only to source-amount quotes. When you quote by `destination.amount`, MoonPay ignores the request value and the quote is always fees-inclusive. The response always echoes the effective `feeBehavior`.
## Exchange rate type
`exchangeRateType` tells you whether the quoted amount is final or an estimate, and pairs with the `exchangeRate` field:
* `fixed`: the exchange rate is locked at quote time. Card and wallet methods (card, Apple Pay, Google Pay) return `fixed`, and the quoted amount is final.
* `floating`: the exchange rate is an estimate confirmed when the payment settles. Bank transfers (SEPA) return `floating`.
When `exchangeRateType` is `floating`, render the estimated crypto amount with a tilde (for example, `~0.2345 BTC`) and tell the customer the final amount is set when the transfer settles.
Bank transfers don't support DeFi assets. If you request a bank-transfer quote
(`sepa`) for a destination identified by `caip19`, the request fails with
`400` and the message "Bank transfers are not supported for DeFi assets."
Offer a card or wallet payment method for these assets instead.
## Challenge
`challenge` is present when the customer must complete a step before this quote
can be executed. It arrives alongside `executable: false`. Mount the
[challenge frame](/platform/frames/challenge) at `url`, then request the quote
again once the challenge resolves.
| Property | Type | Required | Description |
| -------- | ------ | -------- | -------------------------------------------------------------------------------------------- |
| `kind` | string | Yes | How to resolve the challenge. Always `frame` today. |
| `url` | string | Yes | Fully-formed challenge frame URL. Render it directly. Do not construct, parse, or modify it. |
```json theme={null}
{
"executable": false,
"challenge": {
"kind": "frame",
"url": "https://platform.moonpay.com/v2/challenge?challengeToken=eyJhbGciOiJFUzI1NiIs..."
}
}
```
Guest checkout limit upgrades are the one flow that returns `challenge` on a
quote today. See [Upgrade a guest
account](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up).
## Using the Quote
Pass the `signature` to the payment frame when you mount it. The frame uses this signature along with the quote data to execute the payment and create the transaction.
Quotes expire. Check `expiresAt` and create the transaction before this time.
If `executable` is `false`, the customer must provide additional information
before you can use this quote. When the quote also carries a
[`challenge`](#challenge), that object tells you what to render to collect it.
## Payment disclosures
Each item in `paymentDisclosures` identifies a specific piece of text you must render verbatim.
| Property | Type | Description |
| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Identifies a specific disclosure. See the ID table below. |
| `version` | string | Increments on any wording change. If you receive an unrecognised `version`, fall back to the most-conservative disclosure and log an alert so your integration team can update the copy. |
### Disclosure IDs
| ID | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `us-transaction-finality` | Required in NY and WA (NYDFS / state money-transmitter rules). See [disclosure copy](/platform/overview/going-live#disclosures---us-new-york-and-washington). |
| `eea-crypto-asset-risk` | EEA standard crypto-asset risk disclosure (MiCA) |
| `eea-unregulated-stablecoin-risk` | EEA disclosure for non-MiCA-compliant stablecoins (USDT, DAI, PYUSD) |
| `gateway-token` | Required when buying a DeFi token via Gateway. See [disclosure copy](/platform/overview/going-live#disclosures---defi-tokens-gateway). |
## Example
```json theme={null}
{
"source": {
"amount": "100.00",
"asset": {
"code": "USD",
"name": "US Dollar",
"precision": 2
}
},
"destination": {
"amount": "0.0025",
"asset": {
"code": "ETH",
"name": "Ethereum",
"precision": 18
}
},
"fees": {
"network": {
"amount": "2.50",
"currencyCode": "USD"
},
"moonpay": {
"amount": "3.99",
"currencyCode": "USD"
}
},
"wallet": {
"address": "0x1234...abcd"
},
"paymentMethod": {
"type": "apple_pay"
},
"expiresAt": "2026-01-29T14:35:50.000Z",
"executable": true,
"feeBehavior": "inclusive",
"exchangeRate": "87523.17",
"exchangeRateType": "fixed",
"signature": "eyJhbGciOiJIUzI1NiIs...",
"paymentDisclosures": [
{
"id": "eea-crypto-asset-risk",
"version": "1"
}
]
}
```
# Transaction
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/transaction
A transaction representing a crypto purchase or sale
## Properties
| Property | Type | Required | Description |
| ------------------------- | ---------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Yes | The MoonPay ID of the transaction |
| `createdAt` | string (date-time) | Yes | [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp of when the transaction was created |
| `updatedAt` | string (date-time) | Yes | [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp of when the transaction was last updated |
| `status` | string | Yes | The current status: `completed`, `failed`, or `pending` |
| `source` | object | Yes | The source amount and asset (fiat currency) |
| `destination` | object | Yes | The destination amount and asset (cryptocurrency) |
| `fees` | [Fees](/api-reference/platform/objects-and-types/fees) | Yes | Fee breakdown for the transaction |
| `wallet` | [Wallet](/api-reference/platform/objects-and-types/wallet) | Yes | The wallet where crypto was delivered |
| `customer` | object | Yes | The customer who made the transaction |
| `paymentMethod` | object | No | The payment method used |
| `stages` | array | No | The stages of the transaction lifecycle |
| `bankTransferDepositInfo` | object | No | Bank account details and payment reference for completing a bank transfer. Present only on bank-transfer transactions (SEPA). See [Bank transfer deposit info](#bank-transfer-deposit-info). |
## Source / Destination
Both `source` and `destination` have the same structure:
| Property | Type | Required | Description |
| -------- | -------------------------------------------------------- | -------- | -------------------------------------------- |
| `amount` | string | Yes | The amount as a string to preserve precision |
| `asset` | [Asset](/api-reference/platform/objects-and-types/asset) | Yes | The currency or token |
## Customer
| Property | Type | Required | Description |
| -------- | ------ | -------- | ------------------------------ |
| `id` | string | Yes | The MoonPay ID of the customer |
## Transaction Status
| Value | Description |
| ----------- | ---------------------------------- |
| `pending` | Transaction is in progress |
| `completed` | Transaction completed successfully |
| `failed` | Transaction failed |
## Bank transfer deposit info
Bank-transfer payments (SEPA) settle when the customer sends funds to a MoonPay bank account. For these transactions, the response includes a `bankTransferDepositInfo` object with the account details and the payment reference. Render these details in your own UI so the customer can complete the transfer, then poll the transaction until it reaches a terminal status. Bank-transfer transactions stay `pending` until the customer's funds arrive.
| Property | Type | Required | Description |
| ------------------ | ------ | -------- | ------------------------------------------------------------------ |
| `reference` | string | Yes | The payment reference the customer must include with the transfer. |
| `recipientName` | string | Yes | The name of the recipient that receives the funds. |
| `recipientAddress` | string | Yes | The address of the recipient. |
| `iban` | string | No | The IBAN. Provided for SEPA (EUR). |
| `bic` | string | No | The BIC (SWIFT code). Provided for SEPA (EUR). |
| `bankName` | string | No | The name of the receiving bank. |
| `bankAddress` | string | No | The address of the receiving bank. |
The customer must always include the payment `reference`. Transfers sent
without the reference are rejected.
## Example
```json theme={null}
{
"id": "tr_abc123",
"createdAt": "2026-01-29T14:30:50.000Z",
"updatedAt": "2026-01-29T15:30:50.000Z",
"status": "completed",
"source": {
"amount": "100.00",
"asset": {
"code": "USD"
}
},
"destination": {
"amount": "0.0025",
"asset": {
"code": "ETH"
}
},
"fees": {
"network": {
"amount": "2.50",
"currencyCode": "USD"
},
"moonpay": {
"amount": "3.99",
"currencyCode": "USD"
}
},
"wallet": {
"address": "0x1234...abcd"
},
"customer": {
"id": "cust_xyz789"
},
"paymentMethod": {
"type": "apple_pay"
}
}
```
# Wallet
Source: https://dev.moonpay.com/api-reference/platform/objects-and-types/wallet
A blockchain wallet address
## Properties
| Property | Type | Required | Description |
| --------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `address` | string | Yes | The wallet address |
| `tag` | string | No | An optional memo or destination tag (used by some blockchains like XRP, XLM) |
## Example
### Standard Wallet
```json theme={null}
{
"address": "0x1234567890abcdef1234567890abcdef12345678"
}
```
### Wallet with Memo/Tag
Some blockchains like XRP and XLM require a destination tag or memo to route funds to the correct account:
```json theme={null}
{
"address": "rN7n3473SaZBCG4dFL83w7a1RXtXtbk2D9",
"tag": "12345678"
}
```
For blockchains that require tags/memos (XRP, XLM, etc.), always include the
`tag` field when provided. Missing tags can result in lost funds.
# Cancel Sell transaction
Source: https://dev.moonpay.com/api-reference/widget/cancelselltransaction
DELETE /v3/sell_transactions/{transactionId}
Cancels a sell transaction. This endpoint will return HTTP status 204 No Content if the sell transaction was successfully canceled. If sell transaction could not be canceled (e.g. because it has already been completed) it will return HTTP status 409 Conflict.
# Errors
Source: https://dev.moonpay.com/api-reference/widget/errors
The error response shape, the moonPayErrorCode catalog, and how to resolve common errors
Every error from the widget API is a JSON body with a stable, machine-readable `moonPayErrorCode`. This page documents the response shape, the codes you can encounter, and what to do about them.
## Error response shape
```json theme={null}
{
"moonPayErrorCode": "4_SYS_BAD_REQUEST",
"message": "Invalid JSON in request body",
"type": "BadRequestError",
"errors": []
}
```
| Field | Type | Description |
| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `moonPayErrorCode` | string | Stable error code, safe to branch on. Codes never change meaning once published. |
| `message` | string | Human-readable description. May change over time; do not branch on this text. |
| `type` | string | The error class name, e.g. `BadRequestError`, `UnauthorizedError`, `MoonPayApiError`. |
| `errors` | array | Field-level validation details when the request body or parameters were invalid: `{ property, message, children }`. Empty or absent otherwise. |
## Code format
Codes follow the pattern `{category}_{space}_{name}`:
| Prefix | Category |
| ------------ | --------------------------------------------------------------------------------------------------- |
| `1_SYS_` | General system errors |
| `4_SYS_` | Request validation errors |
| `5_{SPACE}_` | Domain-specific errors, e.g. `5_TM_` (transactions), `5_PS_` (payments), `5_AUTH_` (authentication) |
## System codes
These can occur on any endpoint:
| Code | Status | Meaning | What to do |
| ---------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `4_SYS_NOT_AUTHORIZED` | 401 | Missing or invalid API key or token | Check you're sending the right credential for the endpoint; see [Authentication](/api-reference/widget/using-the-api#authentication) |
| `4_SYS_FORBIDDEN` | 403 | Authenticated but not permitted, often a feature that isn't enabled for your account (e.g. "Bank account verification is not enabled") | If the message names a feature, contact your account manager or support to enable it |
| `4_SYS_ACCESS_REVOKED` | 403 | Access has been revoked | Contact support |
| `4_SYS_BAD_REQUEST` | 400 | Malformed request: invalid JSON body or failed validation | Fix the request; check `message` and `errors` |
| `4_SYS_NOT_FOUND` | 404 | Resource not found | Check the identifier; for customers, they must have at least one session initiated with your key |
| `4_SYS_NOT_ALLOWED` | 405 | HTTP method not allowed | Use the method documented on the endpoint page |
| `4_SYS_CONFLICT` | 409 | Request conflicts with current resource state | Re-fetch the resource and check its state |
| `4_SYS_UNPROCESSABLE_ENTITY` | 422 | Input is well-formed but semantically invalid | Fix the request; check `message` |
| `4_SYS_TOO_MANY_REQUESTS` | 429 | Rate limited | Retry with exponential backoff |
| `4_SYS_NOT_IMPLEMENTED` | 501 | Endpoint path not implemented | Check the URL; contact support if it matches the docs |
| `1_SYS_UNKNOWN` | varies | See below | See below |
## `1_SYS_UNKNOWN` is not always a server error
`1_SYS_UNKNOWN` means the error didn't carry a specific code. **The HTTP status tells you whose problem it is:**
* **With a 4xx status**, it is a validation error from an older code path. The `message` is accurate and actionable; treat it exactly like a `4_SYS_*` error. For example, URL signing failures return `400` with `1_SYS_UNKNOWN` and messages like `Invalid signature`, `Missing signature`, or `Invalid API key`. That means your [URL signing](/widget/on-ramp/customization/url-signing) implementation needs fixing, not that MoonPay had an internal error.
* **With a 500 status**, it is a genuine unexpected error on MoonPay's side. The message is `Internal Server Error`. Retry with backoff; if it persists, contact support with the request details and timestamp.
## Common domain codes
Domain codes (`5_*`) are stable and their messages are descriptive. The ones you'll most commonly encounter:
| Code | Status | Meaning | What to do |
| ----------------------------------- | ------ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `5_DP_LIVE_MODE_DISABLED` | 403 | Live API key used against the sandbox environment | Use your `pk_test_`/`sk_test_` keys on sandbox |
| `5_TM_MIN_BUY_AMOUNT_NOT_MET` | 400 | Amount below the currency's minimum | Check limits via [Get currency limits](/api-reference/widget/getcurrencylimits) |
| `5_TM_MAX_BUY_AMOUNT_EXCEEDED` | 400 | Amount above the currency's maximum | Check limits via [Get currency limits](/api-reference/widget/getcurrencylimits) |
| `5_TM_INVALID_WALLET_ADDRESS` | 400 | Wallet address invalid for the currency | Validate against the currency's `addressRegex` from [Get currencies](/api-reference/widget/getcurrencies) |
| `5_TM_CURRENCY_SUSPENDED` | 422 | Transactions for this currency are temporarily suspended | Retry later; check [Get currencies](/api-reference/widget/getcurrencies) for `isSuspended` |
| `5_TM_DEFI_TOKEN_NOT_SUPPORTED` | 400 | DeFi token not supported | Check [Get DeFi tokens](/api-reference/widget/getdefitokens) |
| `5_QC_BUY_QUOTE_INVALID_SIGNATURE` | 400 | Quote signature invalid: the quote was tampered with or expired | Fetch a fresh quote and pass its signature through unchanged |
| `5_PARTNERS_IP_MISMATCH` | 400 | Customer IP doesn't match `allowedIpAddress` on the signed widget URL | See [IP address matching](/widget/on-ramp/customization/ip-matching) |
| `5_PARTNERS_IP_MISSING` | 400 | `allowedIpAddress` missing from a signed widget URL that requires it | See [IP address matching](/widget/on-ramp/customization/ip-matching) |
| `5_PS_PAYMENT_METHOD_NOT_AVAILABLE` | 400 | Payment method not available for this customer/currency/region | Offer an alternative payment method |
| `5_PS_CURRENCY_NOT_SUPPORTED` | 400 | Currency not supported for this operation | Check [Get currencies](/api-reference/widget/getcurrencies) |
| `5_IS_LIMIT_EXCEEDED` | 400 | Transaction amount exceeds the customer's daily limit | Surface the limit to the customer; limits depend on KYC level |
| `5_IS_SCA_REQUIRED` | 400 | Strong customer authentication required | Direct the customer through the widget flow |
If you hit a `5_*` code that isn't listed here, the `message` describes the cause. The code is stable, so it's safe to handle specific cases in your integration.
## Worked examples
**`403` + `4_SYS_FORBIDDEN` + "Bank account verification is not enabled"**: your account doesn't have the named feature enabled. No request change will fix it; ask your account manager or support to enable bank account verification.
**`400` + `1_SYS_UNKNOWN` + "Invalid signature"**: your widget URL signature doesn't match what MoonPay computes. Verify you're signing the exact query string (after URL encoding) with your **secret** key, HMAC-SHA256, base64-encoded; see [URL signing](/widget/on-ramp/customization/url-signing).
# Get Real-time Buy quote
Source: https://dev.moonpay.com/api-reference/widget/getbuyquote
GET /v3/currencies/{currencyCode}/buy_quote
Get detailed real-time quote based on the provided currency code, base amount, your extra fee percentage, payment method, and the inclusion of the fees.
# Get Buy transaction
Source: https://dev.moonpay.com/api-reference/widget/getbuytransaction
GET /v1/transactions/{transactionId}
Retrieve a transaction by id. This call will return an error if no transaction with the supplied identifier exists.
# Get Buy transaction by External identifier
Source: https://dev.moonpay.com/api-reference/widget/getbuytransactionbyexternalid
GET /v1/transactions/ext/{externalTransactionId}
Retrieve a transaction by its externalTransactionId. This is the identifier you assigned the transaction when creating it. This endpoint returns an array of objects because we cannot ensure the uniqueness of externalTransactionId.
# List Buy transactions
Source: https://dev.moonpay.com/api-reference/widget/getbuytransactions
GET /v1/transactions
Returns an array of Buy transactions that match the criteria in the query parameters. Filtering by customerId, externalCustomerId, or externalTransactionId returns transactions of any status. Without one of those filters, the endpoint returns only pending and completed transactions. Each entry in the array is a separate transaction object. Transactions are listed from newest to oldest.
# List supported countries
Source: https://dev.moonpay.com/api-reference/widget/getcountries
GET /v3/countries
Returns the list of countries currently supported by MoonPay. Authentication is optional: pass your publishable `apiKey` to have availability tailored to your account.
# List supported currencies
Source: https://dev.moonpay.com/api-reference/widget/getcurrencies
GET /v3/currencies
Returns the list of currencies supported by MoonPay.
# Get Crypto Currency limits
Source: https://dev.moonpay.com/api-reference/widget/getcurrencylimits
GET /v3/currencies/{currencyCode}/limits
Returns an object containing minimum and maximum buy amounts including or excluding fees for base and quote currencies.
It takes into account the payment method if it's provided, **otherwise it defaults to the payment method with the lowest fees.**
# Get customer
Source: https://dev.moonpay.com/api-reference/widget/getcustomer
GET /v1/customers/{customerId}
Returns very basic information about a customer based on their MoonPay ID. For you to be able to retrieve a customer, they must have at least one session initiated with your `Api-Key`.
# Get customer by externalId
Source: https://dev.moonpay.com/api-reference/widget/getcustomerbyexternalid
GET /v1/customers/ext/{customerId}
Returns very basic information about a customer based on their external customer ID. For you to be able to retrieve a customer, they must have at least one session initiated with your `API-Key`. Please note that this endpoint returns an array of objects because we cannot ensure the uniqueness of the external customer ID.
# Get DeFi token
Source: https://dev.moonpay.com/api-reference/widget/getdefitoken
GET /v1/defi/token
Retrieve defi token for a specific contractAddress and network code
# List DeFi tokens
Source: https://dev.moonpay.com/api-reference/widget/getdefitokens
GET /v1/defi/tokens
Search and retrieve a paginated list of defi tokens
# Check Customer's IP address
Source: https://dev.moonpay.com/api-reference/widget/getipaddress
GET /v3/ip_address
Returns information about an IP address. If the `isAllowed` flag is set to false, it means that MoonPay accepts citizens of this country but not residents.
# Get Crypto network fees
Source: https://dev.moonpay.com/api-reference/widget/getnetworkfees
GET /v3/currencies/network_fees
Returns a set of key-value pairs representing the current network fees of cryptocurrencies against fiat currencies.
Supply the codes of the crypto and fiat currencies you are interested in, and MoonPay will return the relevant network fees.
# Get off ramp transaction
Source: https://dev.moonpay.com/api-reference/widget/getofframptransaction
GET /v1/virtual-accounts/transactions/offramp/{transactionId}
# Get off ramp transactions
Source: https://dev.moonpay.com/api-reference/widget/getofframptransactions
GET /v1/virtual-accounts/transactions/offramp
# Get on ramp transaction
Source: https://dev.moonpay.com/api-reference/widget/getonramptransaction
GET /v1/virtual-accounts/transactions/onramp/{transactionId}
# Get on ramp transactions
Source: https://dev.moonpay.com/api-reference/widget/getonramptransactions
GET /v1/virtual-accounts/transactions/onramp
# Get Sell quote
Source: https://dev.moonpay.com/api-reference/widget/getsellquote
GET /v3/currencies/{currencyCode}/sell_quote
Returns a set of key-value pairs representing a real-time sell quote for a currency. Supply the currency code, the base amount, your extra fee percentage, the payment method and whether the base amount is inclusive of fees, and MoonPay will return a detailed sell quote.
# Get Sell transaction
Source: https://dev.moonpay.com/api-reference/widget/getselltransaction
GET /v3/sell_transactions/{transactionId}
# Get Sell transaction by External identifier
Source: https://dev.moonpay.com/api-reference/widget/getselltransactionbyexternalid
GET /v3/sell_transactions/ext/{externalTransactionId}
Retrieve a transaction by its externalTransactionId. This is the identifier you assigned the transaction when creating it. This endpoint returns an array of objects because we cannot ensure the uniqueness of externalTransactionId.
# List Sell transactions
Source: https://dev.moonpay.com/api-reference/widget/getselltransactions
GET /v3/sell_transactions
Returns an array of successful Sell transactions which fulfill criteria supplied in the query parameters. Each entry in the array is a separate transaction object. Transactions will be listed from newest to oldest. This call will return an error if `customerId` is not supplied in the query parameters.
# List currencies (v4)
Source: https://dev.moonpay.com/api-reference/widget/getv4currencies
GET /v4/currencies
Returns a paginated list of CeFi and DeFi currencies enriched with MoonPay capability flags (buy, sell, swap).
Results are filtered by the caller's IP geolocation and, when an `apiKey` is provided, by the partner account configuration. Pass a customer `authorization` bearer token to apply customer-specific eligibility rules.
Use `cursor` and `limit` for cursor-based pagination. When `nextCursor` is `null`, you have reached the last page.
# Get virtual accounts
Source: https://dev.moonpay.com/api-reference/widget/getvirtualaccounts
GET /v1/virtual-accounts
# List available payment methods
Source: https://dev.moonpay.com/api-reference/widget/listpaymentmethods
GET /payments/v1/payment-method-config
# Overview
Source: https://dev.moonpay.com/api-reference/widget/overview
API reference for the MoonPay widget integration.
The MoonPay widget integration is powered by two APIs:
* **Ramps**: buy and sell quotes; transaction lookups; supported
countries, currencies, and payment methods.
* **Virtual Accounts**: programmatic on-ramp and off-ramp via virtual
bank accounts, plus the associated transaction history.
Buy quotes, currency limits, network fees, and transaction lookups.
Sell quotes and sell transaction lookups.
Supported countries, currencies, payment methods, and IP-address checks.
Token data and token lists for DeFi assets.
Programmatic on-ramp and off-ramp via virtual bank accounts.
Receive asynchronous notifications when transaction status changes.
## Authentication and base URL
See [Using the API](/api-reference/widget/using-the-api) for the base URL, API
key types, and which endpoints use which authentication scheme.
## Errors
Errors return a JSON body with a stable `moonPayErrorCode`. See
[Errors](/api-reference/widget/errors) for the response shape and code catalog.
# Update on ramp virtual account
Source: https://dev.moonpay.com/api-reference/widget/updateonrampvirtualaccount
PATCH /v1/virtual-accounts/onramp/{id}
# Using the API
Source: https://dev.moonpay.com/api-reference/widget/using-the-api
Base URL, API keys, and which widget endpoints need which authentication scheme
## Base URL
```text theme={null}
https://api.moonpay.com
```
Test and live environments use the same base URL. The key prefix determines the environment.
## Availability
Not every endpoint in this reference is available to every account:
| Tier | Meaning | Endpoints |
| ------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Self-serve | Works with your API keys today | Currencies, quotes, limits, countries, transactions, customers, payment methods, DeFi tokens |
| Requires enablement | Returns `403` until enabled for your account. Contact support | Virtual accounts |
## Authentication
The widget API uses two authentication schemes. Every endpoint page in this reference declares the scheme it needs in its **Authorizations** section.
### API keys
You have two kinds of API key, both found on the [API keys page](https://dashboard.moonpay.com/developers/api-keys) of your MoonPay dashboard:
| Key | Prefixes | Where it can live | Used as |
| --------------- | ---------------------- | ----------------------------- | -------------------------------------------- |
| Publishable key | `pk_test_`, `pk_live_` | Client-side code, widget URLs | `apiKey` query parameter |
| Secret key | `sk_test_`, `sk_live_` | **Server-side only** | `Authorization: Api-Key ` header |
`_test_` keys operate against the sandbox, `_live_` keys against production. Test and live data are fully separate.
Never expose a secret key in client-side code, mobile apps, or version
control. Anyone holding it can read your customers' transaction data.
### Publishable key (query parameter)
Currencies, quotes, limits, network fees, and single-transaction lookups authenticate with your publishable key as the `apiKey` query parameter:
```bash theme={null}
curl "https://api.moonpay.com/v3/currencies/btc/buy_quote?apiKey=pk_test_key&baseCurrencyAmount=100&baseCurrencyCode=usd"
```
`GET /v3/countries` works without a key; passing one tailors availability to your account's configuration.
### Secret key (Authorization header)
The [server-to-server endpoints](/api-reference/widget/getbuytransactions) and `GET /payments/v1/payment-method-config` authenticate with your secret key in the `Authorization` header, using the `Api-Key` prefix:
```bash curl theme={null}
curl "https://api.moonpay.com/v1/transactions" \
--header "Authorization: Api-Key sk_test_key"
```
```typescript fetch theme={null}
const response = await fetch("https://api.moonpay.com/v1/transactions", {
headers: { Authorization: "Api-Key sk_test_key" },
});
```
### URL signing
Widget URLs that carry sensitive query parameters require a `signature` parameter, computed with your secret key. This is separate from API authentication. See [URL signing](/widget/on-ramp/customization/url-signing).
# Cancel Sell transaction by externalId
Source: https://dev.moonpay.com/api-reference/widget/webhooks/cancelselltransactionbyexternalid
DELETE /v3/sell_transactions/ext/{transactionId}
Cancels a sell transaction based on their external transaction ID. This endpoint will return HTTP status 204 No Content if the sell transaction was successfully canceled. If sell transaction could not be canceled (e.g. because it has already been completed) it will return HTTP status 409 Conflict.
# identity_check_updated
Source: https://dev.moonpay.com/api-reference/widget/webhooks/identity-check-updated
api-reference/widget/webhooks.openapi.json webhook identity_check_updated
Sent whenever the KYC status of a customer changes.
# Overview
Source: https://dev.moonpay.com/api-reference/widget/webhooks/overview
## When to use webhooks
Webhooks are essential for managing behind-the-scenes transactions. They allow you to receive alerts for asynchronous updates to transaction statuses. MoonPay can send webhook events that notify your application whenever an activity occurs on your account. This feature is particularly valuable for tracking changes like transaction status updates, that are not triggered by a direct API request.
These notifications are delivered through HTTP POST requests to any endpoint URLs you've specified in your account's [Webhooks settings](https://dashboard.moonpay.com/developers/#webhooks). MoonPay is capable of sending a single event to multiple webhook endpoints.
## Configuring your webhook settings
Webhooks are configured in your MoonPay dashboard's [Webhook settings](https://dashboard.moonpay.com/developers/#webhooks). Click Add Endpoint to reveal a form where you can add a new URL for receiving webhooks.
You can enter any URL as the destination for events. However, this should be a dedicated page on your server that is set up to receive webhook notifications. You can choose to be notified of all event types, or only specific ones.
The dashboard may list event types that do not apply to widget integrations, such as `swap_*` events. These never fire for widget accounts and are safe to leave unselected.
Using test or live API keys determines whether test events or live events are sent to your configured URL. If you want to send both live and test events to the same URL, you need to create two separate settings. You can add as many URLs as you like.
## Delivery, retries and ordering
Every event is delivered as an HTTP POST request with a JSON body, signed with the `Moonpay-Signature` and `Moonpay-Signature-V2` headers (see [Request signing](/api-reference/widget/webhooks/signature)).
* **Acknowledge quickly.** Respond with a 2xx status code within 5 seconds. Any non-2xx response, timeout or connection error counts as a failed delivery attempt. If you need to do heavy processing, acknowledge first and process asynchronously.
* **Failed deliveries are retried.** MoonPay retries a failed delivery up to 9 times with exponential backoff, starting at 1 second and roughly doubling per attempt. Once retries are exhausted the event is not redelivered.
* **Delivery is at-least-once.** Duplicate deliveries can occur, so make your handler idempotent. For example, deduplicate on the event `type` plus the transaction `id` and `updatedAt` values in the payload.
* **Events are not ordered.** Events can arrive out of order, especially when retries are involved. Use the `updatedAt` timestamp inside the payload rather than arrival order to decide whether an event is newer than the state you have stored.
## Receiving a webhook notification
Setting up an endpoint to receive webhook HTTP POST requests in the JSON request body can vary based on your backend stack and hosting environment. Below are some methods to achieve this using different technologies:
### Python with Flask
Flask is a lightweight WSGI web application framework in Python.
```python theme={null}
from flask import Flask, request, jsonify
app = Flask(**name**)
@app.route('/webhook', methods=['POST'])
def webhook(): # Flask automatically parses JSON if the Content-Type is application/json
data = request.json
print(f"Received data: {data}")
return jsonify({"status": "success"}), 200
if **name** == '**main**':
app.run(port=5000)
```
### Node.js with Express
Node.js is widely used for server-side development, and Express is one of the most popular frameworks for Node.js.
```typescript theme={null}
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
// Middleware to parse JSON payload from incoming POST request
app.use(bodyParser.json());
app.post("/webhook", (req, res) => {
// req.body contains the parsed JSON payload
const data = req.body;
console.log(`Received data: ${JSON.stringify(data)}`);
res.status(200).json({ status: "success" });
});
app.listen(3000, () => {
console.log("Server started on ");
});
```
### Ruby with Sinatra
Sinatra is a DSL (Domain Specific Language) for quickly creating web applications in Ruby with minimal effort.
```ruby theme={null}
require 'sinatra'
require 'json'
post '/webhook' do # Reading and parsing the JSON payload from the request body
data = JSON.parse(request.body.read)
puts "Received data: #{data}"
[200, { 'Content-Type' => 'application/json' }, { status: 'success' }.to_json]
end
```
### Deployment
After setting up your webhook endpoint, you'll need to deploy it. You can use cloud services like AWS, Google Cloud, Heroku, or any VPS provider for this purpose.
### Secure your webhook
You may validate incoming requests to ensure they are coming from a trusted source. Visit our [webhooks signature](/api-reference/widget/webhooks/signature) for more information. This is a recommended best practice.
### Test your webhook
After deployment, you can test your webhook endpoint using Postman or curl to send a simulated POST request.
# sell_transaction_created
Source: https://dev.moonpay.com/api-reference/widget/webhooks/sell-transaction-created
api-reference/widget/webhooks.openapi.json webhook sell_transaction_created
Sent when a customer creates a Sell transaction in the widget.
# sell_transaction_failed
Source: https://dev.moonpay.com/api-reference/widget/webhooks/sell-transaction-failed
api-reference/widget/webhooks.openapi.json webhook sell_transaction_failed
Sent when a Sell transaction fails.
# sell_transaction_requote_required
Source: https://dev.moonpay.com/api-reference/widget/webhooks/sell-transaction-requote-required
api-reference/widget/webhooks.openapi.json webhook sell_transaction_requote_required
Sent when a Sell transaction requires a requote because the price moved before the customer's deposit was received.
# sell_transaction_updated
Source: https://dev.moonpay.com/api-reference/widget/webhooks/sell-transaction-updated
api-reference/widget/webhooks.openapi.json webhook sell_transaction_updated
Sent whenever a Sell transaction's status or details change.
# Request signing
Source: https://dev.moonpay.com/api-reference/widget/webhooks/signature
## Checking a webhook signature
MoonPay signs the webhook events and requests we send to your endpoints. We do so by including a signature in each event’s `Moonpay-Signature-V2` header. This allows you to validate that the events and requests were sent by MoonPay, not by a third party.
Before you can verify `Moonpay-Signature-V2` signatures for webhook events, you need to retrieve your webhook API key from the [Developers page](https://dashboard.moonpay.com/developers/) on the MoonPay dashboard.
The `Moonpay-Signature-V2` header contains a timestamp and one signature. The timestamp is prefixed by t=, and the signature is prefixed by s=.
```bash bash Moonpay-Signature-V2: theme={null}
t=1492774577,s=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```
MoonPay generates signatures using a hash-based message authentication code ([HMAC](https://en.wikipedia.org/wiki/HMAC)) with [SHA-256](https://en.wikipedia.org/wiki/SHA-2).
Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.
The value for the prefix `t` corresponds to the timestamp, and `s` corresponds to the signature.
You achieve this by concatenating:
* The timestamp (as a string)
* The character . and
* For a `POST` request, the actual JSON payload (i.e., the request's body). For a `GET` request, the search string (e.g., ?externalCustomerId=adbb317d-cde9-4ebb-93a3-1b271812de06).
Compute a HMAC with the SHA-256 hash function. Use your account's webhook API key as the key, and use the `signed_payload` string as the message in both cases.
Compare the signature in the header to the expected signature.
# transaction_created
Source: https://dev.moonpay.com/api-reference/widget/webhooks/transaction-created
api-reference/widget/webhooks.openapi.json webhook transaction_created
Sent when a customer creates a Buy transaction in the widget.
# transaction_failed
Source: https://dev.moonpay.com/api-reference/widget/webhooks/transaction-failed
api-reference/widget/webhooks.openapi.json webhook transaction_failed
Sent when a Buy transaction fails.
# transaction_updated
Source: https://dev.moonpay.com/api-reference/widget/webhooks/transaction-updated
api-reference/widget/webhooks.openapi.json webhook transaction_updated
Sent whenever a Buy transaction's status or details change.
# virtual_account_status_updated
Source: https://dev.moonpay.com/api-reference/widget/webhooks/virtual-account-status-updated
api-reference/widget/webhooks.openapi.json webhook virtual_account_status_updated
Sent when the status of a virtual account changes.
# virtual_account_transaction_status_updated
Source: https://dev.moonpay.com/api-reference/widget/webhooks/virtual-account-transaction-status-updated
api-reference/widget/webhooks.openapi.json webhook virtual_account_transaction_status_updated
Sent when the status of a transaction within a virtual account changes.
# Changelog
Source: https://dev.moonpay.com/platform/changelog
Updates to the MoonPay developer platform
**Challenge frame `clientToken` in manual integrations.** The [Challenge
frame](/platform/frames/challenge) reference said `clientToken` is included in
the URL automatically. That's only true when the SDK opens the frame for you.
If you build the challenge URL yourself, append `clientToken` the same way you
already append `channelId`, before setting it as the frame `src`.
Updated references:
* [Challenge frame](/platform/frames/challenge)
* [Manual integration (Web)](/platform/guides/manual-integration/web#challenge-handling)
* [Manual integration (Android)](/platform/guides/manual-integration/android#challenge-handling)
* [Manual integration (iOS)](/platform/guides/manual-integration/ios#challenge-handling)
* [Manual integration (React Native)](/platform/guides/manual-integration/react-native#challenge-component)
* [Manual integration (Flutter)](/platform/guides/manual-integration/flutter#challenge-widget)
**Guest checkout limit upgrade.** A guest checkout customer who quotes for more
than their spending limit can now raise that limit with their date of birth and
the last four digits of their Social Security number, without completing full
verification. The buy quote returns `executable: false` plus a `challenge`
object when the customer is eligible. Render the challenge frame at
`challenge.url`, then request the quote again. The frame runs a new
`guest_checkout_limit_upgrade` flow and emits `complete` with a `status` of
`upgraded` or `rejected`. Needs `@moonpay/platform-sdk-web` or
`@moonpay/platform-sdk-react-native` 1.15.2 or later, and the capability
enabled on your account.
Updated references:
* [Guest checkout](/platform/guides/guest-checkout#upgrade-a-guest-account)
* [Handle challenges](/platform/guides/handling-challenges)
* [Challenge frame](/platform/frames/challenge)
* [client.setupChallenge()](/platform/sdk-reference/web/setup-challenge)
* [MoonPayChallenge](/platform/sdk-reference/react-native/components/moonpay-challenge)
* [client.getQuote()](/platform/sdk-reference/web/get-quote)
* [Quote](/api-reference/platform/objects-and-types/quote)
**Bank transfers on the Platform SDK.** You can now accept bank-transfer
payments (SEPA for EUR) through the same headless Buy
frame you use for cards. Quote with `paymentMethod.type` set to `"sepa"`,
open the frame, and read the transaction `id` from the `complete`
event. The transaction then carries a `bankTransferDepositInfo` object with the
account details and payment reference. Your app renders those details natively;
MoonPay does not render the deposit UI. Poll `getTransaction` for status.
Bank transfers use floating pricing: quotes are estimates until the customer's
funds settle. The quote now carries an `exchangeRateType` enum (`"floating"` for
bank transfers, `"fixed"` for card and wallet methods) that pairs with the
`exchangeRate` field. When `exchangeRateType` is `"floating"`, render the estimated crypto amount
with a tilde (`~`) and tell the customer the final amount is set at settlement.
Bank transfers don't support DeFi assets — a bank-transfer quote for a `caip19`
destination fails with `400`. Offer a card or wallet payment method for those
assets instead.
New pages:
* [Pay with bank transfer](/platform/guides/pay-with-bank-transfer)
Updated references:
* [client.getQuote() (React Native)](/platform/sdk-reference/react-native/get-quote#exchange-rate-type)
* [client.getTransaction() (React Native)](/platform/sdk-reference/react-native/get-transaction#bank-transfer-deposit-details)
* [client.setupBuy() (React Native)](/platform/sdk-reference/react-native/setup-buy#bank-transfer)
* [client.getPaymentMethods() (React Native)](/platform/sdk-reference/react-native/get-payment-methods)
* [client.getQuote() (Web)](/platform/sdk-reference/web/get-quote#exchange-rate-type)
* [client.getTransaction() (Web)](/platform/sdk-reference/web/get-transaction#bank-transfer-deposit-details)
* [client.setupBuy() (Web)](/platform/sdk-reference/web/setup-buy#bank-transfer)
* [client.getPaymentMethods() (Web)](/platform/sdk-reference/web/get-payment-methods)
* [Choose a payment method](/platform/guides/payment-methods#compare-payment-methods)
* [Quote object](/api-reference/platform/objects-and-types/quote#exchange-rate-type)
* [Transaction object](/api-reference/platform/objects-and-types/transaction#bank-transfer-deposit-info)
* [Payment Method object](/api-reference/platform/objects-and-types/payment-method#payment-method-types)
***
**Simulate bank-transfer settlement in test mode.** Bank-transfer buy
transactions (SEPA for EUR) settle asynchronously when the customer's deposit
arrives, which never happens in test mode, so a `pending` transaction could not
complete without sending real money. A new sandbox-only endpoint drives a
bank-transfer transaction to a terminal state without real funds. Choose an
`outcome`: `settled` completes the transaction, or `timeout` fails it exactly as
a real 7-day timeout does.
Settlement is asynchronous, so poll `getTransaction` for the terminal state.
New pages:
* [Simulate bank-transfer settlement](/api-reference/platform/endpoints/transactions/simulate-bank-transfer)
Updated references:
* [Test mode](/platform/overview/test-mode#bank-transfers)
**Guest checkout with Google Pay.** Guest checkout now covers Apple Pay and
Google Pay. New customers buy with either wallet before they have a MoonPay
account. Recognition still uses `capabilities.guestCheckout`. Google Pay uses
`setupGooglePay` and `/platform/v1/google-pay`. Second-factor and KYC step-up
both surface as the same `challenge` event; a limit-exceeded purchase fails
with `failureCode: "transactionNotAllowed"`.
Updated references:
* [Guest checkout](/platform/guides/guest-checkout)
* [Pay with Google Pay](/platform/guides/pay-with-google-pay)
* [Google Pay frame](/platform/frames/google-pay)
* [Apple Pay frame](/platform/frames/apple-pay)
* [Handle challenges](/platform/guides/handling-challenges)
* [Choose a payment method](/platform/guides/payment-methods)
**Frame handshake failures are no longer silent.** If a frame's handshake ack
comes from an origin that isn't on your allowlist, the frame now sends a
`generic` error and closes the channel instead of leaving you waiting on a
handshake that never completes.
Updated references:
* [Frames overview](/platform/frames/overview#lifecycle)
**Supported currencies and MoonPay Gateway overview.** A new Supported
currencies page brings together MoonPay's on-ramp and off-ramp currency table
with an overview of MoonPay Gateway, including its benefits, purchase flow, and
support for DeFi tokens on Solana, Ethereum, Base, and HyperCore.
New pages:
* [Supported currencies](/platform/overview/supported-currencies)
**Payment presentation and fee behavior clarified.** Going Live requirements
now distinguish the contextual quote summary from the required fee breakdown
across Apple Pay, Google Pay, cards, the buy button, and the widget. Core
concepts now includes an illustrative side-by-side fee-behavior comparison.
Updated references:
* [Going Live](/platform/overview/going-live)
* [Core concepts](/platform/overview/core-concepts#fee-behavior)
* [Pay with Apple Pay](/platform/guides/pay-with-apple-pay)
* [Pay with Google Pay](/platform/guides/pay-with-google-pay)
* [Pay with card](/platform/guides/pay-with-card)
* [Pay with the buy button](/platform/guides/pay-with-buy-button)
* [Pay with widget](/platform/guides/pay-with-widget)
***
**Verification tiers documented.** A new
[Verification tiers](/platform/guides/verification-tiers) page is the public
reference for what customers complete at each KYC tier, per region: the steps
each tier adds, the regional tier ladders, and how higher tiers unlock higher
purchase limits.
New pages:
* [Verification tiers](/platform/guides/verification-tiers)
**DeFi tokens on the Platform API.** You can now discover and buy DeFi tokens
through Gateway. The new list assets endpoint returns both CeFi and DeFi tokens,
each with its CAIP-19 identifier and on-chain metadata. To quote a DeFi token,
pass its `caip19` as the destination instead of a `code`, since a DeFi token's
`code` is not unique. DeFi buys also surface the `gateway-token` payment
disclosure documented in Going Live.
New pages:
* [List assets](/api-reference/platform/endpoints/assets/list)
Updated references:
* [Asset](/api-reference/platform/objects-and-types/asset)
* [Quote](/api-reference/platform/objects-and-types/quote)
* [Get a quote](/api-reference/platform/endpoints/quotes/get)
* [Going Live](/platform/overview/going-live)
***
**`blocks.moonpay.com` references updated to `platform.moonpay.com`.**
Frame URLs, iframe/WebView `src` examples, and reference implementation
snippets that still pointed at the deprecated `blocks.moonpay.com` domain now
use `platform.moonpay.com`, matching the SDK's `DEFAULT_FRAME_BASE_URL`.
Updated references:
* [Widget frame](/platform/frames/widget)
* [Buy frame](/platform/frames/buy)
* [Buy button frame](/platform/frames/buy-button)
* [Apple Pay frame](/platform/frames/apple-pay)
* [Google Pay frame](/platform/frames/google-pay)
* [Add Card frame](/platform/frames/add-card)
* [Connect frame](/platform/frames/connect)
* [Check frame](/platform/frames/check)
* [Reset frame](/platform/frames/reset)
* [Configure frame appearance](/platform/guides/presentation-and-appearance)
* [Handle challenges](/platform/guides/handling-challenges)
* [Test mode](/platform/overview/test-mode)
* [Manual integration — Web](/platform/guides/manual-integration/web)
* [Manual integration — React Native](/platform/guides/manual-integration/react-native)
* [Manual integration — Flutter](/platform/guides/manual-integration/flutter)
* [Manual integration — Android](/platform/guides/manual-integration/android)
* [Manual integration — iOS](/platform/guides/manual-integration/ios)
* [SDK reference implementation — Web](/platform/sdk-reference/reference-implementation/web)
* [SDK reference implementation — React Native](/platform/sdk-reference/reference-implementation/react-native)
* [SDK reference implementation — Flutter](/platform/sdk-reference/reference-implementation/flutter)
***
**`gateway-token` disclosure described in the API reference.** The
[Get a quote](/api-reference/platform/endpoints/quotes/get) endpoint reference
now describes the `gateway-token` disclosure id inline with the other
disclosures, matching the [Going Live](/platform/overview/going-live) copy.
Updated references:
* [Get a quote](/api-reference/platform/endpoints/quotes/get)
***
**Manual integration guides accept `externalTransactionId`.** The buy, Apple
Pay, Google Pay, and widget frame examples across every platform's manual
integration guide now take an optional `externalTransactionId` and forward it
as an initialization parameter, matching the [buy](/platform/frames/buy),
[Apple Pay](/platform/frames/apple-pay), [Google
Pay](/platform/frames/google-pay), and [widget](/platform/frames/widget)
frame references.
Updated references:
* [Manual integration — Web](/platform/guides/manual-integration/web)
* [Manual integration — Android](/platform/guides/manual-integration/android)
* [Manual integration — iOS](/platform/guides/manual-integration/ios)
* [Manual integration — React Native](/platform/guides/manual-integration/react-native)
* [Manual integration — Flutter](/platform/guides/manual-integration/flutter)
**Device and browser support for Apple Pay and Google Pay.** The Pay with Apple
Pay and Pay with Google Pay guides now have Device and browser support
sections. They cover which browsers and devices each payment method works on,
what embedding the frame in a native app requires (`WKWebView` dialog handling
on iOS, Payment Request API setup in Android WebView), and runtime detection
through the frame's `unsupported` event. The Apple Pay guide also documents
that the Apple Pay frame doesn't offer Apple's cross-device flow, where the
customer scans a QR code with their iPhone, and that the widget does.
Updated references:
* [Pay with Apple Pay](/platform/guides/pay-with-apple-pay)
* [Pay with Google Pay](/platform/guides/pay-with-google-pay)
***
**`apple_pay` quotes render Apple Pay in the widget.** The Pay with widget
guide no longer says an `apple_pay` quote renders the card form. The widget
now renders Apple Pay for `apple_pay` quotes, including the cross-device QR
flow in browsers the headless frame doesn't support.
Updated references:
* [Pay with widget](/platform/guides/pay-with-widget)
***
**Supported testnet list corrected.** Solana test transactions run on Devnet,
not Testnet, and Binance Coin is no longer available in test mode. Bitcoin Cash
and the XRP Ledger (`XRP` and `RLUSD`) are supported and now documented. The
[test assets](/platform/overview/test-mode#test-assets) table
also lists asset codes, links the
[MoonPayToken](https://sepolia.etherscan.io/address/0x699cfe8997d647d03325ef4bfd039d5bb0984a17)
contract that ERC-20 test transfers deliver in place of the real token, and
documents that test-mode purchases deliver 1/100th of the quoted amount.
Updated references:
* [Test mode](/platform/overview/test-mode#test-assets)
* [Sandbox testing guide](/widget/sandbox-testing#sandbox-token-and-testnet-support)
* [Widget FAQs](/widget/faqs#which-currencies-does-the-sandbox-support)
**Payment disclosure IDs documented.** The `paymentDisclosures[].id` field on
a buy quote now lists every known disclosure identifier and what each one
means, instead of an undocumented plain string.
Updated references:
* [Get a quote](/api-reference/platform/endpoints/quotes/get)
* [Quote](/api-reference/platform/objects-and-types/quote)
***
**KYC data requirements and the Auth frame are now in the sidebar.** The
per-country [KYC data requirements](/platform/guides/kyc-data-requirements)
reference now appears in the Customers group and is indexed for search, and the
[Auth frame](/platform/frames/auth) — the entry point for onboarding via API —
now appears under Frames → Connections.
Updated references:
* [KYC data requirements](/platform/guides/kyc-data-requirements)
* [Auth](/platform/frames/auth)
***
**Check frame documentation matches the SDK.** The
[Check frame](/platform/frames/check) now documents the `skipKyc`
initialization parameter used by Customer API integrations and the
`termsAcceptanceRequired` connection status, matching the SDK's `Connection`
union. The [Hosted onboarding](/platform/guides/connect-a-customer) guide's
connection statuses section covers `termsAcceptanceRequired` too.
Updated references:
* [Check](/platform/frames/check)
* [Hosted onboarding](/platform/guides/connect-a-customer#connection-statuses)
***
**Live API data called out as canonical.** Pages that enumerate per-country
requirements or regional availability now carry a callout pointing to the live
API response — `kyc.requirements` on the customer, capabilities on the
connection — as the definitive answer for a given customer.
Updated references:
* [KYC data requirements](/platform/guides/kyc-data-requirements)
* [Onboarding via API](/platform/guides/customer-api)
* [Guest checkout](/platform/guides/guest-checkout)
* [Choose a payment method](/platform/guides/payment-methods)
**Per-partner capability enablement documented.** Some Platform API
capabilities must be enabled on your account by MoonPay before you can call
them. Requests to an endpoint for a capability that is not enabled fail with a
plain `404` (`Cannot POST /...`) rather than a permissions error. The API
reference now calls this out in a new
[Capability enablement](/api-reference/platform/documentation/using-the-api#capability-enablement)
note at the top of the Using the Platform API page, the introduction mentions
it, and every Platform API endpoint reference page carries a short reminder.
Updated references:
* [Using the Platform API](/api-reference/platform/documentation/using-the-api)
* [Introduction](/platform/overview/introduction)
* [Platform API reference](/api-reference/platform)
***
**Leftover preview wording removed.** The account setup callout on the
requirements and credentials pages no longer refers to the preview program.
Updated references:
* [Requirements](/platform/overview/requirements)
* [API and SDK credentials](/platform/guides/api-and-sdk-credentials)
**Pay with widget and payment method type corrections.** The Pay with widget
guide no longer hardcodes a list of supported `paymentMethod.type` values —
several were incorrect — and its example now uses the correct `card` type. The
guide and the SDK reference pages for quotes and payment methods now point to
the [Get a quote API reference](/api-reference/platform/endpoints/quotes/get),
which renders the enum from the live OpenAPI spec, as the single source of
truth. The guide and the `setupWidget` SDK reference also state that the widget
requires an executable quote — pass both a `paymentMethod` and a wallet — and
that a non-executable quote will not render. The widget collects payment for
the method you pass; it does not present payment-method selection.
Updated references:
* [Pay with widget](/platform/guides/pay-with-widget)
* [client.setupWidget() (Web)](/platform/sdk-reference/web/setup-widget)
* [client.setupWidget() (React Native)](/platform/sdk-reference/react-native/setup-widget)
* [getQuote() (Web)](/platform/sdk-reference/web/get-quote)
* [getQuote() (React Native)](/platform/sdk-reference/react-native/get-quote)
* [getPaymentMethods() (Web)](/platform/sdk-reference/web/get-payment-methods)
* [getPaymentMethods() (React Native)](/platform/sdk-reference/react-native/get-payment-methods)
* [`` (React Native)](/platform/sdk-reference/react-native/components/moonpay-widget)
**Provide the React Native session token after mount.** The `sessionToken` prop
on `` is now optional. Mount the provider without it and call
`initialize()` from `useMoonPay()` once your server returns the token — handy
when you fetch it after the customer signs in. A new `isInitialized` flag
reports when the SDK has a token; connection methods called before then return
an `err()` result. Passing the `sessionToken` prop still works unchanged.
Updated references:
* [React Native — `MoonPayProvider`](/platform/sdk-reference/react-native/provider)
* [React Native — `useMoonPay()`](/platform/sdk-reference/react-native/use-moonpay)
***
**Widget frame example accepts `externalTransactionId`** — the manual web
integration guide's widget frame example now takes an optional
`externalTransactionId` and forwards it as an initialization parameter,
matching the buy frame example in the same guide and the
[widget frame](/platform/frames/widget) reference.
Updated references:
* [Manual integration — Web](/platform/guides/manual-integration/web)
**Customer API in the API reference.** The `/platform/v1/customers` endpoints
now have dedicated API reference pages: get a customer, submit KYC data, get
a file upload URL, confirm uploaded files, and export customer data. Two new
type pages document the Customer object (including KYC status and
requirements) and the customer export payload.
New pages:
* [Get a customer](/api-reference/platform/endpoints/customers/get)
* [Submit KYC data](/api-reference/platform/endpoints/customers/submit-kyc)
* [Get a file upload URL](/api-reference/platform/endpoints/customers/get-upload-url)
* [Confirm uploaded files](/api-reference/platform/endpoints/customers/submit-files)
* [Export customer data](/api-reference/platform/endpoints/customers/export)
* [Customer](/api-reference/platform/objects-and-types/customer)
* [Customer export](/api-reference/platform/objects-and-types/customer-export)
***
**Due-diligence questionnaires and reporting currency.** The KYC data
requirements reference now documents the `questionnaires` requirement
category: the Customer Due Diligence and Enhanced Due Diligence answer
fields, and the monetary-amount shape they share. Monetary answers such as
`grossAnnualIncome`, `expectedTransactionAmountPerMonth`, and `netWorth` are
denominated in the reporting currency for the customer's
`residentialAddress.country`: GBP for GBR, AUD for AUS and NZL, EUR for EEA
countries, and USD everywhere else. The API rejects a mismatched `currency`
with a 400 validation error. The Onboarding via API guide now shows how to
submit questionnaire answers.
***
**`requiredFields` on requirement entries.** The Onboarding via API guide now
documents which requirement categories populate `requiredFields` when
incomplete — `basicDetails`, `residentialAddress`, `taxIdentifiers`, and
`questionnaires` — and its get-a-customer example shows the `questionnaires`
entry naming the outstanding questionnaire types.
Updated references:
* [KYC data requirements](/platform/guides/kyc-data-requirements)
* [Onboarding via API](/platform/guides/customer-api)
***
**Terms acceptance — new guide for the API-driven path.** Partners on
the API-driven onboarding path must present MoonPay's Terms of Use and
Privacy Policy in their own UI before the customer's first transaction. The
new guide covers the two presentation methods, recording
acceptance with `termsAcceptedAt` when creating a session, handling
`termsAcceptanceRequired`, re-presenting the terms when they change, and the
related biometric-consent and phone-verification steps.
New pages:
* [Terms acceptance](/platform/guides/terms-acceptance)
Updated references:
* [Onboarding via API](/platform/guides/customer-api)
* [Guest checkout](/platform/guides/guest-checkout)
* [Auth frame](/platform/frames/auth)
* [Web SDK — `getConnection()`](/platform/sdk-reference/web/get-connection)
* [React Native — `getConnection()`](/platform/sdk-reference/react-native/get-connection)
* [KYC data requirements](/platform/guides/kyc-data-requirements)
* [Going live](/platform/overview/going-live)
**`externalTransactionId` on the buy widget.** The widget frame now accepts
an optional `externalTransactionId` — a partner-assigned identifier for the
transaction attempt, useful for reconciliation. It matches the field already
available on the buy, Google Pay, Apple Pay, and buy-button frames.
`setupWidget()` forwards it, and on the React Native ``
component the same value is exposed as a mount-time prop.
Updated references:
* [Widget frame](/platform/frames/widget)
* [Web SDK — `setupWidget()`](/platform/sdk-reference/web/setup-widget)
* [React Native — `client.setupWidget()`](/platform/sdk-reference/react-native/setup-widget)
* [React Native — ``](/platform/sdk-reference/react-native/components/moonpay-widget)
**`externalTransactionId` on payment-button setup.** The Apple Pay and buy
button setup methods now accept an optional `externalTransactionId` — a
partner-assigned identifier for the transaction attempt, useful for
reconciliation. It matches the field already available on Google Pay. On the
React Native `` and `` components the
same value is exposed as a mount-time prop.
Updated references:
* [Web SDK — `setupApplePay()`](/platform/sdk-reference/web/setup-apple-pay)
* [Web SDK — `setupBuyButton()`](/platform/sdk-reference/web/setup-buy-button)
* [React Native — ``](/platform/sdk-reference/react-native/components/moonpay-apple-pay-button)
* [React Native — ``](/platform/sdk-reference/react-native/components/moonpay-buy-button)
**Platform guides reorganized around onboarding paths.** The Platform tab now
groups guides into Onboard customers, Accept payments, and Integration
essentials. A new decision page explains the three ways customers can
onboard: the hosted connect flow, the Customer API, or guest checkout. The
Customer API guide now documents the Auth frame entry path
(`getConnection({ skipKyc: true })` + `setupAuth()`) instead of routing
through the hosted connect flow. Customer export moved to its own page, and
the per-country KYC data requirements reference is now in the sidebar.
New pages:
* [Choose an onboarding path](/platform/guides/onboarding-paths)
* [Choose a payment method](/platform/guides/payment-methods)
* [Export customer data](/platform/guides/export-customer-data)
* [KYC data requirements](/platform/guides/kyc-data-requirements)
Updated references:
* [Onboarding via API](/platform/guides/customer-api)
* [Guest checkout](/platform/guides/guest-checkout)
* [Core concepts](/platform/overview/core-concepts)
* [Handle challenges](/platform/guides/handling-challenges)
**`buttonPressed` frame event.** The Apple Pay, Google Pay, and Buy Button
frames now emit a `buttonPressed` event the instant the customer taps the
native pay button, before the OS presents the payment sheet (PassKit on iOS,
the Google Pay sheet on Android). It is an intent-to-buy signal with no
payload, routed by `channelId`. It is distinct from `complete`: it fires
earlier and unconditionally on tap, and still fires if the customer opens the
payment sheet and then cancels. Listen for `complete` for the transaction
outcome.
Updated references:
* [Apple Pay frame](/platform/frames/apple-pay)
* [Google Pay frame](/platform/frames/google-pay)
* [Buy button frame](/platform/frames/buy-button)
***
**`feeBehavior` on buy quotes.** The buy-quote endpoint accepts an optional
`feeBehavior` field, `"inclusive"` or `"exclusive"`, that controls how fees
relate to `source.amount`. With `"inclusive"` (the default) the customer
pays exactly `source.amount` and fees are carved out of it. With
`"exclusive"` fees are added on top, so the full `source.amount` is converted
and the customer receives more crypto. It applies only when you quote by
`source.amount`; quotes by `destination.amount` are always fees-inclusive. The
response echoes the effective `feeBehavior`. Omitting the field keeps the
existing behavior.
Updated references:
* [Quotes concepts](/platform/overview/core-concepts#quotes)
* [Quote object](/api-reference/platform/objects-and-types/quote)
* [Web SDK — `getQuote()`](/platform/sdk-reference/web/get-quote)
* [React Native SDK — `getQuote()`](/platform/sdk-reference/react-native/get-quote)
***
**Identity SDK methods marked deprecated.** The web SDK's identity methods
(`createIdentity`, `getIdentity`, `updateIdentity`, `verifyIdentity`,
`getIdentityUploadUrl`, `submitIdentityFiles`) are deprecated in favor of
the customerId-keyed Customer API. Each method's reference now links to
its Customer API replacement.
Updated references:
* [Web SDK — Identity methods](/platform/sdk-reference/web/identity)
* [Customer API](/platform/guides/customer-api)
**Terms acceptance instructions corrected for headless partners.** The
`termsAcceptanceRequired` handling docs referenced `POST
/platform/v1/terms/attestations`, an endpoint that no longer exists. The
documented flow is now to capture the acceptance timestamp in your own UI,
pass it as `termsAcceptedAt` when you create a new session, and relaunch the
flow. This requires the Identity or Guest Checkout account capability.
Updated references:
* [Auth frame](/platform/frames/auth)
* [Web SDK — `getConnection()`](/platform/sdk-reference/web/get-connection)
* [React Native SDK — `getConnection()`](/platform/sdk-reference/react-native/get-connection)
***
**Consent capture flow documented for customer export.** The Customer API
guide's export section now covers how partners capture a customer's
consent with the web SDK's `setupCustomerExport()`.
Updated references:
* [Customer API](/platform/guides/customer-api)
**Customer API guide added** — new guide for the customerId-keyed Customer
API: getting a customer's KYC status, submitting outstanding requirements,
uploading identity files, verifying a customer, and exporting a customer's
verified identity to another system with their consent.
New pages:
* [Customer API](/platform/guides/customer-api)
**Conflict signal on connection check** — the check frame's `complete` event
now includes an optional `mismatch` field on the `connectionRequired` payload.
It is present and `true` when the session's email and phone number resolve to
two different MoonPay customers, and absent when there is no conflict — never
`false`. Use it to route the customer through the connect flow proactively,
before rendering payment UI such as Apple Pay, instead of discovering the
conflict at purchase time.
The `CustomerCapabilities` documentation is also corrected:
`capabilities.oneTapApplePay` was removed upstream and is replaced by
`capabilities.guestCheckout`, which is present when guest checkout is enabled
for the partner and the session is a guest-checkout session.
Updated references:
* [Check frame](/platform/frames/check)
* [Connect frame](/platform/frames/connect)
* [Web SDK — `getConnection()`](/platform/sdk-reference/web/get-connection)
* [React Native SDK — `getConnection()`](/platform/sdk-reference/react-native/get-connection)
* [Connect a customer](/platform/guides/connect-a-customer)
* [Manual integration — Web](/platform/guides/manual-integration/web)
* [Manual integration — React Native](/platform/guides/manual-integration/react-native)
* [Manual integration — Flutter](/platform/guides/manual-integration/flutter)
* [Manual integration — Android](/platform/guides/manual-integration/android)
* [Manual integration — iOS](/platform/guides/manual-integration/ios)
**WebView and sandboxed iframe embedding documented** — the Google Pay and
Apple Pay frame reference pages are now the source of truth for embedding these
frames in a WebView or sandboxed iframe. The Google Pay frame documents the
Android WebView requirements for Payment Request API setup and the `sandbox`
attribute values you need when embedding in a sandboxed iframe for PCI DSS v4
compliance. The Apple Pay frame documents the WKWebView requirement to handle
JavaScript dialogs. The Android and iOS manual-integration guides point to
these frame pages.
Updated references:
* [Google Pay frame](/platform/frames/google-pay)
* [Apple Pay frame](/platform/frames/apple-pay)
* [Android manual integration](/platform/guides/manual-integration/android)
* [iOS manual integration](/platform/guides/manual-integration/ios)
***
**Guest checkout with Apple Pay** — new guide for letting customers buy crypto
with Apple Pay before they have a MoonPay account. It covers creating a session
with the customer's identity, detecting the `guestCheckout` capability,
executing the purchase, handling verification challenges, and upgrading a guest
account.
New pages:
* [Guest checkout with Apple Pay](/platform/guides/guest-checkout)
**Reset frame accepts `clientToken`** — the [Reset
frame](/platform/frames/reset) reference now documents `clientToken` (from the
connect flow) alongside `apiKey`. Either one authorizes your domain to embed
the frame via the `frame-ancestors` Content Security Policy; without one the
frame only loads on MoonPay-owned origins. `channelId` remains the only
required parameter.
**Reset frame parameters corrected** — removed the `language` parameter from
the [Reset frame](/platform/frames/reset) reference. The frame is headless and
renders no UI, so the parameter had no effect.
**HYPE sustainability indicators added** — added Hyperliquid (HYPE) to the
[Sustainability transparency](/widget/sustainability-transparency) page with
MiCA sustainability indicators provided by CCRI.
**Test payment cards updated** — revised the test card tables on the [Test
mode](/platform/overview/test-mode) page. Removed all American Express test
cards, since Amex is not a supported payment method, and removed a duplicate
card entry that appeared in more than one table.
**Frame theme parameter documented** — frames accept a `theme` query parameter
set to `dark` or `light` to force a specific appearance. Omit it and the frame
follows the user's system appearance. The parameter is now documented across
the frames that render UI.
Updated references:
* [Widget frame](/platform/frames/widget)
* [Add card frame](/platform/frames/add-card)
* [Apple Pay frame](/platform/frames/apple-pay)
* [Google Pay frame](/platform/frames/google-pay)
* [Challenge frame](/platform/frames/challenge)
**Buy Button documentation** — the buy button now has a full guide and frame
reference. The buy button renders a MoonPay-hosted express-checkout payment
button (Apple Pay, Google Pay, or card) and runs the same buy pipeline as the
headless buy frame.
New pages:
* [Pay with the buy button](/platform/guides/pay-with-buy-button) — guide
* [Buy button frame](/platform/frames/buy-button) — frame reference
The [`setupBuyButton()` reference](/platform/sdk-reference/web/setup-buy-button)
now documents the `ready` event, which the SDK emits once the button is rendered
and ready for the customer to tap.
***
**Credentials payload documented** — the decrypted shape of the `credentials`
string returned by the check and connect frames is now explicitly documented.
Once decrypted, `credentials` is a JSON object with `accessToken`,
`clientToken`, and `expiresAt`. See [API and SDK
credentials](/platform/guides/api-and-sdk-credentials#client-credentials).
**Customer geo fields on connection** — `country`, `administrativeArea`, and
`area` are now returned directly on the `customer` object in the `complete`
event payload (connect and check frames) and in the `Connection` type returned
by `getConnection()`. Use these fields to determine which payment disclosures
apply for the customer's jurisdiction.
`capabilities.ramps.requirements.paymentDisclosures` is deprecated. Read
geography from `customer.country`, `customer.administrativeArea`, and
`customer.area` instead.
Updated references:
* [Connect frame](/platform/frames/connect)
* [Check frame](/platform/frames/check)
* [Web SDK — `getConnection()`](/platform/sdk-reference/web/get-connection)
* [React Native SDK — `getConnection()`](/platform/sdk-reference/react-native/get-connection)
* [Manual integration — Web](/platform/guides/manual-integration/web)
* [Manual integration — React Native](/platform/guides/manual-integration/react-native)
* [Manual integration — Flutter](/platform/guides/manual-integration/flutter)
* [Manual integration — Android](/platform/guides/manual-integration/android)
* [Manual integration — iOS](/platform/guides/manual-integration/ios)
***
**React Native SDK is now available** — the [React Native
SDK](/platform/sdk-reference/react-native/overview)
([`@moonpay/platform-sdk-react-native`](https://www.npmjs.com/package/@moonpay/platform-sdk-react-native))
is published on npm and its reference is now in the Platform sidebar:
* **Provider and hook** —
[``](/platform/sdk-reference/react-native/provider) and
[`useMoonPay()`](/platform/sdk-reference/react-native/use-moonpay) give any
descendant component access to the client.
* **Client methods** — customer connection, [email/OTP
auth](/platform/sdk-reference/react-native/setup-auth), payment methods,
quotes, frame setup (widget, buy, buy button, add card, Apple Pay, Google
Pay, challenge), and transactions — mirroring the [Web
SDK](/platform/sdk-reference/web/overview) surface.
* **[Inline
components](/platform/sdk-reference/react-native/components/overview)** —
every frame also ships as a declarative component
(``, ``, ``, and
more) you render directly in your layout, with a reactive `quote` prop that
updates live frames without remounting. These replace the deprecated
`setupApplePay()`, `setupGooglePay()`, and `setupBuyButton()` client
methods.
The [SDK reference overview](/platform/sdk-reference/overview), [Web SDK
overview](/platform/sdk-reference/web/overview), and [manual
integration](/platform/guides/manual-integration/overview) pages now point
React Native integrators at the SDK instead of a direct frame integration.
***
**Web SDK reference — corrections and 1.0.0 coverage** — the [Web
SDK](/platform/sdk-reference/web/overview) docs were audited against the
`@moonpay/platform-sdk-web` 1.0.0 source and corrected:
* **`getQuote()` input shape** — every example and parameter table now uses
the nested request shape the SDK and API accept (`source: { asset: { code },
amount }`, `destination: { asset: { code } }`, `wallet: { address, tag? }`,
`paymentMethod: { type, id? }`). The previously documented flat shape
(`source: "USD"`, `sourceAmount`, `walletAddress`, string `paymentMethod`)
is rejected by the API. Corrected across the
[SDK reference](/platform/sdk-reference/web/get-quote), all `pay-with-*`
guides, and the [introduction](/platform/overview/introduction).
* **New: [`setupAuth()`](/platform/sdk-reference/web/setup-auth)** — the
lighter-weight email/OTP counterpart to `connect()` for headless and
Identity API integrations, added in SDK 1.0.0.
* **New: [Identity methods](/platform/sdk-reference/web/identity)** —
`createIdentity`, `getIdentity`, `updateIdentity`, `verifyIdentity`,
`getIdentityUploadUrl`, and `submitIdentityFiles`, added in SDK 1.0.0.
* **[`getConnection()`](/platform/sdk-reference/web/get-connection)** — now
documents the optional `skipKyc` flag for headless integrations.
* **[`setupChallenge()`](/platform/sdk-reference/web/setup-challenge)** — the
challenge URL no longer needs to carry a `channelId` query parameter; the
SDK generates one automatically (SDK 1.0.0). Identity verification challenge
URLs can be passed straight through.
* **[`setupAddCard()`](/platform/sdk-reference/web/setup-add-card)** — the
Add Card frame does emit a `ready` event; the docs previously said it
didn't.
* **[`setupApplePay()`](/platform/sdk-reference/web/setup-apple-pay) and
[`setupGooglePay()`](/platform/sdk-reference/web/setup-google-pay)** — the
`SetupApplePayError` / `SetupGooglePayError` unions are
`"configurationError" | "genericError"`; quote problems and wallet
availability surface through `onEvent` instead. The Apple Pay page also
gained the previously undocumented `challenge` event.
* **[`getPaymentMethods()`](/platform/sdk-reference/web/get-payment-methods)** —
the response is `{ data: { paymentMethodConfigs, paymentMethods } }`; the
previously documented `storedCards` field doesn't exist on the wire.
* **Package name and `createClient()` usage** — remaining `@moonpay/platform`
imports and `Result`-wrapped `createClient()` examples in the guides were
updated to `@moonpay/platform-sdk-web` and the synchronous `Client` return.
***
**Check and Connect frame references corrected** — resolved discrepancies in
the [Check](/platform/frames/check) and [Connect](/platform/frames/connect)
frame references to ensure the correct event payloads are documented, and
removed the retired `connection_required` / `connection_pending` /
`connection_unavailable` error codes from the SDK references.
**Quotes now reject assets that aren't available in the active mode** —
[`POST /platform/v1/quotes/buy`](/api-reference/platform/endpoints/quotes/get)
returns a `400 invalid_request` error when the destination asset doesn't
support the mode of your session (test or live). Previously the endpoint
returned a quote even when the asset couldn't be purchased in that mode. The
error includes a field-level detail identifying the asset:
```json theme={null}
{
"code": "invalid_request",
"message": "Invalid request",
"errors": [
{
"field": "destination.asset.code",
"message": "Destination asset is not supported in test mode."
}
]
}
```
In test mode, request quotes for assets on a
[supported testnet](/platform/overview/test-mode#test-assets), such as
`ETH` or `SOL`.
**Platform SDK reference — Web** — the [Web
SDK](/platform/sdk-reference/web/overview) (`@moonpay/platform-sdk-web`) is
now fully documented and wired into the Platform sidebar:
* **Customer connection** —
[`getConnection`](/platform/sdk-reference/web/get-connection),
[`connect`](/platform/sdk-reference/web/connect),
[`resetConnection`](/platform/sdk-reference/web/reset-connection).
* **Payment methods** —
[`getPaymentMethods`](/platform/sdk-reference/web/get-payment-methods),
[`deletePaymentMethod`](/platform/sdk-reference/web/delete-payment-method).
* **Quotes** — [`getQuote`](/platform/sdk-reference/web/get-quote).
* **Frame setup** —
[`setupWidget`](/platform/sdk-reference/web/setup-widget),
[`setupBuyButton`](/platform/sdk-reference/web/setup-buy-button),
[`setupBuy`](/platform/sdk-reference/web/setup-buy),
[`setupAddCard`](/platform/sdk-reference/web/setup-add-card),
[`setupApplePay`](/platform/sdk-reference/web/setup-apple-pay),
[`setupGooglePay`](/platform/sdk-reference/web/setup-google-pay),
[`setupChallenge`](/platform/sdk-reference/web/setup-challenge).
* **Transactions** —
[`getTransaction`](/platform/sdk-reference/web/get-transaction),
[`listTransactions`](/platform/sdk-reference/web/list-transactions).
Web SDK pages were rewritten against the current SDK source: the package name
was corrected across samples (`@moonpay/platform` →
`@moonpay/platform-sdk-web`),
[`createClient()`](/platform/sdk-reference/web/create-client) is now correctly
typed as returning a `Client` directly, API error tables use the real
`DevPlatformApiError` shape (`code` + `message` + optional `errors[]`),
success envelopes use `{ data }` and `{ data, pageInfo }`, and event and
error unions for every `setup*` method match what the SDK actually emits
(including the `kind: "frame"` discriminator on `challenge` events,
`oneTapApplePaySecondFactorRequired` for Apple Pay, and `"unsupported"`
rather than an error when Google Pay is unavailable).
[`getConnection`](/platform/sdk-reference/web/get-connection) documents the
full set of connection statuses (including `termsAcceptanceRequired`) and
`CustomerCapabilities`.
***
**Challenge frame `error` event guidance corrected** — the
[Handle challenges](/platform/guides/handling-challenges) guide previously told
partners to surface the `message` payload from the Challenge frame's `error`
event. That contradicts the [Challenge frame](/platform/frames/challenge)
reference, which marks `code` and `message` as developer-facing and explicitly
not for end-user UI. Updated the guide to recommend logging the `code` and
`message` and showing the customer a generic next step (such as retry or
choose a different payment method) instead.
***
**Buy quotes now return payment disclosure IDs** — the [quote
response](/api-reference/platform/endpoints/quotes/get#response-data-payment-disclosures)
includes a `paymentDisclosures` array of `{ id, version }` objects that
identifies exactly which disclosure(s) to render for a transaction. This
replaces the need to infer disclosure requirements from
`capabilities.ramps.requirements.paymentDisclosures` at connect time. The
`capabilities.ramps.requirements.paymentDisclosures` property is deprecated
and will be removed.
***
**Apple Pay frame failure codes documented** — the [Apple Pay
frame](/platform/frames/apple-pay#failure-codes) reference now lists the
`failureCode` values a failed `complete` event can return
(`applePayMerchantUnavailable`, `transactionNotAllowed`, `validationError`,
`serviceUnavailable`, `authorizationDeclined`, and `unknown`), each with its
default `failureReason` and recommended handling. The [Pay with Apple
Pay](/platform/guides/pay-with-apple-pay) guide shows how to branch on
`failureCode` to drive your error handling.
***
**Google Pay frame failure codes documented** — the [Google Pay
frame](/platform/frames/google-pay#failure-codes) reference now lists the
`failureCode` values a failed `complete` event can return
(`transactionNotAllowed`, `validationError`, `serviceUnavailable`,
`authorizationDeclined`, and `unknown`), each with its default `failureReason`
and recommended handling. The [Pay with Google
Pay](/platform/guides/pay-with-google-pay) guide shows how to branch on
`failureCode` to drive your error handling.
**Test mode documents challenge triggers** — the [Test
mode](/platform/overview/test-mode#triggering-challenges) page now explains
how to force a [challenge](/platform/guides/handling-challenges) in test mode
by setting the buy amount to a specific value: `48` triggers a
wallet-ownership challenge (Apple Pay and card) and `49` triggers a CVV
re-entry challenge (card). These triggers only apply in test mode.
**Challenges and quotes docs corrected** — refreshed the guides and SDK
reference to match how the SDK and API actually behave:
* [Handle challenges](/platform/guides/handling-challenges) is now a
flow-agnostic guide. Challenges are surfaced by frames — today the Apple
Pay, Google Pay, and buy (Pay with card) frames each emit a `challenge`
event whose URL is loaded into the dedicated
[Challenge frame](/platform/frames/challenge). The old example showing
`challenge` nested inside a quote response was incorrect and has been
removed.
* [Core concepts → Quotes](/platform/overview/core-concepts#quotes) no longer
describes "price quotes" and "executable quotes" as two types. There is a
single quote with an `executable` boolean — see the
[quotes API reference](/api-reference/platform/endpoints/quotes/get) for the
request fields required to receive `executable: true`.
* The per-flow guides ([Apple Pay](/platform/guides/pay-with-apple-pay),
[Google Pay](/platform/guides/pay-with-google-pay)) and the
[`getQuote`](/platform/sdk-reference/web/get-quote) / setup-method SDK
reference pages now use this single-quote framing.
**Challenge frame docs cover the identity flow** — updated the [Challenge
frame](/platform/frames/challenge) reference so the `complete`, `cancelled`,
and `error` payloads describe both upstream flows (buy and identity):
* `complete` and `cancelled` payloads are now documented as discriminated
unions over `flow: "buy" | "identity"`. The `identity` variant carries
`identityId` on `complete` and no extra fields on `cancelled`.
* `error` codes refreshed to match the frame's wire format (`invalid_token`,
`unsupported_flow`, `invalid_challenge`) and the payload type loosened to
`code: string` to reflect that additional codes can be propagated from the
upstream flow.
**Frame theming** — every frame now accepts a `brandColor` URL parameter that
seeds a runtime-derived palette across the entire UI, plus an optional
`customTheme` for border radius, color scheme lock, and granular palette
control. See the new [theming guide](/platform/frames/theming) for accepted
formats and examples per frame.
**Challenge events for Apple Pay and Google Pay** — the [Apple Pay
frame](/platform/frames/apple-pay) and [Google Pay
frame](/platform/frames/google-pay) now emit a `challenge` event when
verification is required before a transaction can proceed. Challenge handling
added to the [Apple Pay SDK
reference](/platform/sdk-reference/web/setup-apple-pay), [Google Pay SDK
reference](/platform/sdk-reference/web/setup-google-pay), and manual
integration guides for
[web](/platform/guides/manual-integration/web),
[iOS](/platform/guides/manual-integration/ios), [React
Native](/platform/guides/manual-integration/react-native), and
[Flutter](/platform/guides/manual-integration/flutter).
***
**TransactionStatus fix** — corrected the `TransactionStatus` enum value from
`complete` to `completed` in the [Apple Pay
frame](/platform/frames/apple-pay) and [Google Pay
frame](/platform/frames/google-pay) references.
**Google Pay** — new [Pay with Google
Pay](/platform/guides/pay-with-google-pay) guide, [Google Pay
frame](/platform/frames/google-pay) reference, and
[`setupGooglePay`](/platform/sdk-reference/web/setup-google-pay) SDK method.
Covers the standalone Google Pay frame integration and manual integration for
[web](/platform/guides/manual-integration/web),
[Android](/platform/guides/manual-integration/android), [React
Native](/platform/guides/manual-integration/react-native), and
[Flutter](/platform/guides/manual-integration/flutter).
**EEA disclosures** — published the exact verbiage required above the Apple
Pay frame and the card Pay button for customers located in the EEA, with
separate text for standard crypto-assets and non-MiCA-compliant stablecoins
(USDT, cUSD, DAI, PYUSD). See [Going
Live](/platform/overview/going-live#disclosures-eea).
**API key header** — server-to-server requests now authenticate with the
`X-Api-Key` header. See [Using the Platform
API](/api-reference/platform/documentation/using-the-api#server-side-authentication)
and [API and SDK
credentials](/platform/guides/api-and-sdk-credentials#secret-key).
**Payment-disclosure geography** — `paymentDisclosures` now identifies the
customer's geography. `country` is the ISO 3166-1 alpha-3 code,
`administrativeArea` is included for US state-level disclosures, and `area`
can identify broader regions such as `"EEA"`.
**Going Live** — documented the acceptance criteria for the global rollout
(except UK). Each requirement is now tagged with a geo, and new sections cover
Cards, Identity API, and EEA Apple Pay disclosures (verbiage pending). See
[Going Live](/platform/overview/going-live).
**Preview removed** — the Developer Platform is now generally available. The
"currently in preview" notice has been removed from all docs pages.
***
**Card payments** — new [Pay with card](/platform/guides/pay-with-card) guide
and frame references for [Add Card](/platform/frames/add-card),
[Buy](/platform/frames/buy), and [Challenge](/platform/frames/challenge).
Covers the full integration: listing and managing stored cards, getting a card
quote, executing transactions via the headless buy frame, and handling
verification challenges (SCA, 3DS, CVC re-entry, KYC). Also adds the [Delete
payment method](/api-reference/platform/endpoints/payment-methods/delete) API
endpoint.
**Reset frame** — new headless frame at `/platform/v1/reset` that lets you log
a customer out by clearing their authentication state on MoonPay's domain.
Reports completion via postMessage. See [Reset](/platform/frames/reset).
***
**Unified docs site** — Platform and Widget docs now live under a single
Mintlify site with separate top-level tabs. Legacy `/overview/*`, `/guides/*`,
`/frames/*`, `/sdk-reference/*`, and `/api-reference/*` URLs redirect to their
new `/platform/*` paths.
**Manual integration fixes** — corrected the WebView samples to post the
payload as a string, base64-decode credentials, and handle JS dialogs. Affects
[web](/platform/guides/manual-integration/web),
[iOS](/platform/guides/manual-integration/ios),
[Android](/platform/guides/manual-integration/android), [React
Native](/platform/guides/manual-integration/react-native), and
[Flutter](/platform/guides/manual-integration/flutter) guides.
***
**Fee language** updated across guides to clarify partner vs. ecosystem fees.
**Revoke session** — `DELETE /platform/v1/sessions` invalidates an active
session token. See [Revoke a
session](/api-reference/platform/endpoints/sessions/revoke).
**Sessions endpoint renamed** — `POST /platform/v1/session` is now `POST
/platform/v1/sessions` (plural). The old path continues to work; new
integrations should use the plural form.
**Apple Pay going-live guide** — added production-readiness details for the
Apple Pay frame, including merchant verification and domain registration
steps. See [Pay with Apple Pay](/platform/guides/pay-with-apple-pay).
**`customerId` in connect/check payload** — the `complete` postMessage event
now documents `customer.id`, and the session-create request documents the
`customerId` field for returning users (skip the connect flow when you already
have one).
**Manual integration credentials** — the manual integration guides now use
`clientToken` (not `sessionToken`) to initialize frames, matching the
early-credential-issuance flow.
**US payment-disclosure rails narrowed** — the `paymentDisclosures` capability
is now documented as scoped to NY and WA within the US. Customers in other US
states will not receive a `paymentDisclosures` requirement.
***
**HKDF examples** — manual integration code samples consistently pass
`undefined` for the `info` parameter of `hkdf()`.
**Acceptance criteria page** — new compliance checklist outlining the
requirements for going live. See [Going live](/platform/overview/going-live).
***
**Manual integration moved** — the per-platform manual integration pages now
live under [Guides → Manual
Integration](/platform/guides/manual-integration/overview).
***
**Using agents** — new page covering MCP client setup for Claude Code and
Codex, so you can wire your agent to MoonPay's developer docs. See [Using
agents](/platform/overview/using-agents).
***
**Manual integration docs** — initial documented integrations for web, iOS,
Android, React Native, and Flutter (graduated from hidden drafts).
**Early credential issuance** — the credentials-flow guide now reflects that
`clientToken` and `accessToken` are issued before authentication completes, so
partners can initialize sensitive frames sooner.
***
**Connect-flow low-friction callout** — added guidance to the connect-flow
guide about minimizing handoffs back to MoonPay-hosted UI.
***
**Apple Pay frame height** — corrected the documented frame height for the
Apple Pay frame.
Quote API: `fees.partner` field renamed to `fees.ecosystem`.
Apple Pay: documented test-mode and frame sandbox requirements, plus a
corrected frame size.
Customer capabilities and payment-disclosure requirements expanded.
New widget-fallback frame and integration guide.
Frames protocol: documented `version: 2` and added the versioning section.
Frame URLs migrated from `/v2/*` to `/platform/*` across all docs.
OpenAPI now served live from `https://api.moonpay.com/platform/openapi.json`
rather than checked into the repo.
Removed historical `pk_test` / `pk_live` references in favor of the new
credential model.
Initial Apple Pay frame size and frame sandbox requirements published.
Mintlify upgrade and a content-style-guide pass across guides and frames docs.
# Add Card
Source: https://dev.moonpay.com/platform/frames/add-card
Details on working with the Add Card frame used in the [Pay with card](/platform/guides/pay-with-card) flow.
## URL
```
https://platform.moonpay.com/platform/v1/add-card
```
## Requirements
### Size
Render the frame in a modal or sheet. Width and height are flexible — size the
container to fit your UI.
## Initialization parameters
| Property | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | The [client token](/platform/guides/api-and-sdk-credentials#client-token) returned from the [connect flow](/platform/guides/connect-a-customer). |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `theme` | `string` | | Pass `dark` or `light` to force a specific appearance. If you omit this, the frame uses the user's system appearance. |
## Events
All events are dispatched using the message pattern described in the [frames
protocol](/platform/frames/overview#frames-protocol#messages). Below are the
event payloads specific to the Add Card 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The frame finished loading and the card input UI is fully rendered.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `complete`
The card was added successfully. Use `card.id` to get a quote without
re-fetching payment methods.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"card": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "card",
"cardType": "credit",
"brand": "visa",
"last4": "4242",
"expirationMonth": "12",
"expirationYear": "2027",
"availability": { "active": true }
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type CardResponse = {
id: string;
type: "card";
cardType: "credit" | "debit" | "unknown";
brand: "visa" | "mastercard" | "maestro" | "american_express" | "other";
last4: string;
expirationMonth: string;
expirationYear: string;
availability: { active: boolean };
};
type AddCardCompleteEvent = Message<{
kind: "complete";
payload: {
card: CardResponse;
};
}>;
```
#### `error`
An error occurred during card addition.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "generic",
"message": "Card creation failed."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AddCardErrorEvent = Message<{
kind: "error";
payload: {
code: "configurationError" | "generic";
/** A developer-facing error message. Not intended to be rendered in UI. */
message: string;
};
}>;
```
| Code | Description |
| -------------------- | -------------------------------- |
| `configurationError` | Missing or invalid `clientToken` |
| `generic` | Card creation failed |
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
# Apple Pay
Source: https://dev.moonpay.com/platform/frames/apple-pay
Details on working with the [Apple Pay](/platform/guides/pay-with-apple-pay) frame, including [guest checkout](/platform/guides/guest-checkout).
## URL
```html theme={null}
https://platform.moonpay.com/platform/v1/apple-pay
```
## Requirements
### Size
The frame container **height must be 44px**. Width is flexible; the Apple Pay button inside the frame uses 100% of the container width.
### Permissions
The `payment` [permission policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy#iframes) is required.
In [test mode](/platform/overview/test-mode#apple-pay), the frame uses `window.confirm` to simulate the Apple Pay payment. If your iframe uses the [`sandbox`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox) attribute, you will need to include `allow-modals`.
```tsx Example theme={null}
```
### WKWebView
To embed the Apple Pay frame in a native iOS app, you render it inside a `WKWebView`. The frame communicates with the host app through the `postMessage`-based [frames protocol](/platform/frames/overview#frames-protocol). See [Events](#events) below.
You must handle JavaScript dialogs. The frame uses `window.confirm`, for example to present the simulated Apple Pay sheet in [test mode](/platform/overview/test-mode#apple-pay). By default a `WKWebView` silently dismisses `window.confirm`, `alert`, and `prompt`: the call returns `false` 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 confirm panel, for example as a `UIAlertController`:
```swift theme={null}
extension MoonPayFrameViewController: WKUIDelegate {
func webView(_ webView: WKWebView,
runJavaScriptConfirmPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo,
completionHandler: @escaping (Bool) -> Void) {
// Present the dialog and return the customer's choice.
}
}
```
Wire this even if you only ship live mode, since the default behaviour 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](/platform/guides/manual-integration/ios#apple-pay-frame).
## Initialization parameters
| Property | Type | Required | Description |
| ----------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | The [client token](/platform/guides/api-and-sdk-credentials#client-token) from the [connect flow](/platform/guides/connect-a-customer), or from the [connection check](/platform/frames/check) when you offer [guest checkout](/platform/guides/guest-checkout). |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `signature` | `string` | ✅ | The quote `signature` from the quote endpoint. Pass `signature` as returned. |
| `externalTransactionId` | `string` | | Your own identifier for the transaction. Stored and associated with the MoonPay transaction for correlation. |
| `theme` | `string` | | Pass `dark` or `light` to force a specific appearance. If you omit this, the frame uses the user's system appearance. |
## Events
All events are dispatched using the message pattern described in the [frames protocol](/platform/frames/overview#frames-protocol#messages). Below are the event payloads specific to the Apple Pay 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The frame finished loading and the UI is fully rendered. You can use this to coordinate UI transitions if needed.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `buttonPressed`
The customer tapped the Apple Pay button. The frame emits this the instant the
button is tapped, before iOS presents the PassKit 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 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`](#complete), which
reports the outcome once the payment resolves.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "buttonPressed"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ButtonPressedEvent = Message<{
kind: "buttonPressed";
}>;
```
#### `complete`
The transaction is complete. Use the transaction ID to track status updates (for example, by polling or via webhooks).
```json Example (success) theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"id": "txn_01",
"status": "pending"
}
}
}
```
```json Example (fail) theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"status": "failed",
"failureReason": "Your payment was declined by your bank.",
"failureCode": "authorizationDeclined"
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
enum TransactionStatus {
/** The transaction has successfully completed. The payment has been made and the crypto has been transferred. **/
completed = "completed",
/** The payment has been completed and the crypto transfer is underway. **/
pending = "pending",
/** The transaction has failed. No payment was applied and the crypto was not transferred. **/
failed = "failed",
}
type FailureCode =
| "applePayMerchantUnavailable"
| "transactionNotAllowed"
| "validationError"
| "serviceUnavailable"
| "authorizationDeclined"
| "unknown";
type Transaction =
| {
/** The MoonPay identifier for this transaction. **/
id: string;
/** The status of the transaction. **/
status: TransactionStatus.completed | TransactionStatus.pending;
}
| {
status: TransactionStatus.failed;
/** A stable, machine-readable code identifying the failure category. Branch on this value for programmatic handling. May be omitted when the frame cannot classify the failure. **/
failureCode?: FailureCode;
/** A developer-friendly error message detailing the reason for the transaction failure. **/
failureReason: string;
};
type ApplePayCompleteEvent = Message<{
kind: "complete";
payload: {
transaction: Transaction;
};
}>;
```
##### Failure codes
When a `complete` event has `status: "failed"`, the `failureCode` field indicates the failure category. Use this value — not `failureReason` — for programmatic branching. `failureReason` is a human-readable fallback suitable for display when you do not have custom copy for a given code.
`failureCode` is optional. When the frame cannot classify a failure, it sends
`failureReason` alone — fall back to showing that message to the customer.
| `failureCode` | Default `failureReason` | When it fires | Recommended handling |
| ----------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `applePayMerchantUnavailable` | "Apple Pay is temporarily unavailable. Please try again later." | Apple Pay merchant validation failed. | Retry after a short delay or offer an alternative payment method. |
| `transactionNotAllowed` | "This transaction is not allowed for your account." | Payment method or account is not eligible (for example, region restriction or KYC limit). | Show the `failureReason` and guide the customer to resolve the issue or choose a different payment method. |
| `validationError` | "The transaction request was invalid." | Request failed validation (unsupported currency, missing parameters). | Check the quote parameters and retry with corrected values. |
| `serviceUnavailable` | "Service temporarily unavailable. Please try again." | Upstream payment service is degraded or returned a 5xx response. | Retry with exponential back-off. |
| `authorizationDeclined` | "Your payment was declined by your bank." | The issuer or gateway rejected the authorization. | Prompt the customer to try a different card. |
| `unknown` | "An unexpected error occurred." | Any unexpected or unclassified error, including thrown authorization exceptions and fraud blocks. | Show the `failureReason` to the customer and retry. |
Quote expiry is reported through the [`error`](#error) event with `code:
"quoteExpired"`, not through `complete`. Listen for both events to cover all
failure paths.
#### `challenge`
Verification is required before the transaction can proceed. Render the
[challenge frame](/platform/frames/challenge) at the provided URL. Do not
construct the URL yourself — use it as-is.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "challenge",
"payload": {
"kind": "frame",
"url": "https://platform.moonpay.com/platform/v1/challenge?challengeToken=..."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ApplePayChallengeEvent = Message<{
kind: "challenge";
payload: {
kind: string;
/** Fully-formed URL to pass directly as the challenge frame src. */
url: string;
};
}>;
```
#### `error`
This event dispatches errors that occur in the flow and, if available, provides steps for recovery.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "applePayUnavailable",
"message": "Apple Pay is not supported."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ApplePayError = Message<{
kind: "error";
payload: {
code:
| "configurationError"
| "quoteExpired"
| "invalidQuote"
| "applePayUnavailable"
| "generic";
/** A developer-facing error message with details on recovery or documentation. This message is not intended to be rendered in UI. */
message: string;
};
}>;
```
| Code | Description |
| --------------------- | -------------------------------------------------------------------- |
| `configurationError` | The frame configuration is invalid. Check initialization parameters. |
| `quoteExpired` | The quote has expired. Fetch a new quote and send it via `setQuote`. |
| `invalidQuote` | The quote signature is invalid or malformed. |
| `applePayUnavailable` | Apple Pay is not available in the current environment or browser. |
| `generic` | An unexpected error occurred. |
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
#### `setQuote`
Provide a new quote to the frame. Pass `signature` as returned by the quote endpoint. Upon receiving this event, the frame disables the Apple Pay button until the quote is revalidated.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "setQuote",
"payload": {
"quote": {
"signature": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type SetQuoteEvent = Message<{
kind: "setQuote";
payload: {
quote: {
/** The signature from a valid quote. **/
signature: string;
};
};
}>;
```
# Auth
Source: https://dev.moonpay.com/platform/frames/auth
Authenticate a customer with a single OTP and return a fully-scoped access token.
The auth frame is the lowest-friction way to obtain a fully-scoped [access token](/platform/guides/api-and-sdk-credentials#access-token). The customer verifies an email or SMS one-time passcode and, on success, the frame returns encrypted authenticated credentials that your client can use to call the MoonPay API on the customer's behalf — for example to submit KYC information, fetch transactions, or perform other account-level operations.
## How it works
The auth frame should only be launched after the [check frame](/platform/frames/check) returns `connectionRequired`. The customer's email address and optionally phone number (passed when you [create a session](/platform/guides/connect-a-customer#create-a-session)) is how MoonPay identifies the user and where the one-time passcode is delivered:
* If MoonPay has a phone number on file for that user, the customer can choose to receive the code via SMS instead. If not, email is the only option.
* The customer does not need to be a returning MoonPay user. If no MoonPay account exists for the email yet, one is created during the OTP step. You can then drive any required onboarding or verification through other frames or directly via the API with the returned access token.
### Handling email mismatches
Make sure the email you pass when creating the session matches the customer's MoonPay account. If you send the wrong email — for example, a fresh address for a customer who already has an account under a different email — the auth flow will provision a brand-new account against the address you provided.
The customer can later reverify with the correct email, but the duplicate account will be flagged. If a duplicate is detected, the customer will later be asked to resolve it through a challenge flow during verification.
Use the full [connect frame](/platform/frames/connect) instead when you want MoonPay to orchestrate onboarding (KYC, address, document collection) inside the same flow.
## URL
```html theme={null}
https://platform.moonpay.com/v2/auth
```
## Requirements
### Key exchange
Credentials returned from the frame are encrypted to protect their content since they are sent over `postMessage`. You need to generate an [X25519](https://datatracker.ietf.org/doc/html/rfc7748#section-5) keypair and pass the public key into the frame. The frame uses your public key to encrypt the payload, ensuring only you can read it with your private key.
Never persist the private key to disk or storage. Hold it in memory only for
the duration of the session.
The frame uses the [@noble/curves](https://github.com/paulmillr/noble-curves) library internally. On web and React Native, you can use this same library to generate your keypair and handle decryption. For native platforms, use a compatible utility like [CryptoKit](https://developer.apple.com/documentation/cryptokit/curve25519) on iOS or [KeyPairGenerator](https://developer.android.com/reference/java/security/KeyPairGenerator) on Android.
The following example shows how to generate a keypair and decrypt credentials using `@noble/curves`. You'll want to add your own error handling and input validation for production use.
```sh pnpm theme={null}
pnpm i @noble/curves @noble/hashes @noble/ciphers
```
```sh bun theme={null}
bun add @noble/curves @noble/hashes @noble/ciphers
```
```sh npm theme={null}
npm i @noble/curves @noble/hashes @noble/ciphers
```
```ts crypto.ts theme={null}
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!
import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";
/** The credentials returned from the connect flow. */
export type ClientCredentials = {
/** A JWT used to authenticate client requests to the Moonpay API. */
accessToken: string;
/** A JWT used to initialize authenticated frames such as Apple Pay. */
clientToken: string;
/** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
expiresAt: string;
};
/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;
export type DecryptClientCredentialsResult =
| { ok: true; value: ClientCredentials }
| { ok: false; error: string };
const hexToBytes = (hex: string): Uint8Array => {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
};
const bytesToHex = (bytes: Uint8Array): string => {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
/** A base64-encoded string representing the encrypted JSON payload. **/
encryptedCredentials: string,
/** The recipient's X25519 private key as a hex string. **/
privateKeyHex: string,
): DecryptClientCredentialsResult => {
// Base64 decode the encrypted credentials
const payload = atob(encryptedCredentials);
// Guard and validate this deserialization
const parsedPayload = JSON.parse(payload);
// Convert the private key from a hex string to a `Uint8Array`
const privateKey = hexToBytes(privateKeyHex);
// Convert the ephemeral public key from a hex string to a `Uint8Array`
const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
const ivBytes = hexToBytes(parsedPayload.iv);
const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);
const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const cipher = gcm(encryptionKey, ivBytes);
const plainTextBytes = cipher.decrypt(ciphertextBytes);
const plaintext = new TextDecoder().decode(plainTextBytes);
let parsed: unknown;
try {
parsed = JSON.parse(plaintext);
} catch {
return { ok: false, error: "Failed to parse decrypted payload as JSON" };
}
// Validate the decrypted payload
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as Record).accessToken !== "string" ||
typeof (parsed as Record).clientToken !== "string" ||
typeof (parsed as Record).expiresAt !== "string"
) {
return { ok: false, error: "Decrypted payload missing required fields" };
}
return { ok: true, value: parsed as ClientCredentials };
};
/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
const { secretKey: privateKey, publicKey } = x25519.keygen();
return {
privateKey: bytesToHex(privateKey),
publicKey: bytesToHex(publicKey),
};
};
```
The following example shows how to generate a keypair and decrypt credentials using `@noble/curves`. You'll want to add your own error handling and input validation for production use.
In React Native, yuo will need a [polyfill for `getRandomValues`](https://github.com/LinusU/react-native-get-random-values) ([MDN](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)) which is only available in browsers.
```sh pnpm theme={null}
pnpm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```sh bun theme={null}
bun add react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```sh npm theme={null}
npm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```ts crypto.ts theme={null}
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!
import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";
// React Native polyfill for getRandomValues
import "react-native-get-random-values";
/** The credentials returned from the connect flow. */
export type ClientCredentials = {
/** A JWT used to authenticate client requests to the Moonpay API. */
accessToken: string;
/** A JWT used to initialize authenticated frames such as Apple Pay. */
clientToken: string;
/** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
expiresAt: string;
};
/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;
export type DecryptClientCredentialsResult =
| { ok: true; value: ClientCredentials }
| { ok: false; error: string };
const hexToBytes = (hex: string): Uint8Array => {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
};
const bytesToHex = (bytes: Uint8Array): string => {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
/** A base64-encoded string representing the encrypted JSON payload. **/
encryptedCredentials: string,
/** The recipient's X25519 private key as a hex string. **/
privateKeyHex: string,
): DecryptClientCredentialsResult => {
// Base64 decode the encrypted credentials
const payload = atob(encryptedCredentials);
// Guard and validate this deserialization
const parsedPayload = JSON.parse(payload);
// Convert the private key from a hex string to a `Uint8Array`
const privateKey = hexToBytes(privateKeyHex);
// Convert the ephemeral public key from a hex string to a `Uint8Array`
const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
const ivBytes = hexToBytes(parsedPayload.iv);
const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);
const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const cipher = gcm(encryptionKey, ivBytes);
const plainTextBytes = cipher.decrypt(ciphertextBytes);
const plaintext = new TextDecoder().decode(plainTextBytes);
let parsed: unknown;
try {
parsed = JSON.parse(plaintext);
} catch {
return { ok: false, error: "Failed to parse decrypted payload as JSON" };
}
// Validate the decrypted payload
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as Record).accessToken !== "string" ||
typeof (parsed as Record).clientToken !== "string" ||
typeof (parsed as Record).expiresAt !== "string"
) {
return { ok: false, error: "Decrypted payload missing required fields" };
}
return { ok: true, value: parsed as ClientCredentials };
};
/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
const { secretKey: privateKey, publicKey } = x25519.keygen();
return {
privateKey: bytesToHex(privateKey),
publicKey: bytesToHex(publicKey),
};
};
```
## Co-branding
The frame is co-branded: it renders your account's name and logo alongside MoonPay's branding. Configure your name and logo in your [MoonPay dashboard](https://dashboard.moonpay.com). You can't override them with URL parameters.
## Initialization parameters
| Property | Type | Required | Description |
| ------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | The anonymous client token obtained after decrypting the `credentials` returned by the [check frame](/platform/frames/check) when the status is `connectionRequired`. |
| `publicKey` | `string` | ✅ | An ephemeral public key generated on the client. See [requirements](#requirements) for details.
The frame uses this key to encrypt the [client credentials](/platform/guides/api-and-sdk-credentials#client-credentials) returned from the auth flow. |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `brandColor` | `string` | | A 6-digit hex color (with or without leading `#`) used as the partner brand color in the frame UI. Invalid values fall back to the MoonPay default. |
| `customTheme` | `string` | | A JSON-stringified theme-override object. Supports `borderRadius`, `colorScheme` (`auto`/`light`/`dark`), and `palette.semanticColors` (`positive`/`negative`/`caution` hex). Invalid or oversized values fall back to the MoonPay default. |
| `autoFocus` | `boolean` | | Whether the one-time passcode input is focused automatically when the frame loads. Defaults to `true`. Pass `false` if your app manages focus itself (for example, to avoid the mobile keyboard opening before the frame is visible). |
## Events
All events are dispatched using the message pattern described in the [frames protocol](/platform/frames/overview#frames-protocol#messages). Below are the event payloads specific to the auth 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The frame has loaded and dispatched the customer's first one-time passcode. Use this to reveal the frame and drop any loading state you were showing in its place.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `complete`
The auth flow finished. The `payload` is a `Connection` object whose `status` field tells you the outcome:
* **`active`** — the customer is authenticated. The payload includes the customer's id and encrypted authenticated client credentials. Any outstanding KYC or onboarding requirement is surfaced through `capabilities`, not a separate status — use the returned access token to drive it.
* **`termsAcceptanceRequired`** — the customer must accept updated Terms of Use before the connection can be used. No credentials are attached: display the Terms of Use in your own UI, capture the acceptance timestamp, pass it as `termsAcceptedAt` when you create a new session (`POST /platform/v1/sessions`), then relaunch the flow. Passing `termsAcceptedAt` requires the Identity or Guest Checkout account capability. See [Terms acceptance](/platform/guides/terms-acceptance) for the presentation requirements and recovery loop.
* **`pending`** / **`unavailable`** — status-only completions with no credentials: the connection exists but isn't currently usable (a KYC decision is still pending, or the customer is in a restricted location).
```json Active theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "active",
"credentials": "",
"customer": { "id": "" }
}
}
```
```json Terms acceptance required theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "termsAcceptanceRequired"
}
}
```
```ts twoslash TypeScript definition expandable theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
enum ConnectionStatus {
/** The connection is valid and can be used. **/
active = "active",
/** The customer must accept updated Terms of Use before the connection can be used. **/
termsAcceptanceRequired = "termsAcceptanceRequired",
/** A KYC decision is still pending; the connection is not yet usable. **/
pending = "pending",
/** The connection cannot be used (for example, a restricted location). **/
unavailable = "unavailable",
}
type PaymentDisclosuresRequirement = {
/** The customer's ISO 3166-1 alpha-3 residential country code. E.g. `"USA"` or `"FRA"` */
country: string;
/** The state or province code, when disclosures apply to a subdivision. E.g. `"NY"` or `"WA"` */
administrativeArea?: string;
/** The broader regulatory area for the country, when applicable. Currently `"EEA"`. */
area?: string;
};
type CustomerCapabilities = {
ramps: {
requirements: {
/** Present when disclosure geography is available. Use `country`, `administrativeArea`, and `area` to determine which disclosure, if any, applies. **/
paymentDisclosures?: PaymentDisclosuresRequirement;
};
};
};
type ActiveConnection = {
status: ConnectionStatus.active;
/** The authenticated MoonPay customer. **/
customer: { id: string };
/** Encrypted client credentials containing the accessToken and clientToken. Once decrypted, the value contains a stringified JSON object with the following structure:
*
* {
* "accessToken": "",
* "clientToken": "",
* "expiresAt": "",
* }
*
* - `accessToken`: A fully-scoped token that can be used to make authenticated API requests on behalf of the customer.
* - `clientToken`: A token that can be used to initialize sensitive frames such as Apple Pay.
* - `expiresAt`: An ISO 8601 formatted string indicating when the tokens expire.
**/
credentials: string;
/** Regulatory capabilities for the authenticated customer. Present only when there is a requirement to surface; omitted otherwise. **/
capabilities?: CustomerCapabilities;
};
type TermsAcceptanceRequiredConnection = {
status: ConnectionStatus.termsAcceptanceRequired;
};
type PendingConnection = {
status: ConnectionStatus.pending;
};
type UnavailableConnection = {
status: ConnectionStatus.unavailable;
};
type AuthCompleteEvent = Message<{
kind: "complete";
payload:
| ActiveConnection
| TermsAcceptanceRequiredConnection
| PendingConnection
| UnavailableConnection;
}>;
```
#### `error`
This event dispatches errors that occur in the flow. The error message is developer-facing and not intended to be rendered in UI.
The frame emits this event for every [terminal error](#terminal-errors) so the parent always knows when the flow has failed and can tear down the iframe or surface a fallback in your app. Recoverable mistakes the customer can retry inside the frame (such as an invalid or expired code) are not reported through this event — see [Error handling](#error-handling) for the full breakdown.
Terminal errors that occur during the flow carry `code: "generic"`. Invalid initialization parameters — a missing or malformed `clientToken`, `publicKey`, or `channelId` — are instead reported with `code: "validationError"` and a list of the offending fields, before the OTP flow starts.
```json Generic theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "generic",
"message": "Authorization failed"
}
}
```
```json Validation error theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "validationError",
"errors": [
{ "code": "invalidClientToken", "message": "clientToken is required" }
]
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ValidationFieldError = {
/** Which initialization parameter failed validation. */
code: "invalidClientToken" | "invalidPublicKey" | "invalidChannelId";
message: string;
};
type AuthFlowError = Message<{
kind: "error";
payload:
| {
/** A terminal failure during the flow. */
code: "generic";
/** A developer-facing error message with details on recovery or documentation. This message is not intended to be rendered in UI. */
message: string;
}
| {
/** One or more initialization parameters were missing or malformed; emitted before the flow starts. */
code: "validationError";
errors: ValidationFieldError[];
};
}>;
```
#### `close`
The customer dismissed the frame. The parent should tear down the iframe or WebView in response.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "close"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type CloseEvent = Message<{
kind: "close";
}>;
```
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
## Error handling
The auth frame splits failures into two categories. Inline errors are handled silently inside the frame so the customer can retry without the parent doing anything. Terminal errors end the flow — the frame shows a full-screen error state to the customer **and** sends an [`error`](#error) post-message to the parent at the same time, so your app can react (for example, by tearing down the iframe or surfacing a fallback).
### Inline errors
These are recoverable mistakes the customer can fix without leaving the OTP screen. The frame shows an inline message under the code input and stays open so the customer can retry. The parent does not receive an `error` event for these.
| Scenario | Message shown to the customer |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Wrong code entered | "That doesn't look right, try a new code" |
| Code has expired | "That code is old, try a new one" |
| Rate limit hit when **resending** a fresh code | "Too many requests. Try again later." (shown briefly as an auto-clearing hint; the customer can wait out the cooldown and try again) |
### Terminal errors
These end the flow. The frame replaces its content with a full-screen error state — depending on the cause, the customer is offered a **Retry** button, a **Close** button, or both. The parent always receives an [`error`](#error) event with `code: "generic"` for every terminal error, so it can dismiss the iframe or surface a fallback in your app.
| Scenario | Message shown to the customer |
| --------------------------------------------------------- | --------------------------------------------------- |
| Too many failed code attempts | "Too many failed attempts. Please try again later." |
| OTP session expired before the customer entered the code | "Verification session expired. Please start over." |
| Rate limit hit while **sending the initial OTP** (boot) | "Too many requests. Try again later." |
| Authorization failed when exchanging the verified OTP | "Authorization failed. Please try again." |
| Account cannot be used (for example, restricted location) | "This account is unavailable." |
| Any other unexpected failure | "Something went wrong. Please try again." |
# Buy
Source: https://dev.moonpay.com/platform/frames/buy
Details on working with the headless Buy frame used in the [Pay with card](/platform/guides/pay-with-card) flow.
The Buy frame is a headless frame. It evaluates transaction requirements, creates
the transaction, and completes post-transaction processing — all without
rendering any UI. Your confirmation screen, loading states, and purchase button
remain fully under your control.
## URL
```
https://platform.moonpay.com/platform/v1/buy
```
## Requirements
### Size
The frame is headless. Mount it with zero dimensions so it does not affect your
layout:
```tsx Example theme={null}
```
## Initialization parameters
| Property | Type | Required | Description |
| ----------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | The [client token](/platform/guides/api-and-sdk-credentials#client-token) returned from the [connect flow](/platform/guides/connect-a-customer). |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `signature` | `string` | ✅ | The quote `signature` from the [quote endpoint](/api-reference/platform/endpoints/quotes/get). Pass `signature` as returned. |
| `externalTransactionId` | `string` | | Your own identifier for the transaction. Stored and associated with the MoonPay transaction for correlation. |
## Events
All events are dispatched using the message pattern described in the [frames
protocol](/platform/frames/overview#frames-protocol#messages). Below are the
event payloads specific to the Buy 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The buy pipeline is starting. Use this to show a loading indicator in your UI.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `complete`
The transaction is complete. Use the transaction ID to track final status via
polling.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"id": "txn_01",
"status": "pending"
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type BuyCompleteEvent = Message<{
kind: "complete";
payload: {
transaction: {
id: string;
status: "pending" | "completed" | "failed";
};
};
}>;
```
#### `challenge`
Verification is required before the transaction can proceed. Render the
[challenge frame](/platform/frames/challenge) at the provided URL. Do not
construct the URL yourself — use it as-is.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "challenge",
"payload": {
"kind": "frame",
"url": "https://platform.moonpay.com/platform/v1/challenge?challengeToken=..."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type BuyChallengeEvent = Message<{
kind: "challenge";
payload: {
kind: string;
/** Fully-formed URL to pass directly as the challenge frame src. */
url: string;
};
}>;
```
#### `error`
A terminal error occurred. Remove the frame and surface the message to the
developer.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "invalidQuote",
"message": "Unable to decode the quote signature."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type BuyErrorEvent = Message<{
kind: "error";
payload: {
code: "configurationError" | "invalidQuote" | "generic";
/** A developer-facing error message. Not intended to be rendered in UI. */
message: string;
};
}>;
```
| Code | Description |
| -------------------- | ---------------------------------------- |
| `configurationError` | Missing or invalid `signature` parameter |
| `invalidQuote` | Unable to decode the quote signature |
| `generic` | Unspecified error |
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
#### `setQuote`
Provide a new quote to the frame. Send this when the current quote expires
before the customer completes the purchase. Pass `signature` as returned by the
quote endpoint.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "setQuote",
"payload": {
"quote": {
"signature": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type SetQuoteEvent = Message<{
kind: "setQuote";
payload: {
quote: {
/** The signature from a valid quote. */
signature: string;
};
};
}>;
```
# Buy Button
Source: https://dev.moonpay.com/platform/frames/buy-button
Details on working with the Buy Button frame, an express-checkout payment button that runs the same buy orchestration as the headless [Buy frame](/platform/frames/buy).
The Buy Button frame renders a compact express-checkout payment button. It shows
the available payment options — Apple Pay, Google Pay, and card — and opens a
confirmation sheet when the customer taps to pay. Behind the UI, it runs the same
buy orchestration pipeline as the headless [Buy frame](/platform/frames/buy):
it evaluates transaction requirements, creates the transaction, and completes
post-transaction processing.
The difference is the experience. The Buy frame is headless and leaves every
screen under your control. The Buy Button frame provides a visible payment button
and confirmation sheet, so you can offer express checkout without building that UI
yourself. The message protocol is identical to the Buy frame, so you handle the
same events either way.
## URL
```
https://platform.moonpay.com/platform/v1/buy-button
```
## Initialization parameters
| Property | Type | Required | Description |
| ----------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | The [client token](/platform/guides/api-and-sdk-credentials#client-token) returned from the [connect flow](/platform/guides/connect-a-customer). |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `signature` | `string` | ✅ | The quote `signature` from the [quote endpoint](/api-reference/platform/endpoints/quotes/get). Pass `signature` as returned. |
| `externalTransactionId` | `string` | | Your own identifier for the transaction. Stored and associated with the MoonPay transaction for correlation. |
## Events
All events are dispatched using the message pattern described in the [frames
protocol](/platform/frames/overview#frames-protocol#messages). The Buy Button frame
uses the same event payloads as the [Buy frame](/platform/frames/buy); only the
experience differs.
### 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The frame finished loading and the payment button is rendered. Use this to
coordinate UI transitions if needed.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `buttonPressed`
The customer tapped the Apple Pay or Google Pay button in the buy button. The
frame emits this the instant the button is tapped, before the OS presents the
payment sheet (PassKit on iOS, the Google Pay sheet on Android). 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 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`](#complete), which
reports the outcome once the payment resolves.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "buttonPressed"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ButtonPressedEvent = Message<{
kind: "buttonPressed";
}>;
```
#### `complete`
The transaction is complete. Use the transaction ID to track final status via
polling.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"id": "txn_01",
"status": "pending"
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type BuyButtonCompleteEvent = Message<{
kind: "complete";
payload: {
transaction: {
id: string;
status: "pending" | "completed" | "failed";
};
};
}>;
```
#### `challenge`
Verification is required before the transaction can proceed. Render the
[challenge frame](/platform/frames/challenge) at the provided URL. Do not
construct the URL yourself — use it as-is.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "challenge",
"payload": {
"kind": "frame",
"url": "https://platform.moonpay.com/platform/v1/challenge?challengeToken=..."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type BuyButtonChallengeEvent = Message<{
kind: "challenge";
payload: {
kind: string;
/** Fully-formed URL to pass directly as the challenge frame src. */
url: string;
};
}>;
```
#### `error`
A terminal error occurred. Remove the frame and surface the message to the
developer.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "invalidQuote",
"message": "Unable to decode the quote signature."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type BuyButtonErrorEvent = Message<{
kind: "error";
payload: {
code: "configurationError" | "invalidQuote" | "generic";
/** A developer-facing error message. Not intended to be rendered in UI. */
message: string;
};
}>;
```
| Code | Description |
| -------------------- | ---------------------------------------- |
| `configurationError` | Missing or invalid `signature` parameter |
| `invalidQuote` | Unable to decode the quote signature |
| `generic` | Unspecified error |
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
#### `setQuote`
Provide a new quote to the frame. Send this when the current quote expires
before the customer completes the purchase. Pass `signature` as returned by the
quote endpoint.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "setQuote",
"payload": {
"quote": {
"signature": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type SetQuoteEvent = Message<{
kind: "setQuote";
payload: {
quote: {
/** The signature from a valid quote. */
signature: string;
};
};
}>;
```
# Challenge
Source: https://dev.moonpay.com/platform/frames/challenge
Details on working with the Challenge frame used by the [Pay with card](/platform/guides/pay-with-card) flow, by identity verification, and by guest checkout limit upgrades.
The Challenge frame handles verification steps required by another flow. Three
upstream flows mount it today:
* The [buy frame](/platform/frames/buy) emits a `challenge` event with a URL —
use that URL to mount the frame and complete the card transaction.
* 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](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up).
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.
## URL
For the `buy` flow, the URL is provided by the buy frame'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
| Property | Type | Required | Description |
| ---------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | SDK integrations append this automatically. In a manual integration, append the same `clientToken` used to initialize your other frames to the challenge URL before setting it as the frame `src`. |
| `channelId` | `string` | ✅ | Generate a fresh channel ID on your side and append it to the challenge URL before setting it as the frame `src`. |
| `challengeToken` | `string` | ✅ | Opaque token. Do not modify or parse it. |
| `theme` | `string` | | Pass `dark` or `light` to force a specific appearance. If you omit this, the frame uses the user's system appearance. |
## Events
All events are dispatched using the message pattern described in the [frames
protocol](/platform/frames/overview#frames-protocol#messages). 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The challenge UI is rendered and visible to the customer.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `complete`
All required verification steps have resolved. Remove the challenge frame and
branch on the discriminated payload:
* `flow: "buy"` — the transaction pipeline finished. Remove the buy frame too
and navigate to the confirmation screen.
* `flow: "identity"` — the customer was verified. Use `identityId` to continue
your onboarding flow.
* `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.
```json Buy example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "complete",
"payload": {
"flow": "buy",
"transaction": {
"id": "txn_01",
"status": "pending"
}
}
}
```
```json Identity example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "complete",
"payload": {
"flow": "identity",
"identityId": "idn_01"
}
}
```
```json Guest checkout limit upgrade example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "complete",
"payload": {
"flow": "guest_checkout_limit_upgrade",
"status": "upgraded"
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type GuestCheckoutLimitUpgradeStatus = "pending" | "upgraded" | "rejected";
type ChallengeCompleteEvent = Message<{
kind: "complete";
payload:
| {
flow: "buy";
transaction: {
id: string;
status: "pending" | "completed" | "failed";
};
}
| {
flow: "identity";
identityId: string;
}
| {
flow: "guest_checkout_limit_upgrade";
status: GuestCheckoutLimitUpgradeStatus;
};
}>;
```
#### `cancelled`
The customer dismissed the challenge. Remove the challenge frame and act on
the discriminated payload:
* `flow: "buy"` — remove the buy frame 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.
```json Buy example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "cancelled",
"payload": {
"flow": "buy",
"transactionId": "txn_01",
"challengeToken": "eyJhbGciOi..."
}
}
```
```json Identity example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "cancelled",
"payload": {
"flow": "identity"
}
}
```
```json Guest checkout limit upgrade example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "cancelled",
"payload": {
"flow": "guest_checkout_limit_upgrade"
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ChallengeCancelledEvent = Message<{
kind: "cancelled";
payload:
| {
flow: "buy";
transactionId?: string;
challengeToken?: string;
}
| {
flow: "identity";
}
| {
flow: "guest_checkout_limit_upgrade";
};
}>;
```
#### `error`
The challenge failed. Remove the challenge frame (and the buy frame, if the
buy flow was driving it), then surface the message to the developer.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "error",
"payload": {
"code": "invalid_challenge",
"message": "Could not decode challenge from response."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ChallengeErrorEvent = Message<{
kind: "error";
payload: {
/**
* A machine-readable error category. Surface to logs, not UI. Additional
* codes can be propagated from the upstream flow.
*/
code: string;
/** A developer-facing error message. Not intended to be rendered in UI. */
message: string;
};
}>;
```
Common codes:
| Code | Description |
| -------------------- | ---------------------------------------------------------------------------- |
| `configurationError` | Frame initialization failed (for example, the `challengeToken` is missing) |
| `invalid_token` | The `challengeToken` could not be decoded |
| `unsupported_flow` | The token type is not supported by this frame |
| `invalid_challenge` | Challenge details could not be decoded from the token |
| `missing_quote` | The buy flow could not decode the quote from the `challengeToken` |
| `generic` | The guest checkout limit upgrade could not run. Read `message` for the cause |
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake). This is the only message you send to
the challenge frame — it is self-driving after the handshake.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_challenge_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
# Check
Source: https://dev.moonpay.com/platform/frames/check
Check if a customer has an active connection.
The check frame is a lightweight, headless page hosted on a MoonPay domain. Use it to check whether the customer already has an active connection.
The frame always returns encrypted credentials. If the customer is connected, these are authenticated credentials. If the customer is not connected (or their connection has expired), these are anonymous credentials — store them and use the `clientToken` to initialize the connect flow.
## URL
```html theme={null}
https://platform.moonpay.com/platform/v1/check-connection
```
## Requirements
### Key exchange
Credentials returned from the frame are encrypted to protect their content since they are sent over `postMessage`. You need to generate an [X25519](https://datatracker.ietf.org/doc/html/rfc7748#section-5) keypair and pass the public key into the frame. The frame uses your public key to encrypt the payload, ensuring only you can read it with your private key.
Never persist the private key to disk or storage. Hold it in memory only for
the duration of the session.
The frame uses the [@noble/curves](https://github.com/paulmillr/noble-curves) library internally. On web and React Native, you can use this same library to generate your keypair and handle decryption. For native platforms, use a compatible utility like [CryptoKit](https://developer.apple.com/documentation/cryptokit/curve25519) on iOS or [KeyPairGenerator](https://developer.android.com/reference/java/security/KeyPairGenerator) on Android.
The following example shows how to generate a keypair and decrypt credentials using `@noble/curves`. You'll want to add your own error handling and input validation for production use.
```sh pnpm theme={null}
pnpm i @noble/curves @noble/hashes @noble/ciphers
```
```sh bun theme={null}
bun add @noble/curves @noble/hashes @noble/ciphers
```
```sh npm theme={null}
npm i @noble/curves @noble/hashes @noble/ciphers
```
```ts crypto.ts theme={null}
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!
import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";
/** The credentials returned from the connect flow. */
export type ClientCredentials = {
/** A JWT used to authenticate client requests to the Moonpay API. */
accessToken: string;
/** A JWT used to initialize authenticated frames such as Apple Pay. */
clientToken: string;
/** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
expiresAt: string;
};
/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;
export type DecryptClientCredentialsResult =
| { ok: true; value: ClientCredentials }
| { ok: false; error: string };
const hexToBytes = (hex: string): Uint8Array => {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
};
const bytesToHex = (bytes: Uint8Array): string => {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
/** A base64-encoded string representing the encrypted JSON payload. **/
encryptedCredentials: string,
/** The recipient's X25519 private key as a hex string. **/
privateKeyHex: string,
): DecryptClientCredentialsResult => {
// Base64 decode the encrypted credentials
const payload = atob(encryptedCredentials);
// Guard and validate this deserialization
const parsedPayload = JSON.parse(payload);
// Convert the private key from a hex string to a `Uint8Array`
const privateKey = hexToBytes(privateKeyHex);
// Convert the ephemeral public key from a hex string to a `Uint8Array`
const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
const ivBytes = hexToBytes(parsedPayload.iv);
const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);
const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const cipher = gcm(encryptionKey, ivBytes);
const plainTextBytes = cipher.decrypt(ciphertextBytes);
const plaintext = new TextDecoder().decode(plainTextBytes);
let parsed: unknown;
try {
parsed = JSON.parse(plaintext);
} catch {
return { ok: false, error: "Failed to parse decrypted payload as JSON" };
}
// Validate the decrypted payload
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as Record).accessToken !== "string" ||
typeof (parsed as Record).clientToken !== "string" ||
typeof (parsed as Record).expiresAt !== "string"
) {
return { ok: false, error: "Decrypted payload missing required fields" };
}
return { ok: true, value: parsed as ClientCredentials };
};
/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
const { secretKey: privateKey, publicKey } = x25519.keygen();
return {
privateKey: bytesToHex(privateKey),
publicKey: bytesToHex(publicKey),
};
};
```
The following example shows how to generate a keypair and decrypt credentials using `@noble/curves`. You'll want to add your own error handling and input validation for production use.
In React Native, yuo will need a [polyfill for `getRandomValues`](https://github.com/LinusU/react-native-get-random-values) ([MDN](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)) which is only available in browsers.
```sh pnpm theme={null}
pnpm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```sh bun theme={null}
bun add react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```sh npm theme={null}
npm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```ts crypto.ts theme={null}
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!
import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";
// React Native polyfill for getRandomValues
import "react-native-get-random-values";
/** The credentials returned from the connect flow. */
export type ClientCredentials = {
/** A JWT used to authenticate client requests to the Moonpay API. */
accessToken: string;
/** A JWT used to initialize authenticated frames such as Apple Pay. */
clientToken: string;
/** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
expiresAt: string;
};
/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;
export type DecryptClientCredentialsResult =
| { ok: true; value: ClientCredentials }
| { ok: false; error: string };
const hexToBytes = (hex: string): Uint8Array => {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
};
const bytesToHex = (bytes: Uint8Array): string => {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
/** A base64-encoded string representing the encrypted JSON payload. **/
encryptedCredentials: string,
/** The recipient's X25519 private key as a hex string. **/
privateKeyHex: string,
): DecryptClientCredentialsResult => {
// Base64 decode the encrypted credentials
const payload = atob(encryptedCredentials);
// Guard and validate this deserialization
const parsedPayload = JSON.parse(payload);
// Convert the private key from a hex string to a `Uint8Array`
const privateKey = hexToBytes(privateKeyHex);
// Convert the ephemeral public key from a hex string to a `Uint8Array`
const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
const ivBytes = hexToBytes(parsedPayload.iv);
const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);
const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const cipher = gcm(encryptionKey, ivBytes);
const plainTextBytes = cipher.decrypt(ciphertextBytes);
const plaintext = new TextDecoder().decode(plainTextBytes);
let parsed: unknown;
try {
parsed = JSON.parse(plaintext);
} catch {
return { ok: false, error: "Failed to parse decrypted payload as JSON" };
}
// Validate the decrypted payload
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as Record).accessToken !== "string" ||
typeof (parsed as Record).clientToken !== "string" ||
typeof (parsed as Record).expiresAt !== "string"
) {
return { ok: false, error: "Decrypted payload missing required fields" };
}
return { ok: true, value: parsed as ClientCredentials };
};
/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
const { secretKey: privateKey, publicKey } = x25519.keygen();
return {
privateKey: bytesToHex(privateKey),
publicKey: bytesToHex(publicKey),
};
};
```
## Initialization parameters
| Property | Type | Required | Description |
| -------------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionToken` | `string` | ✅ | The [session token](/platform/guides/api-and-sdk-credentials#session-token) obtained from your server when [creating a session](/platform/guides/connect-a-customer#create-a-session). |
| `publicKey` | `string` | ✅ | An ephemeral public key generated on the client. See [requirements](#requirements) for details.
The frame uses this key to encrypt the [client credentials](/platform/guides/api-and-sdk-credentials#client-credentials) returned from the connect flow. |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `skipKyc` | `boolean` | | Pass `true` for [Customer API](/platform/guides/customer-api) integrations so the check skips KYC-based statuses: the result won't resolve to `pending` or `failed` due to KYC alone. `termsAcceptanceRequired` is still surfaced (legal requirements can't be skipped). Defaults to `false`. |
## Events
All events are dispatched using the message pattern described in the [frames protocol](/platform/frames/overview#frames-protocol#messages). Below are the event payloads specific to the check 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `complete`
The frame finished checking the customer’s connection status.
```json Example expandable theme={null}
// Active
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "active",
"customer": {
"id": "Y3VzX2FiYzEyMw==",
"country": "USA",
"administrativeArea": "NY"
},
"credentials": ""
}
}
// Terms acceptance required
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "termsAcceptanceRequired"
}
}
// Pending
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "pending"
}
}
// Unavailable
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "unavailable"
}
}
// Failed
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "failed",
"reason": "Unable to create MoonPay account."
}
}
// Connection required
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "connectionRequired",
"credentials": "",
// Present only when the session's email and phone number
// resolve to different MoonPay customers
"mismatch": true
}
}
```
```ts twoslash TypeScript definition expandable theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
enum ConnectionStatus {
/** The connection is valid and can be used. **/
active = "active",
/** The customer must accept updated Terms of Use before the connection can be used. No credentials are attached. See the Terms acceptance guide for the recovery loop. **/
termsAcceptanceRequired = "termsAcceptanceRequired",
/** The connection could not be completed. This typically occurs for customers whose KYC decisions are delayed. Often these cases are resolved out of band and the customer can connect on a subsequent visit to your app. **/
pending = "pending",
/** The connection cannot be used at the current time. This typically occurs when a KYC-verified customer is using a device or application from a restricted location. **/
unavailable = "unavailable",
/** The connection was not created or is invalid and should not be retried. This usually happens if a customer fails KYC or cannot be onboarded to MoonPay. It can also happen if a customer rejects a connection to your application. In these cases, direct the customer to an alternate flow within your app. **/
failed = "failed",
/** A new connection needs to be created. **/
connectionRequired = "connectionRequired",
}
type RampsCapability = {
requirements: Record;
};
type CustomerCapabilities = {
/** Capabilities for the ramps (buy and sell) product. **/
ramps?: RampsCapability;
/** Capabilities for the guest checkout flow. Present when guest checkout is enabled for the partner and the session is a guest-checkout session. **/
guestCheckout?: RampsCapability;
};
type ActiveConnection = {
status: ConnectionStatus.active;
/** The MoonPay customer associated with this connection. **/
customer: {
/** The MoonPay customer identifier. **/
id: string;
/**
* The customer's ISO 3166-1 alpha-3 residential country code.
*
* Example: `"USA"` or `"FRA"`
*/
country?: string;
/**
* The state or province code for the customer's residence, included when
* disclosures apply at the subdivision level.
*
* Example: `"NY"` or `"WA"`
*/
administrativeArea?: string;
/**
* The broader regulatory area for the customer's country, when applicable.
* Currently `"EEA"`.
*/
area?: string;
};
/** Encrypted client credentials containing the accessToken and clientToken. Once decrypted, the value contains a stringified JSON object with the following structure:
*
* {
* "accessToken": "",
* "clientToken": "",
* "expiresAt": "",
* }
*
* - `accessToken`: A token that can be used to make API requests from the client.
* - `clientToken`: A token that can be used to initialize sensitive frames such as Apple Pay.
* - `expiresAt`: An ISO 8601 formatted string indicating when the tokens expire.
**/
credentials: string;
/** Regulatory capabilities for the connected customer. Present only when there is a requirement to surface. **/
capabilities?: CustomerCapabilities;
};
type TermsAcceptanceRequiredConnection = {
status: ConnectionStatus.termsAcceptanceRequired;
};
type PendingConnection = {
status: ConnectionStatus.pending;
};
type UnavailableConnection = {
status: ConnectionStatus.unavailable;
};
type FailedConnection = {
status: ConnectionStatus.failed;
/** A developer-friendly description for the failure. **/
reason: string;
};
type RequiredConnection = {
status: ConnectionStatus.connectionRequired;
/** Encrypted anonymous client credentials. Store these and use the decrypted `clientToken` to initialize the connect frame.
*
* Once decrypted, the value contains a stringified JSON object with the following structure:
* {
* "accessToken": "",
* "clientToken": "",
* "expiresAt": "",
* }
*
* When authentication completes via the connect frame, replace these with the authenticated credentials returned from the connect flow.
**/
credentials: string;
/** Regulatory capabilities for the customer, when available. **/
capabilities?: CustomerCapabilities;
/** Present only when the session's email and phone number resolve to different MoonPay customers (a conflict). When `true`, route the customer through the connect flow before rendering payment UI such as Apple Pay. Absent when there is no conflict — never `false`. **/
mismatch?: boolean;
};
type ConnectionCompleteEvent = Message<{
kind: "complete";
payload:
| ActiveConnection
| TermsAcceptanceRequiredConnection
| PendingConnection
| UnavailableConnection
| FailedConnection
| RequiredConnection;
}>;
```
The `credentials` value is an encrypted string. Once decrypted, it contains a
JSON object with `accessToken`, `clientToken`, and `expiresAt`. See [API and SDK
credentials](/platform/guides/api-and-sdk-credentials#client-credentials) for
how to use each field.
#### `error`
This event dispatches errors that occur in the flow and, if available, provides steps for recovery.
```json Example expandable theme={null}
// Validation error — one or more initialization parameters are missing or invalid
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "validationError",
"errors": [
{
"code": "invalidSessionToken",
"message": "sessionToken is required"
}
]
}
}
// Generic error — the connection could not be activated
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "generic",
"message": "Unable to activate connection"
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ValidationErrorSubCode =
| "invalidSessionToken"
| "invalidChannelId"
| "invalidPublicKey";
type ConnectFlowError = Message<{
kind: "error";
payload:
| {
code: "validationError";
/** One entry per invalid initialization parameter. **/
errors: {
code: ValidationErrorSubCode;
/** A developer-facing error message. This message is not intended to be rendered in UI. */
message: string;
}[];
}
| {
code: "generic";
/** A developer-facing error message. This message is not intended to be rendered in UI. */
message: string;
};
}>;
```
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
# Connect flow
Source: https://dev.moonpay.com/platform/frames/connect
Details on working with the [connect](/platform/guides/connect-a-customer) frame.
## URL
```html theme={null}
https://platform.moonpay.com/platform/v1/connect
```
## Requirements
### Permissions
The following [permission policies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy#iframes) are required:
* `accelerometer`
* `autoplay`
* `camera`
* `encrypted-media`
* `gyroscope`
```tsx Example theme={null}
```
### Key exchange
Credentials returned from the frame are encrypted to protect their content since they are sent over `postMessage`. You need to generate an [X25519](https://datatracker.ietf.org/doc/html/rfc7748#section-5) keypair and pass the public key into the frame. The frame uses your public key to encrypt the payload, ensuring only you can read it with your private key.
Never persist the private key to disk or storage. Hold it in memory only for
the duration of the session.
The frame uses the [@noble/curves](https://github.com/paulmillr/noble-curves) library internally. On web and React Native, you can use this same library to generate your keypair and handle decryption. For native platforms, use a compatible utility like [CryptoKit](https://developer.apple.com/documentation/cryptokit/curve25519) on iOS or [KeyPairGenerator](https://developer.android.com/reference/java/security/KeyPairGenerator) on Android.
The following example shows how to generate a keypair and decrypt credentials using `@noble/curves`. You'll want to add your own error handling and input validation for production use.
```sh pnpm theme={null}
pnpm i @noble/curves @noble/hashes @noble/ciphers
```
```sh bun theme={null}
bun add @noble/curves @noble/hashes @noble/ciphers
```
```sh npm theme={null}
npm i @noble/curves @noble/hashes @noble/ciphers
```
```ts crypto.ts theme={null}
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!
import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";
/** The credentials returned from the connect flow. */
export type ClientCredentials = {
/** A JWT used to authenticate client requests to the Moonpay API. */
accessToken: string;
/** A JWT used to initialize authenticated frames such as Apple Pay. */
clientToken: string;
/** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
expiresAt: string;
};
/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;
export type DecryptClientCredentialsResult =
| { ok: true; value: ClientCredentials }
| { ok: false; error: string };
const hexToBytes = (hex: string): Uint8Array => {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
};
const bytesToHex = (bytes: Uint8Array): string => {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
/** A base64-encoded string representing the encrypted JSON payload. **/
encryptedCredentials: string,
/** The recipient's X25519 private key as a hex string. **/
privateKeyHex: string,
): DecryptClientCredentialsResult => {
// Base64 decode the encrypted credentials
const payload = atob(encryptedCredentials);
// Guard and validate this deserialization
const parsedPayload = JSON.parse(payload);
// Convert the private key from a hex string to a `Uint8Array`
const privateKey = hexToBytes(privateKeyHex);
// Convert the ephemeral public key from a hex string to a `Uint8Array`
const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
const ivBytes = hexToBytes(parsedPayload.iv);
const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);
const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const cipher = gcm(encryptionKey, ivBytes);
const plainTextBytes = cipher.decrypt(ciphertextBytes);
const plaintext = new TextDecoder().decode(plainTextBytes);
let parsed: unknown;
try {
parsed = JSON.parse(plaintext);
} catch {
return { ok: false, error: "Failed to parse decrypted payload as JSON" };
}
// Validate the decrypted payload
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as Record).accessToken !== "string" ||
typeof (parsed as Record).clientToken !== "string" ||
typeof (parsed as Record).expiresAt !== "string"
) {
return { ok: false, error: "Decrypted payload missing required fields" };
}
return { ok: true, value: parsed as ClientCredentials };
};
/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
const { secretKey: privateKey, publicKey } = x25519.keygen();
return {
privateKey: bytesToHex(privateKey),
publicKey: bytesToHex(publicKey),
};
};
```
The following example shows how to generate a keypair and decrypt credentials using `@noble/curves`. You'll want to add your own error handling and input validation for production use.
In React Native, yuo will need a [polyfill for `getRandomValues`](https://github.com/LinusU/react-native-get-random-values) ([MDN](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)) which is only available in browsers.
```sh pnpm theme={null}
pnpm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```sh bun theme={null}
bun add react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```sh npm theme={null}
npm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```ts crypto.ts theme={null}
// An example module for generating keypairs and decrypting client credentials.
// This should not be used as-is in production!
import { gcm } from "@noble/ciphers/aes.js";
import { x25519 } from "@noble/curves/ed25519.js";
import { hkdf } from "@noble/hashes/hkdf.js";
import { sha256 } from "@noble/hashes/sha2.js";
// React Native polyfill for getRandomValues
import "react-native-get-random-values";
/** The credentials returned from the connect flow. */
export type ClientCredentials = {
/** A JWT used to authenticate client requests to the Moonpay API. */
accessToken: string;
/** A JWT used to initialize authenticated frames such as Apple Pay. */
clientToken: string;
/** An [ISO 8601 timestamp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) representing the expiration time of the tokens. */
expiresAt: string;
};
/** X25519 `privateKey` and `publicKey`as hex strings. */
export type KeyPair = Record<"privateKey" | "publicKey", string>;
export type DecryptClientCredentialsResult =
| { ok: true; value: ClientCredentials }
| { ok: false; error: string };
const hexToBytes = (hex: string): Uint8Array => {
if (hex.length % 2 !== 0) {
throw new Error("Invalid hex string");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
};
const bytesToHex = (bytes: Uint8Array): string => {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
};
/** Decrypts an encrypted `ClientCredentials`. */
export const decryptClientCredentials = (
/** A base64-encoded string representing the encrypted JSON payload. **/
encryptedCredentials: string,
/** The recipient's X25519 private key as a hex string. **/
privateKeyHex: string,
): DecryptClientCredentialsResult => {
// Base64 decode the encrypted credentials
const payload = atob(encryptedCredentials);
// Guard and validate this deserialization
const parsedPayload = JSON.parse(payload);
// Convert the private key from a hex string to a `Uint8Array`
const privateKey = hexToBytes(privateKeyHex);
// Convert the ephemeral public key from a hex string to a `Uint8Array`
const publicKey = hexToBytes(parsedPayload.ephemeralPublicKey);
const ivBytes = hexToBytes(parsedPayload.iv);
const ciphertextBytes = hexToBytes(parsedPayload.ciphertext);
const sharedSecret = x25519.getSharedSecret(privateKey, publicKey);
const encryptionKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const cipher = gcm(encryptionKey, ivBytes);
const plainTextBytes = cipher.decrypt(ciphertextBytes);
const plaintext = new TextDecoder().decode(plainTextBytes);
let parsed: unknown;
try {
parsed = JSON.parse(plaintext);
} catch {
return { ok: false, error: "Failed to parse decrypted payload as JSON" };
}
// Validate the decrypted payload
if (
typeof parsed !== "object" ||
parsed === null ||
typeof (parsed as Record).accessToken !== "string" ||
typeof (parsed as Record).clientToken !== "string" ||
typeof (parsed as Record).expiresAt !== "string"
) {
return { ok: false, error: "Decrypted payload missing required fields" };
}
return { ok: true, value: parsed as ClientCredentials };
};
/** Generates a new X25519 key pair encryption. */
export const generateKeyPair = (): KeyPair => {
const { secretKey: privateKey, publicKey } = x25519.keygen();
return {
privateKey: bytesToHex(privateKey),
publicKey: bytesToHex(publicKey),
};
};
```
## Initialization parameters
| Property | Type | Required | Description |
| ------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | The anonymous client token obtained after decrypting the `credentials` returned by the [check frame](/platform/frames/check) when the status is `connectionRequired`. |
| `publicKey` | `string` | ✅ | An ephemeral public key generated on the client. See [requirements](#requirements) for details.
The frame uses this key to encrypt the [client credentials](/platform/guides/api-and-sdk-credentials#client-credentials) returned from the connect flow. |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `theme` | `string` | | Pass `dark` or `light` to force a specific appearance. If you omit this, the frame uses the user's system appearance. |
## Events
All events are dispatched using the message pattern described in the [frames protocol](/platform/frames/overview#frames-protocol#messages). Below are the event payloads specific to the connect 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.
```json Example theme={null}
{
"version": 2,
"meta": {
"channelId": "ch_1"
},
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The frame finished loading and the UI is fully rendered. You can use this to coordinate UI transitions, but you don’t need it to complete the flow.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `complete`
The connect flow finished. If it succeeds, the payload includes encrypted client credentials.
```json Example expandable theme={null}
// Active
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "active",
"customer": {
"id": "Y3VzX2FiYzEyMw==",
"country": "USA",
"administrativeArea": "NY"
},
"credentials": ""
}
}
// Pending
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "pending"
}
}
// Unavailable
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "unavailable"
}
}
// Failed
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"status": "failed",
"reason": "Unable to create MoonPay account."
}
}
```
```ts twoslash TypeScript definition expandable theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
enum ConnectionStatus {
/** The connection is valid and can be used. **/
active = "active",
/** The connection could not be completed. This typically occurs for customers whose KYC decisions are delayed. Often these cases are resolved out of band and the customer can connect on a subsequent visit to your app. **/
pending = "pending",
/** The connection cannot be used at the current time. This typically occurs when a KYC-verified customer is using a device or application from a restricted location. **/
unavailable = "unavailable",
/** The connection was not created or is invalid and should not be retried. This usually happens if a customer fails KYC or cannot be onboarded to MoonPay. It can also happen if a customer rejects a connection to your application. In these cases, direct the customer to an alternate flow within your app. **/
failed = "failed",
}
type RampsCapability = {
requirements: Record;
};
type CustomerCapabilities = {
/** Capabilities for the ramps (buy and sell) product. **/
ramps?: RampsCapability;
/** Capabilities for the guest checkout flow. Present when guest checkout is enabled for the partner and the session is a guest-checkout session. **/
guestCheckout?: RampsCapability;
};
type ActiveConnection = {
status: ConnectionStatus.active;
/** The MoonPay customer associated with this connection. **/
customer: {
/** The MoonPay customer identifier. **/
id: string;
/**
* The customer's ISO 3166-1 alpha-3 residential country code.
*
* Example: `"USA"` or `"FRA"`
*/
country?: string;
/**
* The state or province code for the customer's residence, included when
* disclosures apply at the subdivision level.
*
* Example: `"NY"` or `"WA"`
*/
administrativeArea?: string;
/**
* The broader regulatory area for the customer's country, when applicable.
* Currently `"EEA"`.
*/
area?: string;
};
/** Encrypted client credentials containing the accessToken and clientToken. Once decrypted, the value contains a stringified JSON object with the following structure:
*
* {
* "accessToken": "",
* "clientToken": "",
* "expiresAt": "",
* }
*
* - `accessToken`: A token that can be used to make API requests from the client.
* - `clientToken`: A token that can be used to initialize sensitive frames such as Apple Pay.
* - `expiresAt`: An ISO 8601 formatted string indicating when the tokens expire.
**/
credentials: string;
/** Regulatory capabilities for the connected customer. Present only when there is a requirement to surface. **/
capabilities?: CustomerCapabilities;
};
type PendingConnection = {
status: ConnectionStatus.pending;
};
type UnavailableConnection = {
status: ConnectionStatus.unavailable;
};
type FailedConnection = {
status: ConnectionStatus.failed;
/** A developer-friendly description for the failure. **/
reason: string;
};
type ConnectionCompleteEvent = Message<{
kind: "complete";
payload:
| ActiveConnection
| PendingConnection
| UnavailableConnection
| FailedConnection;
}>;
```
The `credentials` value is an encrypted string. Once decrypted, it contains a
JSON object with `accessToken`, `clientToken`, and `expiresAt`. See [API and SDK
credentials](/platform/guides/api-and-sdk-credentials#client-credentials) for
how to use each field.
#### `error`
This event dispatches errors that occur in the flow and, if available, provides steps for recovery.
```json Example expandable theme={null}
// Validation error — one or more initialization parameters are missing or invalid
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "validationError",
"errors": [
{
"code": "invalidClientToken",
"message": "clientToken is required"
}
]
}
}
// Generic error — the connection could not be activated
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "generic",
"message": "Unable to activate connection"
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ValidationErrorSubCode =
| "invalidClientToken"
| "invalidSessionToken"
| "invalidChannelId"
| "invalidPublicKey";
type ConnectFlowError = Message<{
kind: "error";
payload:
| {
code: "validationError";
/** One entry per invalid initialization parameter. **/
errors: {
code: ValidationErrorSubCode;
/** A developer-facing error message. This message is not intended to be rendered in UI. */
message: string;
}[];
}
| {
code: "generic";
/** A developer-facing error message. This message is not intended to be rendered in UI. */
message: string;
};
}>;
```
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
# Google Pay
Source: https://dev.moonpay.com/platform/frames/google-pay
Details on working with the [Google Pay](/platform/guides/pay-with-google-pay) frame, including [guest checkout](/platform/guides/guest-checkout).
## URL
```html theme={null}
https://platform.moonpay.com/platform/v1/google-pay
```
## Requirements
### Size
The frame container **height must be 44px**. Width is flexible; the Google Pay button inside the frame uses 100% of the container width.
### Permissions
The `payment` [permission policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy#iframes) is required.
```tsx Example theme={null}
```
If you render the frame in a sandboxed iframe (for example, for PCI DSS v4 compliance), include `allow-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](https://developers.googleblog.com/google-pay-inside-sandboxed-iframe-for-pci-dss-v4-compliance/) for details.
In [test mode](/platform/overview/test-mode#google-pay), the frame uses `window.prompt` to simulate the Google Pay payment. If your iframe uses the [`sandbox`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox) attribute, include `allow-modals` in addition to the values above.
When you use the `sandbox` attribute, each value serves a specific purpose:
* `allow-scripts` runs `pay.js`.
* `allow-popups` opens the payment window on user interaction.
* `allow-same-origin` grants the browser storage and cookie access needed for compatibility.
* `allow-forms` submits the Google Pay sign-in form.
* `allow-modals` shows the test-mode billing prompt (`window.prompt`).
### Android WebView
Google Pay relies on the [Payment Request API](https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API), which is disabled by default in Android WebView, so embedding this frame natively requires extra setup.
Requires Google Play services **25.18.30+** and Android WebView for Chrome
**137+**. The `isReadyToPay` API returns `false` when these requirements are
not met. See [Using Android
WebView](https://developers.google.com/pay/api/android/guides/recipes/using-android-webview)
for full details.
Add the AndroidX WebKit dependency:
```kotlin theme={null}
// build.gradle.kts (app level)
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
}
```
Declare the required intent queries in `AndroidManifest.xml`:
```xml theme={null}
```
Enable the Payment Request API on the WebView, after `settings.javaScriptEnabled = true`:
```kotlin theme={null}
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
WebSettingsCompat.setPaymentRequestEnabled(webView.settings, true)
}
```
For a complete native walkthrough that covers rendering the frame and wiring events, see [Android manual integration](/platform/guides/manual-integration/android#google-pay-frame).
## Initialization parameters
| Property | Type | Required | Description |
| ----------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientToken` | `string` | ✅ | The [client token](/platform/guides/api-and-sdk-credentials#client-token) from the [connect flow](/platform/guides/connect-a-customer), or from the [connection check](/platform/frames/check) when you offer [guest checkout](/platform/guides/guest-checkout). |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `signature` | `string` | ✅ | The quote `signature` from the quote endpoint. Pass `signature` as returned. |
| `externalTransactionId` | `string` | | Your own identifier for the transaction. Stored and associated with the MoonPay transaction for correlation. |
| `theme` | `string` | | Pass `dark` or `light` to force a specific appearance. If you omit this, the frame uses the user's system appearance. |
## Events
All events are dispatched using the message pattern described in the [frames protocol](/platform/frames/overview#frames-protocol#messages). Below are the event payloads specific to the Google Pay 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The frame finished loading and the UI is fully rendered. You can use this to coordinate UI transitions if needed.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `buttonPressed`
The customer tapped the Google Pay button. The frame emits this the instant the
button is tapped, before Android presents the Google Pay 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 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`](#complete), which
reports the outcome once the payment resolves.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "buttonPressed"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ButtonPressedEvent = Message<{
kind: "buttonPressed";
}>;
```
#### `complete`
The transaction is complete. Use the transaction ID to track status updates (for example, by polling or via webhooks).
```json Example (success) theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"id": "txn_01",
"status": "pending"
}
}
}
```
```json Example (fail) theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"status": "failed",
"failureReason": "Your payment was declined by your bank.",
"failureCode": "authorizationDeclined"
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
enum TransactionStatus {
/** The transaction has successfully completed. The payment has been made and the crypto has been transferred. **/
completed = "completed",
/** The payment has been completed and the crypto transfer is underway. **/
pending = "pending",
/** The transaction has failed. No payment was applied and the crypto was not transferred. **/
failed = "failed",
}
type FailureCode =
| "transactionNotAllowed"
| "validationError"
| "serviceUnavailable"
| "authorizationDeclined"
| "unknown";
type Transaction =
| {
/** The MoonPay identifier for this transaction. **/
id: string;
/** The status of the transaction. **/
status: TransactionStatus.completed | TransactionStatus.pending;
}
| {
status: TransactionStatus.failed;
/** A stable, machine-readable code identifying the failure category. Branch on this value for programmatic handling. May be omitted when the frame cannot classify the failure. **/
failureCode?: FailureCode;
/** A developer-friendly error message detailing the reason for the transaction failure. **/
failureReason: string;
};
type GooglePayCompleteEvent = Message<{
kind: "complete";
payload: {
transaction: Transaction;
};
}>;
```
##### Failure codes
When a `complete` event has `status: "failed"`, the `failureCode` field indicates the failure category. Use this value — not `failureReason` — for programmatic branching. `failureReason` is a human-readable fallback suitable for display when you do not have custom copy for a given code.
`failureCode` is optional. When the frame cannot classify a failure, it sends
`failureReason` alone — fall back to showing that message to the customer.
| `failureCode` | Default `failureReason` | When it fires | Recommended handling |
| ----------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `transactionNotAllowed` | "This transaction is not allowed for your account." | Payment method or account is not eligible (for example, region restriction or KYC limit). | Show the `failureReason` and guide the customer to resolve the issue or choose a different payment method. |
| `validationError` | "The transaction request was invalid." | Request failed validation (unsupported currency, missing parameters). | Check the quote parameters and retry with corrected values. |
| `serviceUnavailable` | "Service temporarily unavailable. Please try again." | Upstream payment service is degraded or returned a 5xx response. | Retry with exponential back-off. |
| `authorizationDeclined` | "Your payment was declined by your bank." | The issuer or gateway rejected the authorization. | Prompt the customer to try a different card. |
| `unknown` | "An unexpected error occurred." | Any unexpected or unclassified error, including thrown authorization exceptions and fraud blocks. | Show the `failureReason` to the customer and retry. |
Quote expiry is reported through the [`error`](#error) event with `code:
"quoteExpired"`, not through `complete`. Listen for both events to cover all
failure paths.
#### `challenge`
Verification is required before the transaction can proceed. Render the
[challenge frame](/platform/frames/challenge) at the provided URL. Do not
construct the URL yourself — use it as-is.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "challenge",
"payload": {
"kind": "frame",
"url": "https://platform.moonpay.com/platform/v1/challenge?challengeToken=..."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type GooglePayChallengeEvent = Message<{
kind: "challenge";
payload: {
kind: string;
/** Fully-formed URL to pass directly as the challenge frame src. */
url: string;
};
}>;
```
#### `error`
This event dispatches errors that occur in the flow and, if available, provides steps for recovery.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "configurationError",
"message": "The frame configuration is invalid."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type GooglePayError = Message<{
kind: "error";
payload: {
code:
| "configurationError"
| "quoteExpired"
| "invalidQuote"
| "googlePayUnavailable"
| "generic";
/** A developer-facing error message with details on recovery or documentation. This message is not intended to be rendered in UI. */
message: string;
};
}>;
```
| Code | Description |
| ---------------------- | -------------------------------------------------------------------- |
| `configurationError` | The frame configuration is invalid. Check initialization parameters. |
| `quoteExpired` | The quote has expired. Fetch a new quote and send it via `setQuote`. |
| `invalidQuote` | The quote signature is invalid or malformed. |
| `googlePayUnavailable` | Google Pay is not available in the current environment or browser. |
| `generic` | An unexpected error occurred. |
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
#### `setQuote`
Provide a new quote to the frame. Pass `signature` as returned by the quote endpoint. Upon receiving this event, the frame disables the Google Pay button until the quote is revalidated.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "setQuote",
"payload": {
"quote": {
"signature": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type SetQuoteEvent = Message<{
kind: "setQuote";
payload: {
quote: {
/** The signature from a valid quote. **/
signature: string;
};
};
}>;
```
# Overview
Source: https://dev.moonpay.com/platform/frames/overview
Protocol details for co-branded and headless [frames](/platform/overview/core-concepts#frames).
Frames communicate with your app using `postMessage`. Each frame defines its own events and payloads. If you use the MoonPay SDK, it handles message serialization, validation, and dispatch for you. If you integrate frames directly (for example, with your own WebView or iframe wrapper), this page documents the shared protocol so you know what to implement.
## Libraries
Coming soon!
If you’re not using the SDK, MoonPay libraries can help you manage `postMessage` communication on web and mobile (via WebViews). Until those ship, use the protocol details below to build your own bridge.
## Frames protocol
### Messages
Frames use an event-driven model. You and the frame exchange events using a strongly typed message structure. Treat these messages like an API contract between two parties: the parent window (or app) and the frame.
#### Transport
In both web and mobile apps, frames send and receive messages over `postMessage` as **stringified JSON**.
If you integrate directly, you handle serialization and validation yourself. If you use the SDK, it handles this for you.
#### Format
Every message follows the same envelope format. The `kind` tells you what event you’re handling, and the `payload` shape depends on that `kind`.
| Field | Type | Required | Description |
| ---------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------- |
| `version` | `2` | ✅ | The frames protocol version.
This value will always be `2`. |
| `meta` | `object` | ✅ | Transport metadata for the message. |
| `meta.channelId` | `string` | ✅ | A unique identifier for messages between frames. |
| `kind` | `enum` | ✅ | The name of the event. |
| `payload` | `object` | | An object containing the data for different events. This value depends on the `kind`. |
```json Example payload (deserialized) theme={null}
{
"version": 2,
"meta": {
"channelId": "some_unique_value"
},
"kind": "example",
"payload": {
"example": "an example payload"
}
}
```
```json Example payload (serialized) theme={null}
"{\"version\":\"2\",\"meta\":{\"channelId\":\"some_unique_value\"},\"kind\":\"example\",\"payload\":{\"example\":\"an example payload\"}}"
```
#### Validation and safety checks
You’ll have an easier time (and fewer mysterious bugs) if you validate messages like you would any external input:
* **Check the origin and sender**: only accept messages from the frame origin(s) you expect, and ignore everything else.
* **Parse defensively**: `postMessage` delivers strings; treat JSON parsing as fallible and handle errors.
* **Verify the envelope**: reject messages that don’t match the expected `version`, don’t include a `meta.channelId`, or use an unknown `kind`.
* **Route by `channelId`**: if you can have multiple frames open at once, use `meta.channelId` to keep messages from crossing streams.
### Lifecycle
Each frame follows the same basic handshake lifecycle to establish a bi-directional channel with your app. The SDK manages this automatically and gives you an events callback; in a direct integration, you implement these steps yourself.
```mermaid theme={null}
sequenceDiagram
%% autonumber
participant a as App
participant f as Frame
%% -----------------
activate f
a ->> a: Generate a channel ID
a ->> f: Inject the WebView or iframe with URL params including a channelId.
alt If no handshake request received in 5s
a -x a: Handle loading error
end
f ->> a: Send handshake w/channel ID
a ->> f: Reply with ack (w/channel ID)
break ack from an origin not on your allowlist
f -x a: Send error (code: "generic") and terminate the connection
end
f ->> f: Validate params
break Invalid params
f -x a: Send error and terminate the connection
end
f <<->> a: Bi-directional channel opened
deactivate f
```
In practice, you’ll typically: generate a channel, wait for the handshake, ack it, then start handling `kind` events for that channel. If the handshake doesn’t arrive quickly, fail fast and show a useful error to the developer (or retry, if that fits your app).
If the frame doesn't recognize your ack's origin as one you've allowlisted for your integration, it sends a `generic` error and closes the channel instead of leaving you waiting on a handshake that will never complete. Add the missing domain in your [MoonPay Dashboard settings](https://dashboard.moonpay.com/developers) and retry.
# Reset
Source: https://dev.moonpay.com/platform/frames/reset
Clear a user's session and authentication state
The reset frame is a headless page hosted on a MoonPay domain. Use it to clear the user's authentication tokens stored on MoonPay's domain, allowing partners to log users out. The frame communicates completion or errors to the parent window via postMessage.
## URL
```html theme={null}
https://platform.moonpay.com/platform/v1/reset
```
## Initialization parameters
| Parameter | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `clientToken` | `string` | | The client token returned from the connect flow. Authorizes your domain to embed the frame (see note). |
| `apiKey` | `string` | | Your publishable API key. Also authorizes your domain to embed the frame. |
The reset frame carries no session of its own, so MoonPay identifies your
integration from the token you pass — either your connect-flow `clientToken`
or your publishable `apiKey`. That identity authorizes your domain to embed
the frame (via the `frame-ancestors` Content Security Policy). Without one,
the browser blocks the frame from loading on your domain — it never runs, so
it cannot even report an `error`; the parent simply never receives a
`handshake`.
## Events
All events are dispatched using the message pattern described in the [frames protocol](/frames/overview#frames-protocol#messages). Below are the event payloads specific to the reset frame.
### Outbound events
frame->parent
These events are sent from this frame to the parent window.
#### `handshake`
Sent when the frame loads. The parent must respond with `ack`.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `complete`
Sent when the user's session has been successfully cleared.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ResetCompleteEvent = Message<{
kind: "complete";
}>;
```
#### `error`
Sent when the reset fails.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "generic",
"message": "Failed to clear session"
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ResetErrorEvent = Message<{
kind: "error";
payload: {
code: "generic";
/** A developer-facing error message with details on recovery or documentation. This message is not intended to be rendered in UI. */
message: string;
};
}>;
```
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Must be sent in response to `handshake`. The frame will not proceed until it receives this.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
# Widget
Source: https://dev.moonpay.com/platform/frames/widget
Details on working with the [widget](/platform/guides/pay-with-widget) frame.
## URL
```html theme={null}
https://platform.moonpay.com/platform/v1/widget
```
## Requirements
### Permissions
The `payment` [permission policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy#iframes) is required.
```tsx Example theme={null}
```
## Initialization parameters
| Property | Type | Required | Description |
| ----------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flow` | `string` | ✅ | The transaction flow. Currently only `buy` is supported. |
| `clientToken` | `string` | ✅ | The [client token](/platform/guides/api-and-sdk-credentials#client-token) returned from the [connect flow](/platform/guides/connect-a-customer). |
| `quoteSignature` | `string` | ✅ | The quote `signature` from the quote endpoint. Pass `signature` as returned. |
| `channelId` | `string` | ✅ | A unique identifier for the frame generated on your client. This value is attached to each `postMessage` payload to help identify messages.
The format of this string is up to you. |
| `externalTransactionId` | `string` | | Your own identifier for the transaction. Stored and associated with the MoonPay transaction for correlation. |
| `theme` | `string` | | Pass `dark` or `light` to force a specific appearance. If you omit this, the frame uses the user's system appearance. |
## Events
All events are dispatched using the message pattern described in the [frames protocol](/platform/frames/overview#frames-protocol#messages). Below are the event payloads specific to the widget 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.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "handshake"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type HandshakeEvent = Message<{
kind: "handshake";
}>;
```
#### `ready`
The widget finished loading and the UI is visible.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ready"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type ReadyEvent = Message<{
kind: "ready";
}>;
```
#### `transactionCreated`
A transaction has been initiated. The customer may still need to complete additional steps such as 3-D Secure authorization.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "transactionCreated",
"payload": {
"transaction": {
"id": "txn_01",
"status": "waitingAuthorization"
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type TransactionCreatedEvent = Message<{
kind: "transactionCreated";
payload: {
transaction: {
/** The MoonPay identifier for this transaction. **/
id: string;
/** The current status of the transaction. **/
status: string;
};
};
}>;
```
#### `complete`
The transaction has reached a terminal state. Use the transaction ID to track status updates via polling or webhooks.
```json Example (success) theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"id": "txn_01",
"status": "complete"
}
}
}
```
```json Example (fail) theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "complete",
"payload": {
"transaction": {
"status": "failed",
"failureReason": "The payment could not be completed."
}
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
enum TransactionStatus {
complete = "complete",
pending = "pending",
failed = "failed",
}
type Transaction =
| {
/** The MoonPay identifier for this transaction. **/
id: string;
/** The status of the transaction. **/
status: TransactionStatus.complete | TransactionStatus.pending;
}
| {
status: TransactionStatus.failed;
/** A developer-friendly error message detailing the reason for the transaction failure. **/
failureReason: string;
};
type WidgetCompleteEvent = Message<{
kind: "complete";
payload: {
transaction: Transaction;
};
}>;
```
#### `error`
An error occurred in the widget flow.
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "error",
"payload": {
"code": "apiError",
"message": "Failed to build widget URL."
}
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type WidgetError = Message<{
kind: "error";
payload: {
code: "configurationError" | "apiError" | "generic";
/** A developer-facing error message. Not intended for end-user display. */
message: string;
};
}>;
```
### Inbound events
parent->frame
These events are sent from the parent window to this frame.
#### `ack`
Acknowledge the [handshake](#handshake).
```json Example theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "ack"
}
```
```ts twoslash TypeScript definition theme={null}
type Message = T & {
version: 2;
meta: { channelId: string };
};
type AckEvent = Message<{
kind: "ack";
}>;
```
# API and SDK credentials
Source: https://dev.moonpay.com/platform/guides/api-and-sdk-credentials
Understand the tokens and credentials you need for your integration.
MoonPay will work with you directly to set up your account and credentials.
## Server credentials
### Secret key
A server-to-server credential passed in the `X-Api-Key` request header.
Keep your secret key secure and never expose it. Never commit it to your
codebase or send it to your frontend.
```text Test mode theme={null}
X-Api-Key: sk_test_123
```
```text Live mode theme={null}
X-Api-Key: sk_live_123
```
## Client credentials
The check and connect frames return an encrypted `credentials` string. Once
decrypted, it contains the following tokens. Use them to make API requests from
your frontend and initialize frames for sensitive actions.
```json Decrypted credentials theme={null}
{
"accessToken": "eyJhbGci...",
"clientToken": "eyJhbGci...",
"expiresAt": "2026-12-09T07:16:57Z"
}
```
Never persist client credentials to disk or storage. Hold them in memory only.
Use your server to get a new `sessionToken` on each app visit. When you
receive new credentials (for example, authenticated credentials after a
connect flow), replace the previously stored ones.
### Session token
A token you create on your server and send to your frontend. You use it to start a [connect flow](/platform/guides/connect-a-customer).
### Access token
A token returned from the [check frame](/platform/frames/check) and the [connect frame](/platform/frames/connect). Use it to make API requests from your frontend (via the SDK or directly), such as:
* Getting quotes
* Listing payment methods
* Listing transactions
Anonymous credentials returned when `connectionRequired` give you a scoped access token. Authenticated credentials returned after the connect flow give you a fully-privileged one. Always replace stored credentials when you receive new ones.
This token is intended for client use and shouldn’t be persisted to disk.
### Client token
A token returned from the [check frame](/platform/frames/check) and the [connect frame](/platform/frames/connect). Use it to initialize subsequent frames (for example, the Apple Pay frame or the connect frame). Within frames, this token is used to make authenticated requests.
When `connectionRequired` is returned, pass this anonymous `clientToken` to the connect frame. After authentication, replace it with the `clientToken` from the authenticated credentials.
This token is intended for client use and shouldn’t be persisted to disk.
# Hosted onboarding
Source: https://dev.moonpay.com/platform/guides/connect-a-customer
Connect a customer's account with login and full KYC in MoonPay's co-branded connect frame.
Connect a customer's MoonPay account so you can list payment methods, get executable quotes, and execute transactions. This is the MoonPay-hosted onboarding path: the co-branded connect frame renders login and the full KYC flow, so you build no verification UI. To compare it with onboarding via API and guest checkout, see [Choose an onboarding path](/platform/guides/onboarding-paths). Before you start, review the [requirements](/platform/overview/requirements).
## Prerequisites
* A server that can create session tokens with your secret key.
* A client that can render frames (SDK or manual integration).
Connecting is a one or two-step process depending on whether the customer is new or returning:
* If the customer has never connected, render the co-branded connect frame.
* If the customer has connected before, first check whether their connection is still valid. You only need to render UI again if the connection has [expired](#connection-required).
For all cases, first [create a session](#create-a-session), then [check if a connection exists](#check-connection). If a connection is required, initialize the [connect flow](#connect-flow).
```mermaid theme={null}
sequenceDiagram
autonumber
actor c as customer
participant s as Your server
box Your app
participant fe as Your frontend
participant cf as MoonPay frame (check connection)
participant cnf as MoonPay frame (connection UI)
end
participant api as MoonPay API
%% -----------------
c ->> fe: Customer visits app and signs in
fe ->> s: Request session token
s ->> api: POST /platform/v1/sessions
api ->> s: { sessionToken: "c3N0XzAwMQ" }
s ->> fe: Send session token
activate cf
fe ->> cf: getConnection()
cf -->> fe: Dispatch result + credentials
deactivate cf
fe ->> fe: Store credentials
alt active connection
fe ->> fe: Continue to buy flow
else requires connect flow
activate cnf
fe ->> cnf: connect(clientToken)
c ->> cnf: Sign in or onboard to MoonPay
cnf -->> fe: Dispatch result + credentials
deactivate cnf
fe ->> fe: Replace stored credentials
alt active connection?
fe ->> fe: Continue to buy flow
end
end
```
## Create a session
To initiate a session on your server, provide:
1. A unique identifier from your system for the customer (`externalCustomerId`).
2. The IP address of the customer's device. This is used across frames to ensure the integrity of the session.
Once initiated, you receive a [`sessionToken`](/platform/guides/api-and-sdk-credentials#session-token) to send to your frontend.
```ts Create session token theme={null}
// Server-side code example
const url = "https://api.moonpay.com/platform/v1/sessions";
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
"X-Api-Key": "sk_test_123",
},
method: "POST",
body: JSON.stringify({
externalCustomerId: "your_user_id",
deviceIp: "...ip address from client",
}),
});
console.log(await res.json());
```
```json Result theme={null}
{
"sessionToken": "c3N0XzAwMQ=="
}
```
Check the [API reference](/api-reference/platform/endpoints/sessions/create)
for detailed usage.
## Check connection
Using the `sessionToken`, check if the customer already has an active connection. No UI appears in this step, but it runs in a frame. You can check the session using the SDK or [manually](/platform/frames/check).
The check frame always returns encrypted credentials. Hold them in memory only and do not persist them, regardless of the status returned. For an `active` connection these are authenticated credentials. For `connectionRequired`, these are anonymous credentials whose `clientToken` you pass into the connect flow.
```ts Check the connection theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
// Create the client with your session token
const client = createClient({
sessionToken: "c3N0XzAwMQ==", // The session token from your server
});
// Check if the customer has an active connection
const connectionResult = await client.getConnection();
if (!connectionResult.ok) {
// Handle error
}
console.log(connectionResult.value);
```
```ts Result (active) theme={null}
{
status: "active",
customer: {
id: "Y3VzX2FiYzEyMw=="
},
// Encrypted credentials — the SDK decrypts and stores them internally
credentials: "ZW5jXzAwMQ==",
capabilities: {}
}
```
```ts Result (requires connection) theme={null}
{
status: "connectionRequired",
// Encrypted credentials — the SDK decrypts and stores them internally
credentials: "ZW5jXzAwMQ=="
}
```
The SDK injects an invisible frame to check the connection. If you are integrating without the SDK, load the frame directly and listen for the [`postMessage` events](/platform/frames/check#events).
## Low-friction authentication
You can simplify login for returning customers by passing the optional `email` and `phoneNumber` parameters when you [create a session](/api-reference/platform/endpoints/sessions/create). Both are optional on this path. [Guest checkout](/platform/guides/guest-checkout) requires them.
If these values match an existing MoonPay account, the [connect frame](#connect-flow) skips the full login and prompts the customer to enter an OTP code sent to their phone.
## Connect flow
If you need to create or revalidate a connection, initialize the connect flow with the `clientToken` from the anonymous credentials returned by the check frame. The SDK provides hooks to coordinate rendering the connect UI (for example, to animate a modal or sheet). You can also do this [manually](/platform/frames/connect).
When the connect flow completes, replace the anonymous credentials from the check step with the authenticated credentials from the `complete` event.
The resulting connection has one of the following [statuses](#connection-statuses): `active`, `pending`, `unavailable`, or `failed`.
In mobile apps, present the connect flow as a full sheet. See [presentation
and appearance](/platform/guides/presentation-and-appearance) for UI guidance.
```ts Initialize connect with SDK theme={null}
import { createClient, type ConnectEvent } from "@moonpay/platform-sdk-web";
// Create the client
const client = createClient({
sessionToken: "c3N0XzAwMQ==", // The session token from your server
});
// Initialize the connect flow
const connectResult = await client.connect({
container: connectContainer, // DOM element to render the connect frame
theme: { appearance: "dark" }, // Optional: force dark or light mode
onEvent: (event: ConnectEvent) => {
switch (event.kind) {
case "ready":
// The frame is ready and rendered
break;
case "complete":
// The connection is complete
console.log(event.connection);
// { status: "active", customer: { id: "..." }, credentials: { accessToken: "...", clientToken: "...", expiresAt: "..." } }
// You can unmount the frame using the reference in the payload
event.payload.frame.dispose();
break;
case "error":
// Handle error
console.error(event.payload.message);
break;
}
},
});
// If there is an error setting up the connect frame, no events are
// dispatched via the `onEvent` callback, and you receive an error here.
if (!connectResult.ok) {
// Handle error
}
// If the frame is successfully mounted, the returned value provides a reference for disposal at any time.
connectResult.value.dispose();
```
```ts Result (complete event) theme={null}
{
kind: "complete",
connection: {
status: "active",
customer: {
id: "Y3VzX2FiYzEyMw=="
},
credentials: {
accessToken: "c2F0XzAwMQ==",
clientToken: "c2N0XzAwMQ==",
expiresAt: "2026-12-09T07:16:57Z"
},
capabilities: {
ramps: {
requirements: {}
}
}
},
payload: {
frame: {
dispose: [Function]
}
}
}
```
When you receive the `complete` event, the payload includes authenticated client
credentials and a `customer` object identifying the connected MoonPay customer.
Discard any anonymous credentials from the check step and use the new ones instead.
Use them to make scoped API calls from your frontend (for example, listing
payment methods and getting quotes). See [Pay with Apple
Pay](/platform/guides/pay-with-apple-pay) for a complete example flow.
```ts Active connection theme={null}
{
kind: "complete",
connection: {
status: "active",
customer: {
id: "Y3VzX2FiYzEyMw=="
},
credentials: {
accessToken: "c2F0XzAwMQ==",
clientToken: "c2N0XzAwMQ==",
expiresAt: "2026-12-09T07:16:57Z"
}
},
payload: {
frame: {
dispose: [Function]
}
}
}
```
[Client
credentials](/platform/guides/api-and-sdk-credentials#client-credentials)
should never be persisted and should only be held in memory as this could pose
a security risk.
## Connection statuses
### Active
An `active` status means the connection is valid and can be used. Active connections typically remain live for 180 days without revalidation. If the connection expires, refresh it via the connect flow.
### Terms acceptance required
A `termsAcceptanceRequired` status means the customer must accept updated Terms of Use before the connection can be used. No credentials are attached. Present the terms in your UI, capture the acceptance timestamp, pass it as `termsAcceptedAt` on a new session, then re-check the connection. See [Terms acceptance](/platform/guides/terms-acceptance#handle-termsacceptancerequired) for the presentation methods and recovery loop.
### Unavailable
An `unavailable` status means the connection cannot be used at the current time. This typically occurs when a KYC-verified customer is using a device or application from a restricted location.
### Pending
A `pending` status typically occurs for customers whose KYC decisions are delayed. Often these cases are resolved out of band and the customer can connect on a subsequent visit to your app.
### Failed
A `failed` status is a terminal state. This usually happens if the customer fails KYC or cannot be onboarded to MoonPay. It can also happen if the customer rejects the connection. In these cases, direct the customer to an alternate flow in your app.
### Connection required
The `connectionRequired` status is returned from the [check frame](#check-connection) as a signal to guide the customer through the full [connect flow](#connect-flow). This status is returned for new customers who have not connected to your app, or returning customers whose connections have expired. The response also includes anonymous `credentials` — keep them only in memory and use the `clientToken` to initialize the connect flow. If the payload includes `mismatch: true`, the session's email and phone number resolve to two different MoonPay customers — route the customer through the connect flow proactively, before rendering payment UI such as Apple Pay or Google Pay.
# Onboarding via API
Source: https://dev.moonpay.com/platform/guides/customer-api
Onboard customers in your own UI with the Customer API: check KYC status, submit outstanding requirements, and handle verification.
Run KYC in your own UI. With the Customer API you own the verification screens: MoonPay tells you what's required through `kyc.status` and `kyc.requirements`, you capture the data from the customer, and you submit it, keyed on `customerId`. It answers three questions, server-side or client-side: is this customer KYC'd, what's missing, and how do you submit it.
This is the API-driven onboarding path. If you're deciding between this path, the hosted connect flow, and guest checkout, see [Choose an onboarding path](/platform/guides/onboarding-paths).
See the [Going Live](/platform/overview/going-live) section for requirements you must meet before taking this integration to production.
## Prerequisites
* A server that can create session tokens with your secret key.
* A client that can render the Auth frame, with the [SDK](/platform/sdk-reference/web/setup-auth) or [manually](/platform/frames/auth).
* Either a [secret key](/platform/guides/api-and-sdk-credentials#secret-key) or an [access token](/platform/guides/api-and-sdk-credentials#access-token) for API calls. Secret-key calls are scoped to your customers; access-token calls are scoped to the token's own customer.
* A UI surface that presents MoonPay's Terms of Use and Privacy Policy and records the customer's acceptance. See [Terms acceptance](/platform/guides/terms-acceptance).
Customers who arrive through the hosted [connect flow](/platform/guides/connect-a-customer) also work with this API. You need a customer `id` from an authenticated connection, however that connection was established.
## How it works
1. You [present the terms](#present-the-terms): show MoonPay's Terms of Use and Privacy Policy in your UI and capture the acceptance timestamp.
2. You [authenticate the customer](#authenticate-the-customer): create a session, check the connection with `skipKyc: true`, and render the Auth frame when the customer isn't connected yet. This gives you the customer's `id`.
3. You call `GET /customers/{id}` to read `kyc.status` and `kyc.requirements`.
4. You submit outstanding requirements with `PATCH /customers/{id}/kyc`, and upload any required files. Once every requirement is submitted, verification starts automatically. If MoonPay can't complete verification from the submitted data alone, the response includes a hosted challenge to render.
5. Verification runs asynchronously, so you poll `GET /customers/{id}` until it reaches a terminal status or surfaces new requirements, then handle the result.
## Present the terms
Before a customer's first transaction, present MoonPay's Terms of Use and Privacy Policy in your UI using one of the two presentation methods, and capture the timestamp at the moment the customer explicitly accepts. You pass that timestamp as `termsAcceptedAt` when you create the session in the next step. For the presentation methods, the rendering rules, and what the recorded acceptance covers, see [Terms acceptance](/platform/guides/terms-acceptance).
## Authenticate the customer
Every Customer API call is keyed on a customer `id` from an authenticated connection. To establish one without the hosted connect flow, create a session, check the connection, and render the Auth frame.
On your server, create a session token with your secret key and send it to your client. Include the customer's `email` when you create the session: the [Auth frame](/platform/frames/auth) identifies the customer by that address and delivers the one-time passcode there. If no MoonPay account exists for the email yet, the OTP step creates one.
```ts Create session token theme={null}
// Server-side code example
const url = "https://api.moonpay.com/platform/v1/sessions";
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
"X-Api-Key": "sk_test_123",
},
method: "POST",
body: JSON.stringify({
externalCustomerId: "your_user_id",
deviceIp: "...ip address from client",
}),
});
console.log(await res.json());
```
```json Result theme={null}
{
"sessionToken": "c3N0XzAwMQ=="
}
```
Include the `termsAcceptedAt` timestamp you captured when you [presented the terms](#present-the-terms) — see [Record the acceptance](/platform/guides/terms-acceptance#record-the-acceptance) for the field's rules.
On your client, check for an existing connection with `client.getConnection({ skipKyc: true })`. The `skipKyc` option opts the check out of KYC-based statuses, so an unverified customer isn't blocked with `pending` or `failed` before you submit their data. The check still surfaces legal statuses: `termsAcceptanceRequired` appears when a Terms of Use attestation is outstanding — see [Terms acceptance](/platform/guides/terms-acceptance#handle-termsacceptancerequired) for how to present the terms and record acceptance.
When the status is `connectionRequired`, render the Auth frame with `client.setupAuth()`. The Auth frame handles authentication only, with no payment-method setup or KYC steps. On the `complete` event with `status: "active"`, you have the customer's `id` and subsequent SDK calls are authenticated automatically.
```ts Authenticate the customer theme={null}
import { createClient, type AuthEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const connectionResult = await client.getConnection({ skipKyc: true });
if (!connectionResult.ok) {
// Handle error
return;
}
if (connectionResult.value.status === "connectionRequired") {
const authResult = await client.setupAuth({
container: document.querySelector("#authContainer"),
onEvent: (event: AuthEvent) => {
switch (event.kind) {
case "ready":
// The auth UI is rendered. Reveal the container if you hide it while loading.
break;
case "complete":
if (event.payload.status === "active") {
// Customer authenticated — SDK calls are now authenticated.
console.log(event.payload.customer.id);
}
break;
case "error":
console.error(event.payload);
break;
}
},
});
if (!authResult.ok) {
// Handle error
console.error(authResult.error.kind, authResult.error.message);
return;
}
// Remove the frame from the DOM now that the flow has completed:
authResult.value.dispose();
}
```
If the connection check returns `status: "active"`, the customer is already authenticated: read `customer.id` from the result and skip the Auth frame. For the other statuses, including `termsAcceptanceRequired`, see [`client.getConnection()`](/platform/sdk-reference/web/get-connection#connection).
The connection check primes the client with the token the Auth frame needs. If
you call `setupAuth()` without a prior connection check that resolved with
`status: "connectionRequired"`, it returns a `configurationError`. See
[`client.setupAuth()`](/platform/sdk-reference/web/setup-auth) for the full
event and error reference.
## Get a customer
Returns the customer's KYC standing and outstanding requirements. For request
and response details, see the
[Get a customer API](/api-reference/platform/endpoints/customers/get).
```ts Get a customer theme={null}
const res = await fetch(
`https://api.moonpay.com/platform/v1/customers/${customerId}`,
{
headers: { "X-Api-Key": "sk_test_123" },
},
);
const { data: customer } = await res.json();
console.log(customer.kyc.status, customer.kyc.requirements);
```
```json Result theme={null}
{
"data": {
"id": "c1a2b3c4-0000-4000-8000-000000000000",
"externalCustomerId": "your_user_id",
"kyc": {
"status": "collecting",
"requirements": {
"basicDetails": { "status": "complete" },
"residentialAddress": {
"status": "incomplete",
"requiredFields": ["street", "locality", "postalCode"]
},
"questionnaires": {
"status": "incomplete",
"requiredFields": ["customerDueDiligence", "enhancedDueDiligence"]
}
}
}
}
}
```
When a requirement is `incomplete`, `requiredFields` lists the outstanding fields the customer must provide. Only `basicDetails`, `residentialAddress`, `taxIdentifiers`, and `questionnaires` populate it; it's omitted when the requirement is `complete`, and the other requirement categories don't surface field-level detail. For `questionnaires`, the values are the outstanding questionnaire types — see [Due diligence questionnaires](/platform/guides/kyc-data-requirements#due-diligence-questionnaires).
## Submit KYC data
Submit one or more outstanding requirement categories. Only send the categories listed as `incomplete` in `kyc.requirements`. For the fields and documents each country requires, see [KYC data requirements](/platform/guides/kyc-data-requirements).
```ts Submit KYC data theme={null}
const res = await fetch(
`https://api.moonpay.com/platform/v1/customers/${customerId}/kyc`,
{
method: "PATCH",
headers: { "Content-Type": "application/json", "X-Api-Key": "sk_test_123" },
body: JSON.stringify({
residentialAddress: {
street: "123 Main St",
locality: "San Francisco",
administrativeArea: "CA",
postalCode: "94105",
country: "USA",
},
}),
},
);
const { data: customer } = await res.json();
```
Returns the updated customer, so you can re-check `kyc.requirements` for what's still outstanding. For request and response details, see the [Submit KYC data API](/api-reference/platform/endpoints/customers/submit-kyc).
You submit due-diligence questionnaires through this same endpoint. When `questionnaires` is `incomplete` in `kyc.requirements`, its `requiredFields` names the questionnaires to complete: `customerDueDiligence`, `enhancedDueDiligence`, or both. Submit each one as an entry in `questionnaires[]`, with a `type` and an `answers` object.
Monetary answers (`grossAnnualIncome`, `expectedTransactionAmountPerMonth`, and `netWorth`) take a `currency` and an `amount`. Denominate each amount in the reporting currency for the customer's `residentialAddress.country`; the API rejects any other `currency` with a 400 validation error that identifies the expected one. If the customer's local currency differs from the reporting currency, convert the amount before submitting, using a current exchange rate. For the country-to-currency mapping and the full questionnaire field reference, see [Due diligence questionnaires](/platform/guides/kyc-data-requirements#due-diligence-questionnaires).
```ts Submit questionnaire answers theme={null}
const res = await fetch(
`https://api.moonpay.com/platform/v1/customers/${customerId}/kyc`,
{
method: "PATCH",
headers: { "Content-Type": "application/json", "X-Api-Key": "sk_test_123" },
body: JSON.stringify({
questionnaires: [
{
type: "customerDueDiligence",
answers: {
employmentStatus: "employedOrSelfEmployed",
accountPurpose: "investing",
// USD: the reporting currency for a USA residential address
grossAnnualIncome: { currency: "USD", amount: "55000" },
sourceOfWealth: "salary",
transactionFrequencyPerMonth: 7,
expectedTransactionAmountPerMonth: {
currency: "USD",
amount: "1500",
},
},
},
],
}),
},
);
const { data: customer } = await res.json();
```
## Upload a file
For document-based requirements (for example, `identityDocuments`, `selfie`, or `proofOfAddress`), first get a presigned upload URL, `PUT` the file to it, then confirm the upload.
Before you submit selfie or identity-document images, display the required
biometric consent disclosure to the customer. See [Biometric consent for
selfie and document
images](/platform/guides/terms-acceptance#biometric-consent-for-selfie-and-document-images).
```ts Get an upload URL theme={null}
const uploadUrlRes = await fetch(
`https://api.moonpay.com/platform/v1/customers/${customerId}/files/upload-url`,
{
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": "sk_test_123" },
body: JSON.stringify({ fileType: "passport", mimeType: "image/jpeg" }),
},
);
const { data: uploadUrl } = await uploadUrlRes.json();
```
```ts Upload the file theme={null}
await fetch(uploadUrl.url, {
method: "PUT",
headers: uploadUrl.headers,
body: passportImageBlob,
});
```
```ts Confirm the upload theme={null}
const filesRes = await fetch(
`https://api.moonpay.com/platform/v1/customers/${customerId}/files`,
{
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": "sk_test_123" },
body: JSON.stringify({
files: [{ uploadId: uploadUrl.uploadId, fileType: "passport" }],
}),
},
);
```
The upload URL is valid for 15 minutes. For request and response details, see
the [Get a file upload URL](/api-reference/platform/endpoints/customers/get-upload-url)
and [Confirm uploaded files](/api-reference/platform/endpoints/customers/submit-files)
APIs.
## Poll for the outcome
Submitting the last outstanding requirement starts verification automatically, and it runs asynchronously. Poll `GET /customers/{id}` while `kyc.status` is `verifying`. Once it changes, verification has finished: a terminal value (`active` or `unavailable`) is a final outcome, and `collecting` means MoonPay needs more information from the customer. Submit the newly outstanding requirements with `PATCH /customers/{id}/kyc` to restart verification.
```ts Poll for the outcome theme={null}
async function pollCustomerKyc(customerId: string) {
while (true) {
const res = await fetch(
`https://api.moonpay.com/platform/v1/customers/${customerId}`,
{ headers: { "X-Api-Key": "sk_test_123" } },
);
const { data: customer } = await res.json();
if (customer.kyc.status !== "verifying") {
return customer;
}
await new Promise((r) => setTimeout(r, 3000));
}
}
```
## Handle the hosted challenge
When MoonPay can't complete verification from the submitted data alone, `kyc.challenge` is present on the `PATCH /customers/{id}/kyc` response (`{ url, expiresAt }`). Render this URL for the customer to finish verification; do not resubmit requirements. The challenge can also appear on a later `GET /customers/{id}` while it's still outstanding, so check for it whenever you re-fetch the customer. See [Handle challenges](/platform/guides/handling-challenges) for the full flow.
Error codes include `verification_rejected` on a terminal rejection, and
`country_mismatch` when submitted data conflicts with the declared country.
## KYC status
| Value | Description |
| ------------- | ----------------------------------------------------------------------------------------------- |
| `not_created` | No KYC session exists yet for this customer with your account. |
| `collecting` | The customer has outstanding requirements to submit. |
| `verifying` | MoonPay is processing the submitted data. No action is needed from you or the customer. |
| `active` | KYC is complete. The customer is in good standing. |
| `unavailable` | KYC cannot proceed for this customer (for example, an unsupported region, or a closed account). |
## Next steps
Share a customer's verified KYC data with another system, with their
consent.
Render the Challenge frame when verification needs extra steps.
Present the terms, record acceptance, and cover the related consent and
verification steps before going live.
Compare the payment surfaces you can build once the customer is verified.
Understand the secret keys and access tokens this API accepts.
# Export customer data
Source: https://dev.moonpay.com/platform/guides/export-customer-data
Capture a customer's consent and export their verified KYC data to another system so they don't have to verify again.
Export a customer's verified KYC data to another system, with their consent, so they don't have to verify again elsewhere.
## Prerequisites
* Customer export enabled on your partner account. Contact your MoonPay account team.
* A customer with an approved KYC record with MoonPay.
* A server that can call the export endpoint with your secret key.
* The customer's consent, captured by rendering a MoonPay-hosted consent frame on their device. See [Capture consent](#capture-consent).
## Capture consent
Render the consent frame with `client.setupCustomerExport()`, the same way
you'd render the [Auth frame](/platform/sdk-reference/web/setup-auth). The
customer reviews what's being shared and authorizes the export inside the
MoonPay-hosted frame.
```ts Capture consent theme={null}
import {
createClient,
type CustomerExportEvent,
} from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const exportResult = await client.setupCustomerExport({
container: document.querySelector("#exportContainer"),
onEvent: (event: CustomerExportEvent) => {
switch (event.kind) {
case "ready":
// The consent UI is rendered. Reveal the container if you hide it while loading.
break;
case "complete":
// Forward event.payload.token to your backend now. It expires at event.payload.tokenExpiresAt.
console.log(event.payload.token, event.payload.tokenExpiresAt);
break;
case "error":
console.error(event.payload);
break;
}
},
});
if (!exportResult.ok) {
console.error(exportResult.error.kind, exportResult.error.message);
} else {
// Remove the frame from the DOM now that the flow has completed:
exportResult.value.dispose();
}
```
## Export the data
The frame delivers the consent token on its `complete` event. Forward it to
your backend and call the
[export endpoint](/api-reference/platform/endpoints/customers/export) within
its 5-minute validity window.
```ts Export customer data theme={null}
const res = await fetch(
"https://api.moonpay.com/platform/v1/customers/export",
{
method: "POST",
headers: {
"X-Api-Key": "sk_test_123",
Authorization: `Bearer ${consentToken}`,
},
},
);
const { data: customerData } = await res.json();
```
```json Result theme={null}
{
"data": {
"basicDetails": {
"firstName": "Jane",
"lastName": "Doe",
"dateOfBirth": "1990-01-15",
"nationality": "USA"
},
"residentialAddress": {
"country": "USA",
"administrativeArea": "NY",
"locality": "New York",
"street": "350 Fifth Avenue",
"postalCode": "10118"
},
"phoneNumber": { "number": "+14155551234" },
"taxIdentifiers": [{ "type": "ssn", "value": "123-45-6789" }],
"files": [
{
"id": "file_abc123",
"type": "passport",
"uploadedAt": "2026-06-01T12:00:00Z",
"downloadUrl": "https://files.moonpay.com/..."
}
]
}
}
```
Fields you don't hold data for are `null` or omitted. `taxIdentifiers` is included when the customer has one on file; each entry's `type` is `tin`, `ssn`, or `cpf`, with `country` present only for `tin`. `residentialAddress.subStreet` and each file's `side` (`front` or `back`) are included only when applicable, for example a two-sided ID document. Each file's `downloadUrl` is pre-signed and expires after 60 minutes; if it expires before you fetch the file, get a fresh consent token and call the export endpoint again.
The consent token is consumed on the first call, whether it succeeds or not. Notable responses: `403` if the token isn't bound to a customer, `409` if the customer has no approved KYC to export, and `429` (with a `Retry-After` header) if you've exceeded the rate limit.
## Next steps
Bring a customer to an approved KYC record before you export their data.
Understand the secret key the export endpoint accepts.
# Guest checkout
Source: https://dev.moonpay.com/platform/guides/guest-checkout
Let new customers buy crypto with Apple Pay or Google Pay without a MoonPay account.
Guest checkout lets new customers buy crypto with Apple Pay or Google Pay before
they have a MoonPay account. Customers buy first: MoonPay creates a guest
account at transaction time and requests verification only when a purchase
requires it, through a step-up challenge. This is the deferred-verification
[onboarding path](/platform/guides/onboarding-paths), the alternative to
verifying customers before their first purchase.
You supply the customer's email address and phone number when you create the
session. Both are required: without them, guest checkout is not offered. MoonPay
creates the guest account from the wallet sheet: Apple Pay billing contact, or
Google Pay billing. Returning customers are recognized automatically and connect
instead.
Guest checkout is available for customers in the United States, excluding New
York and Washington.
See [Going live](/platform/overview/going-live) for the requirements you must
meet before taking this integration to production.
## Prerequisites
* Guest checkout enabled on your partner account. Contact your MoonPay account
team.
* A server that can create session tokens with your secret key.
* A UI surface where you can render the [Apple Pay
frame](/platform/frames/apple-pay), the [Google Pay
frame](/platform/frames/google-pay), and the [challenge
frame](/platform/frames/challenge). Offer the wallet that matches the
customer's environment; you do not need both buttons on every surface.
* The customer's email address and phone number, captured in your own UI and
passed when you create the session. Guest checkout requires both. MoonPay
renders no field for either one in the payment flow.
* **Your responsibility.** Confirm the customer owns the email address and the
phone number by sending a one-time passcode before you create the session.
Re-verify the phone number at least once every 30 days. MoonPay accepts both
fields as partner-verified and does not re-verify them, so passing them is
your attestation that verification occurred. See [Verify phone numbers before
you submit
them](/platform/guides/terms-acceptance#verify-phone-numbers-before-you-submit-them).
* The timestamp at which the customer accepted MoonPay's terms (see [Record
terms acceptance](#record-terms-acceptance)).
You can test the full flow in [test mode](/platform/overview/test-mode). Test
mode uses simulated payments so no real assets are transferred. Apple Pay and
Google Pay each have a dedicated mock button: [Apple
Pay](/platform/overview/test-mode#apple-pay) and [Google
Pay](/platform/overview/test-mode#google-pay).
## How it works
1. Your server creates a session that includes the customer's email, phone
number, and terms acceptance.
2. You check the connection, which returns the customer's capabilities. When
`capabilities.guestCheckout` is present, offer Apple Pay or Google Pay on
the guest path; otherwise [connect the
customer](/platform/guides/connect-a-customer) with the standard flow.
3. You get a quote for `apple_pay` or `google_pay`. A quote that comes back
`executable: true` is ready to use.
4. A quote for more than the customer's guest limit comes back
`executable: false`, and carries a `challenge` when the customer can raise
that limit without full verification. You render the challenge, then quote
again. See [Upgrade a guest account](#upgrade-a-guest-account).
5. You render the matching payment frame ([Apple
Pay](/platform/frames/apple-pay) at `/platform/v1/apple-pay`, or [Google
Pay](/platform/frames/google-pay) at `/platform/v1/google-pay`).
6. On the customer's first purchase, MoonPay creates the guest account from
the wallet billing details and processes the payment. If extra verification
is needed, the frame emits a challenge that resolves the purchase.
## Device and browser support
Offer the wallet the customer's environment supports. When a wallet isn't
available, the frame emits `unsupported` — hide that button and offer the
other wallet or a connected-customer flow.
Apple Pay is available in Safari on macOS and in every iOS browser. The
customer also needs a card set up in Apple Pay.
In other browsers, the frame reports Apple Pay as unavailable and renders
nothing. It does not offer Apple's cross-device QR flow. The
[widget](/platform/guides/pay-with-widget) does support that flow.
In a native iOS app, embed the [Apple Pay
frame](/platform/frames/apple-pay) in a `WKWebView` and handle JavaScript
dialogs through `WKUIDelegate`. See the [frame
requirements](/platform/frames/apple-pay#wkwebview) and the [iOS manual
integration guide](/platform/guides/manual-integration/ios#apple-pay-frame).
Google Pay relies on the [Payment Request
API](https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API),
so it only works in browsers that support that API, such as Chrome. The
customer also needs a card set up with Google Pay.
In a native Android app, the Payment Request API is disabled by default in
Android WebView. Offering Google Pay requires Google Play services
25.18.30+, Android WebView for Chrome 137+, and the API enabled on your
WebView. See the [frame
requirements](/platform/frames/google-pay#android-webview) and the [Android
manual integration
guide](/platform/guides/manual-integration/android#google-pay-frame).
For connected-customer Apple Pay and Google Pay (after login), see [Pay with
Apple Pay](/platform/guides/pay-with-apple-pay) and [Pay with Google
Pay](/platform/guides/pay-with-google-pay).
## Record terms acceptance
Present MoonPay's Terms of Use and Privacy Policy in your UI using one of the
[presentation
methods](/platform/guides/terms-acceptance#choose-a-presentation-method), and
capture the timestamp when the customer accepts. Pass it as `termsAcceptedAt`
when you create the session: MoonPay records the live terms version at that
moment and binds the acceptance to the guest account. For the rendering rules
and the records you keep once customers accept, see [Terms
acceptance](/platform/guides/terms-acceptance).
`termsAcceptedAt` must be no more than 60 seconds ahead of server time. Capture
it the moment the customer taps your accept control, then create the session
immediately.
## Create a session
Create the session on your server with your secret key. For guest checkout,
include the customer's `email`, `phoneNumber`, and `termsAcceptedAt` alongside
the standard fields.
The sessions endpoint accepts all three as optional fields, because the other
[onboarding paths](/platform/guides/onboarding-paths) do not need them. Guest
checkout does. A session created without `email` or `phoneNumber` still returns
`200`, and `capabilities.guestCheckout` is absent from the connection in the
next step.
```ts Create session token theme={null}
const url = "https://api.moonpay.com/platform/v1/sessions";
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
"X-Api-Key": "sk_test_123",
},
method: "POST",
body: JSON.stringify({
externalCustomerId: "your_user_id",
deviceIp: "...ip address from client",
email: "customer@example.com",
phoneNumber: "+14155551234", // E.164 format
termsAcceptedAt: "2026-01-12T14:44:30Z", // ISO 8601, within 60s ahead of server time
}),
});
console.log(await res.json());
```
```json Result theme={null}
{
"sessionToken": "c3N0XzAwMQ=="
}
```
See the [sessions API
reference](/api-reference/platform/endpoints/sessions/create) for all fields
and error responses.
Passing `termsAcceptedAt` requires the Identity or Guest Checkout account
capability. Without one of those two, the session fails with `503` and the
code `service_unavailable`. The message reads "This service is temporarily
unavailable". The cause is not temporary: the capability is not enabled on
your account. Contact your MoonPay account team instead of retrying.
If the customer's region is not supported, the session still succeeds. The
`guestCheckout` capability is absent in the next step, and you connect the
customer with the standard flow instead.
## Check for guest checkout
Pass the `sessionToken` to the client and check the connection. The check runs
in an invisible frame and returns the customer's capabilities. When
`capabilities.guestCheckout` is present, offer Apple Pay or Google Pay on the
guest path. You can also check the session [manually](/platform/frames/check).
```ts Check for guest checkout theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({
sessionToken: "c3N0XzAwMQ==", // The session token from your server
});
const connectionResult = await client.getConnection();
if (!connectionResult.ok) {
// Handle error
}
const connection = connectionResult.value;
if (connection.capabilities?.guestCheckout) {
// Offer Apple Pay and/or Google Pay on the guest path (continue below)
} else {
// Connect the customer with the standard flow.
// See /platform/guides/connect-a-customer
}
```
```ts Result (guest checkout available) theme={null}
{
status: "connectionRequired",
// Encrypted credentials — the SDK decrypts and stores them internally
credentials: "ZW5jXzAwMQ==",
capabilities: {
guestCheckout: {
requirements: {}
}
}
}
```
A returning customer whose email and phone match an existing MoonPay account
is recognized at this step. Connect them with the [standard
flow](/platform/guides/connect-a-customer): low-friction authentication
prompts for a one-time passcode instead of a full login.
If `mismatch` is `true` on a `connectionRequired` result, the session's email
and phone number resolve to different MoonPay customers. Route the customer
through the [connect flow](/platform/guides/connect-a-customer) before rendering
Apple Pay or Google Pay.
### When `guestCheckout` is absent
In every condition below, the session and the connection check both succeed.
Nothing errors, so there is no error message to inspect. Check these in order:
| Condition | What to do |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| `email` missing from the session | Pass the customer's email address when you create the session. |
| `phoneNumber` missing from the session | Pass the phone number in E.164 format, for example `+14155551234`. |
| Guest checkout not enabled on your account | Contact your MoonPay account team. |
| Customer outside the supported region | Check the `deviceIp` you passed, as described below. |
| No terms acceptance on record | Pass `termsAcceptedAt`. A returning customer who already accepted the current terms does not need it. |
Region eligibility comes from the `deviceIp` you pass when you create the
session. Pass the customer's real client IP, not your server's. An IP that
MoonPay cannot resolve to a supported state counts as unsupported, so
`guestCheckout` is absent. It is also absent for a phone number from outside the
United States, even when the IP resolves to a supported state.
Gate the wallet buttons on `capabilities.guestCheckout` being present. Rendering
them on the guest path without it fails the transaction.
## Get a quote
Request an executable quote for the wallet you will render. Only quotes with
`executable: true` can be used to execute a transaction.
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" }, // The fiat currency and amount to pay
destination: { asset: { code: "ETH" } }, // The crypto the customer will receive
wallet: { address: "0x1234..." }, // The destination wallet address
paymentMethod: { type: "apple_pay" },
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value.signature);
```
```ts Result theme={null}
{
// ...quote details
expiresAt: "2026-01-12T14:45:00Z",
executable: true,
signature: "eyJhbGciOiJFUzI1NiIs..."
}
```
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" }, // The fiat currency and amount to pay
destination: { asset: { code: "ETH" } }, // The crypto the customer will receive
wallet: { address: "0x1234..." }, // The destination wallet address
paymentMethod: { type: "google_pay" },
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value.signature);
```
```ts Result theme={null}
{
// ...quote details
expiresAt: "2026-01-12T14:45:00Z",
executable: true,
signature: "eyJhbGciOiJFUzI1NiIs..."
}
```
A quote for more than the customer's guest limit comes back
`executable: false`. When the customer can raise that limit by verifying a
little more about themselves, the quote also carries a `challenge`. Do not
render the payment frame with that quote. Resolve the challenge first, then
quote again. See [Upgrade a guest account](#upgrade-a-guest-account).
## Render the payment frame
Render the Apple Pay or Google Pay frame with the quote signature. When the
customer taps the button and authorizes, MoonPay creates the guest account from
the wallet name and billing address, then processes the payment.
| Wallet | SDK method | Frame URL | Reference |
| ---------- | ---------------- | ------------------------- | ----------------------------------------------- |
| Apple Pay | `setupApplePay` | `/platform/v1/apple-pay` | [Apple Pay frame](/platform/frames/apple-pay) |
| Google Pay | `setupGooglePay` | `/platform/v1/google-pay` | [Google Pay frame](/platform/frames/google-pay) |
`/platform/v1/google-pay` is the primary Google Pay URL for this flow. Do not
use the [buy button](/platform/guides/pay-with-buy-button) for guest checkout:
that path expects a connected customer.
```ts Apple Pay theme={null}
import type { ApplePayEvent } from "@moonpay/platform-sdk-web";
const applePayResult = await client.setupApplePay({
quote: quoteResult.value.signature, // The quote signature from getQuote
container: document.querySelector("#applePayContainer"), // DOM element to render the button
onEvent: (event: ApplePayEvent) => {
switch (event.kind) {
case "ready":
// The frame is ready. Reveal the button if needed.
break;
case "complete": {
const txn = event.payload.transaction;
if (txn.status === "failed") {
if (txn.failureCode === "transactionNotAllowed") {
// Amount is above the customer's limit. Prompt them to try a smaller amount.
break;
}
// Show txn.failureReason.
break;
}
// The transaction is executing. Track the final status via polling or webhooks.
console.log(txn); // { id: "txn_01", status: "pending" }
break;
}
case "challenge":
// Verification required — render the challenge frame at the provided URL.
// See "Handle verification" below.
console.log(event.payload.url);
break;
case "quoteExpired":
// Fetch a new quote, then pass its signature into the frame:
// event.payload.setQuote(newQuote.value.signature);
break;
case "error":
console.error(event.payload.message);
break;
case "unsupported":
// Apple Pay isn't supported in the current environment.
break;
}
},
});
if (!applePayResult.ok) {
// Handle error setting up Apple Pay
}
```
```ts Google Pay theme={null}
import type { GooglePayEvent } from "@moonpay/platform-sdk-web";
const googlePayResult = await client.setupGooglePay({
quote: quoteResult.value.signature, // The quote signature from getQuote
container: document.querySelector("#googlePayContainer"), // DOM element to render the button
onEvent: (event: GooglePayEvent) => {
switch (event.kind) {
case "ready":
// The frame is ready. Reveal the button if needed.
break;
case "complete": {
const txn = event.payload.transaction;
if (txn.status === "failed") {
if (txn.failureCode === "transactionNotAllowed") {
// Amount is above the customer's limit. Prompt them to try a smaller amount.
break;
}
// Show txn.failureReason.
break;
}
// The transaction is executing. Track the final status via polling or webhooks.
console.log(txn); // { id: "txn_01", status: "pending" }
break;
}
case "challenge":
// Verification required — render the challenge frame at the provided URL.
// See "Handle verification" below.
console.log(event.payload.url);
break;
case "quoteExpired":
// Fetch a new quote, then pass its signature into the frame:
// event.payload.setQuote(newQuote.value.signature);
break;
case "error":
console.error(event.payload.message);
break;
case "unsupported":
// Google Pay isn't supported in the current environment.
break;
}
},
});
if (!googlePayResult.ok) {
// Handle error setting up Google Pay
}
```
## Handle verification
This section covers the challenge the payment frame emits after the customer
authorizes. A quote can carry a challenge of its own, before you render a
payment frame at all. That one is a limit upgrade, covered in [Upgrade a guest
account](#upgrade-a-guest-account).
Some guest purchases need the customer to complete an extra step before the
payment goes through. Second-factor authentication (for example, confirming
identity when email and phone match an existing account, or authenticating when
the purchase is larger than the guest limit allows) and KYC step-up (providing
more identity details) both surface as the same `challenge` event. You render
the [challenge frame](/platform/frames/challenge) the same way in every case.
See [Handle challenges](/platform/guides/handling-challenges) for the full
flow.
The payment frame emits `challenge` with a URL. Render the challenge frame at
that URL. The challenge frame guides the customer through the required steps
and completes the purchase itself, then emits `complete`. You do not re-render
the Apple Pay or Google Pay button.
An amount above the customer's maximum limit that verification cannot raise is
terminal. The frame emits `complete` with `status: "failed"` and
`failureCode: "transactionNotAllowed"`. Prompt the customer to try a smaller
amount.
## Upgrade a guest account
A guest account is a real MoonPay account with lower limits. There are two ways
to raise them. A step-up lifts the guest limit in place and keeps the customer
on the guest path. Full verification lifts the limits further and moves the
customer off the guest path entirely.
### Raise the limit with a step-up
When a customer quotes for more than their guest limit allows, MoonPay can
offer to raise that limit in exchange for their date of birth and the last four
digits of their Social Security number. The customer stays a guest and never
completes full verification.
MoonPay decides who qualifies. Eligibility depends on the amount the customer
is quoting for, on their history with MoonPay, and on the capability being
enabled for your account. The rules shift with MoonPay's compliance decisions,
so do not model them yourself: the quote tells you.
The step-up is enabled per partner, and separately from guest checkout itself.
To request it, contact your MoonPay account team at
[team@moonpay.com](mailto:team@moonpay.com). Without it, quotes never carry a
`challenge`, and an over-limit purchase fails with `failureCode:
"transactionNotAllowed"`.
The step-up needs `@moonpay/platform-sdk-web` or
`@moonpay/platform-sdk-react-native` 1.15.2 or later. Earlier versions omit the
client token from the challenge frame URL and the frame fails to load.
**Detect the limit on the quote.** An eligible over-limit quote returns
`executable: false` and a `challenge` object. Branch on `challenge` being
present rather than comparing the amount against a limit yourself.
**Render the challenge frame.** Pass `challenge.url` to `setupChallenge()`
unchanged. This is the same [challenge frame](/platform/frames/challenge) the
card and identity flows use, running its `guest_checkout_limit_upgrade` flow.
MoonPay collects and validates the date of birth and the SSN digits inside the
frame, so neither value passes through your code.
**Act on the result.** The frame emits `complete` with a `status`:
| `status` | What it means | What you do |
| ---------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `upgraded` | The limit is raised. | Quote again. The new quote returns `executable: true`. Render the payment frame as normal. |
| `rejected` | Verification failed. The limit is unchanged, and running the step-up again does not change it. | Offer [full verification](#complete-full-verification) instead. |
**Tear the frame down.** Call `dispose()` on `complete`, `cancelled`, and
`error`. On `rejected` the frame keeps its own message on screen, so removing
it is your call.
```ts Raise the guest limit theme={null}
import type { ChallengeEvent } from "@moonpay/platform-sdk-web";
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "500.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234..." },
paymentMethod: { type: "apple_pay" },
});
if (!quoteResult.ok) {
// Handle error
}
const quote = quoteResult.value;
if (!quote.challenge) {
// Nothing to upgrade. Render the payment frame with quote.signature and
// skip the rest of this block.
}
const challengeResult = await client.setupChallenge({
url: quote.challenge.url, // Comes from the quote. Pass it through as-is.
container: document.querySelector("#challengeContainer"),
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "ready":
// The upgrade UI is visible. Reveal your modal if it starts hidden.
break;
case "complete": {
if (event.payload.flow !== "guest_checkout_limit_upgrade") break;
if (event.payload.status === "upgraded") {
// Limit raised. Quote again to get an executable quote.
} else {
// "rejected". The step-up cannot lift this limit any further.
// Offer full verification instead.
}
challengeResult.value.dispose();
break;
}
case "cancelled":
// The customer dismissed the frame. The limit is unchanged.
challengeResult.value.dispose();
break;
case "error":
console.error(event.payload.code, event.payload.message);
challengeResult.value.dispose();
break;
}
},
});
if (!challengeResult.ok) {
// Handle error setting up the challenge frame
}
```
```ts Quote result (upgrade offered) theme={null}
{
// ...quote details
expiresAt: "2026-01-12T14:45:00Z",
executable: false,
signature: "eyJhbGciOiJFUzI1NiIs...",
challenge: {
kind: "frame",
url: "https://platform.moonpay.com/v2/challenge?challengeToken=eyJhbGciOiJFUzI1NiIs..."
}
}
```
On `cancelled`, nothing was submitted. Offer a retry: render the frame again
at the same URL, or quote again for a fresh `challenge.url` if the old one has
expired.
### Complete full verification
To lift the limits beyond what a step-up reaches, connect the customer and have
them complete full identity verification. The limits lift on the same account,
with no migration. Use the [connect
flow](/platform/guides/connect-a-customer), then guide the customer through
verification. You can prompt this after a completed guest purchase, when a
purchase exceeds the guest limit, or when a step-up comes back `rejected`.
## Transaction statuses
Transactions have the following statuses:
* **Pending:** The transaction has been initiated and the payment accepted. The
assets are being transferred.
* **Complete:** The transaction is finalized. The payment is complete and the
assets have been delivered to their destination.
* **Failed:** The transaction has failed. The payment was not executed and
funds were not transferred.
## Next steps
Connected-customer Apple Pay after login.
Connected-customer Google Pay after login.
Frame URL, size, permissions, and events.
Frame URL, size, permissions, and events.
Render the challenge frame when a quote or a purchase needs extra
verification.
Frame URL, events, and the `guest_checkout_limit_upgrade` payloads.
Present terms and record `termsAcceptedAt`.
# Handle challenges
Source: https://dev.moonpay.com/platform/guides/handling-challenges
Detect and respond to challenges.
This guide shows you how to detect and handle challenges that require customer
authentication or verification before MoonPay can continue an action.
## Prerequisites
* A customer in a payment or onboarding flow: connected through the
[connect flow](/platform/guides/connect-a-customer), logged in through the
[Auth frame](/platform/frames/auth) on the
[Customer API](/platform/guides/customer-api) path, or on the
[guest checkout](/platform/guides/guest-checkout) path.
* A UI surface where you can render frames (WebView on mobile, iframe on web).
## When challenges appear
Challenges are extra steps a customer must complete before MoonPay can continue
an action. Common reasons include:
* Strong Customer Authentication (SCA), for example 3D Secure
* Identity verification (KYC)
* Card-specific checks such as CVC re-entry or micro-authorization
Frames, the Customer API, and buy quotes all surface challenges. Frames emit a
`challenge` event that points to the
[Challenge frame](/platform/frames/challenge). The Customer API returns a
`kyc.challenge` URL when verification needs a hosted step. A buy quote returns
a `challenge` object when the customer must clear a step before the quote
becomes executable.
## Where challenges are emitted
Each emitting surface hands you a fully-formed URL that you load into the
dedicated [Challenge frame](/platform/frames/challenge):
* The [Apple Pay frame](/platform/frames/apple-pay) emits `challenge` for
Apple Pay transactions, including
[guest checkout](/platform/guides/guest-checkout).
* The [Google Pay frame](/platform/frames/google-pay) emits `challenge` for
Google Pay transactions, including
[guest checkout](/platform/guides/guest-checkout).
* The [buy frame](/platform/frames/buy) emits `challenge` for card
transactions (see [Pay with card](/platform/guides/pay-with-card)).
* The Customer API returns `kyc.challenge` (`{ url, expiresAt }`) on
`PATCH /customers/{id}/kyc` when verification needs a hosted step, and on
subsequent `GET /customers/{id}` responses while the challenge is
outstanding. Render the `url` in the Challenge frame the same way you render
a frame-emitted challenge URL. See
[Handle the hosted challenge](/platform/guides/customer-api#handle-the-hosted-challenge).
* The buy quote returns `challenge` (`{ kind, url }`) alongside
`executable: false` when a
[guest checkout](/platform/guides/guest-checkout) customer can raise their
spending limit. This is the only surface that raises a challenge before a
payment frame exists, so you resolve it and request the quote again rather
than resuming a transaction. See
[Upgrade a guest account](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up).
On guest checkout, second-factor authentication and KYC step-up both arrive as
the same `challenge` event for Apple Pay and Google Pay. Render the Challenge
frame the same way.
The `challenge` event uses the same envelope across all of the frames. The
quote returns the same `kind` and `url` fields, without the event wrapper:
```json Example challenge event theme={null}
{
"version": 2,
"meta": { "channelId": "ch_1" },
"kind": "challenge",
"payload": {
"kind": "frame",
"url": "https://platform.moonpay.com/platform/v1/challenge?challengeToken=..."
}
}
```
## How to handle a challenge
1. **Listen for the `challenge` event** on the frame. Treat the action as
blocked until the challenge resolves.
2. **Mount the Challenge frame** at the URL from the event payload. Pass the
URL through as-is — do not construct or modify it. See the
[Challenge frame](/platform/frames/challenge) reference for the frame URL,
parameters, and events.
3. **Handle the Challenge frame events**:
* `ready` — the challenge UI is rendered and visible.
* `complete` — verification resolved. The Challenge frame reports any
downstream artifacts (for example, the transaction `id` and `status` for
the buy flow).
* `cancelled` — the customer dismissed the challenge. Offer a retry path.
* `error` — the challenge failed. Log the `code` and `message` (both are
developer-facing and not intended for end-user UI) and show the customer
a generic next step, such as retrying or choosing a different payment
method.
4. **Tear down the originating frame** after `complete`, `cancelled`, or
`error`. For the buy frame, call `buyResult.value.dispose()`. A
quote-emitted challenge has no originating frame, so dispose the Challenge
frame itself.
## Implementation tips
* **Use a full-screen surface on mobile**: challenge flows often involve
authentication or verification, so treat them like a separate screen or full
sheet.
* **Validate `postMessage` events**: if you integrate frames manually, validate
origin and message shape. The [frames protocol](/platform/frames/overview)
documents the shared envelope format.
* **Handle cancellation and timeouts**: if the customer closes the challenge or
it fails, show a clear next step (retry, choose a different payment method,
or exit the flow).
# KYC data requirements
Source: https://dev.moonpay.com/platform/guides/kyc-data-requirements
Per-country field and document requirements for verifying customers with the Customer API.
The [Customer API](/platform/guides/customer-api) puts KYC data capture in your
hands. You are responsible for capturing the required customer data and
submitting it through the API, whether you build the capture flow yourself or
delegate it to a KYC provider. Use this page as your pre-integration reference.
For when each requirement category becomes required in a customer's journey,
see [Verification tiers](/platform/guides/verification-tiers).
Each section covers one of the requirement categories that appear in
`kyc.requirements` on
[`GET /platform/v1/customers/{id}`](/platform/guides/customer-api#get-a-customer):
`basicDetails`, `residentialAddress`, and `phoneNumber` (the profile fields),
plus `taxIdentifiers`, `questionnaires`, `identityDocuments`, `selfie`, and
`proofOfAddress`. You fulfill field-based categories with
[`PATCH /platform/v1/customers/{id}/kyc`](/platform/guides/customer-api#submit-kyc-data)
and document-based categories with the
[file-upload endpoints](/platform/guides/customer-api#upload-a-file).
Country is the key driver: once you submit `residentialAddress.country`, the
API returns the full requirement set for that jurisdiction.
## Profile fields
Every customer requires the following profile fields, submitted with
[`PATCH /platform/v1/customers/{id}/kyc`](/api-reference/platform/endpoints/customers/submit-kyc).
The API path is the field's location in the PATCH body.
| Field | API path | US | EEA | Rest of world |
| -------------------- | --------------------------------------- | -------- | -------- | ------------------- |
| First name | `basicDetails.firstName` | Required | Required | Required |
| Last name | `basicDetails.lastName` | Required | Required | Required |
| Date of birth | `basicDetails.dateOfBirth` | Required | Required | Required |
| Nationality | `basicDetails.nationality` | Required | Required | Required |
| Street address | `residentialAddress.street` | Required | Required | Required |
| Apartment / unit | `residentialAddress.subStreet` | Optional | Optional | Optional |
| City | `residentialAddress.locality` | Required | Required | Required |
| State or province | `residentialAddress.administrativeArea` | Required | Optional | Required for Canada |
| Postal code | `residentialAddress.postalCode` | Required | Required | Required |
| Country of residence | `residentialAddress.country` | Required | Required | Required |
| Phone number | `phoneNumber.number` | Required | Required | Required |
Date of birth must be in `YYYY-MM-DD` format. The customer must be 18 or older.
Country codes must be ISO 3166-1 alpha-3 (e.g. `USA`, `GBR`, `DEU`).
Phone number must be in E.164 format (e.g. `+12025550143`). Verify the
customer's phone number via OTP before submitting it. Re-verify at least once
every 30 days. Submitting the number is your attestation that this
verification occurred — see [Terms
acceptance](/platform/guides/terms-acceptance#verify-phone-numbers-before-you-submit-them).
## Tax identifiers
Tax identifier requirements depend on the customer's country of residence.
Submit tax identifiers as `taxIdentifiers[]` on
[`PATCH /platform/v1/customers/{id}/kyc`](/api-reference/platform/endpoints/customers/submit-kyc),
each with a `type`, `value`, and (for `tin`) a `country` field.
| Region | Type | Example format |
| ------------------------------------- | ----- | ------------------------------------------------------------------------------------ |
| United States | `ssn` | `123-45-6789` |
| Brazil | `cpf` | 11 digits |
| EEA, UK, and selected other countries | `tin` | Varies by country. See [EEA and UK](#eea-and-uk) and [Rest of world](#rest-of-world) |
### EEA and UK
| Country | Format | Present on ID documents | Where to find it (if not on documents) |
| -------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Austria | 9 digits (`99-999/9999`) | No | Tax assessments, upper right of first page |
| Belgium | 11 digits | ID card (Numéro National) | |
| Bulgaria | 10 digits | Passport, ID card, driving licence (Unified Civil Number) | Foreigners: Certificate of Fiscal Residence |
| Croatia | 11 digits | ID card, driving licence, biometric passport (issued after 2009–2013) | Health card or Certificate of Personal ID Number |
| Cyprus | 8 digits + 1 letter | No | Registration letter or Tax Clearance Certificate |
| Czech Republic | 9–10 digits (`999999/999` or `999999/9999`) | ID card, passport, driving licence (Personal Number) | |
| Denmark | 10 digits (`999999-9999`) (CPR number) | Passport, driving licence | Health Insurance Card (front cover) |
| Estonia | 11 digits (Isikukood) | Passport, ID card, driving licence | |
| Finland | 11 characters (`DDMMYY` + sign + 3 digits + check character) | ID card, passport, driving licence | |
| France | 13 digits | No | Pre-printed income tax declaration form (Numéro fiscal) or property tax notices |
| Germany | 11 digits | No | IdNr. Allocation Letter from the Federal Central Tax Office, or tax assessments |
| Greece | 9 digits | No | Tax Completeness Status or Ministry of Finance certification |
| Hungary | 10 digits | ID card chip only (issued after 1 January 2016) | Tax Card (Adóigazolvány) |
| Iceland | 10 digits (Kennitala) | Passport, ID card, driving licence | |
| Ireland | 7 digits + 1–2 letters (PPS No) | No | Tax Return (Form 12) |
| Italy | 16 characters (alphanumeric, Codice Fiscale) | No | Health Card (Tessera Sanitaria) or TIN card |
| Latvia | 11 digits (PIC) | Passport, ID card, driving licence | |
| Lithuania | 11 digits | Passport, ID card, driving licence | |
| Liechtenstein | Up to 12 digits (PEID number) | Residence permit only (rollout in progress) | Recent residence permits for foreigners and cross-border workers |
| Luxembourg | 13 digits | No | Social Security Identification Card |
| Malta | 8 characters (nationals) / 9 digits (non-nationals) | ID card and passport (nationals only) | Non-nationals: Inland Revenue Department |
| Netherlands | 9 digits | Passport, ID card, driving licence | |
| Norway | 11 digits; Fødselsnummer for residents, D-number for temporary residents | Passport, national ID card, driving licence | Correspondence from the Tax Administration (Skatteetaten) |
| Poland | 11 digits (PESEL) for individuals, 10 digits (NIP) for businesses | Passport, ID card (PESEL) | NIP: National Court Register |
| Portugal | 9 digits | Citizen card | |
| Romania | 13 digits (CNP) | Passport, ID card, driving licence, residence permit | |
| Slovakia | 10 digits (Personal Number, used as TIN-equivalent) | ID card, passport | Official TIN: tax documents only |
| Slovenia | 8 digits | No | Certificate of Entry into Tax Register or Certificate of Residence |
| Spain | DNI: `99999999L` / NIE: `X/Y/Z9999999L` | ID card, driving licence, foreigners' residence card | Tax Identification Card |
| Sweden | 10 digits (`999999-9999`) | Passport, ID card, driving licence | |
| United Kingdom | 2 letters + 6 digits + 1 letter (National Insurance number) | No | Payslip, P60, or National Insurance card |
### Rest of world
| Country | Format | Present on ID documents | Where to find it |
| ------------ | ----------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------- |
| Brazil | 11 digits (CPF) | No | |
| Japan | 12 digits (Individual Number / My Number) | No | Notification card from municipal office |
| Jersey | 10 digits (`999-999-9999`) | No | Official correspondence from Revenue Jersey (tax assessments, return forms, statements of account) |
| San Marino | 9 digits | ID card (front, next to photo) | Carta Azzurra (San Marino health card) |
| South Africa | 10 digits | No | SARS correspondence (Form IT 12) |
## Due diligence questionnaires
When `questionnaires` is `incomplete` in `kyc.requirements`, the customer must
complete one or both due-diligence questionnaires. The entry's `requiredFields`
names which ones: `customerDueDiligence`, `enhancedDueDiligence`, or both.
Submit answers as `questionnaires[]` on
`PATCH /platform/v1/customers/{id}/kyc`, one entry per required questionnaire.
Each entry carries a `type` identifying the questionnaire and an `answers`
object; each questionnaire type may appear at most once per request.
### Customer Due Diligence
Answers for the `customerDueDiligence` questionnaire.
| Field | Type | Required | Description |
| ----------------------------------- | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `employmentStatus` | `string` | Yes | The customer's employment status: `employedOrSelfEmployed`, `retired`, `student`, or `unemployed`. |
| `accountPurpose` | `string` | Yes | The primary purpose for the account: `investing`, `payments`, `purchasingDigitalAssets`, `purchasingGoodsOrServices`, or `trading`. |
| `grossAnnualIncome` | `object` | Yes | The customer's gross annual income, as a [monetary amount](#monetary-amounts). |
| `sourceOfWealth` | `string` | Yes | The primary source of wealth: `salary`, `savings`, `investments`, `cryptoTrading`, or `other`. |
| `transactionFrequencyPerMonth` | `integer` | Yes | Expected number of transactions per month; a strictly positive integer. |
| `expectedTransactionAmountPerMonth` | `object` | Yes | Expected total transaction amount per month, as a [monetary amount](#monetary-amounts). |
### Enhanced Due Diligence
Answers for the `enhancedDueDiligence` questionnaire.
| Field | Type | Required | Description |
| ------------ | -------- | -------- | -------------------------------------------------------------------- |
| `profession` | `string` | Yes | The customer's profession. One of the values below. |
| `netWorth` | `object` | Yes | The customer's net worth, as a [monetary amount](#monetary-amounts). |
`profession` accepts: `agriculture`, `artsAndEntertainment`, `businessOwner`,
`education`, `financialServices`, `healthcare`,
`industrialTradesAndTransport`, `informationAndTechnology`,
`legalAndProfessionalServices`, `publicSector`, `realEstate`, `retail`, and
`seniorManagement`.
### Monetary amounts
`grossAnnualIncome`, `expectedTransactionAmountPerMonth`, and `netWorth` share
one shape: an object with a `currency` code and an `amount`.
| Field | Type | Required | Description |
| ---------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `currency` | `string` | Yes | The fiat currency, as an ISO 4217 code: `AUD`, `EUR`, `GBP`, or `USD`. Must be the reporting currency for `residentialAddress.country`. |
| `amount` | `string` | Yes | The amount as a decimal string, strictly greater than zero, with at most 2 decimal places (for example `"55000"` or `"1234.56"`). |
The reporting currency is fixed by the customer's `residentialAddress.country`:
| Residential address country | Reporting currency |
| ------------------------------------------ | ------------------ |
| GBR | GBP |
| AUS, NZL | AUD |
| EEA (European Economic Area) countries | EUR |
| All other countries, including USA and CAN | USD |
The expected value is fixed by the residential address country, but `currency`
is still required: it acts as an explicit confirmation so amounts are never
silently misinterpreted. The API rejects any other currency with a 400
validation error that identifies the expected currency, for example
`Expected currency EUR for residentialAddress.country CZE, got PLN`.
If the customer's local currency differs from the reporting currency, for
example CZK or PLN in an EEA country, convert the amount to the reporting
currency before submitting, using a current exchange rate.
## Identity documents
When `identityDocuments` is `incomplete` in `kyc.requirements`, the customer
must provide one of the document types listed below. Submit documents through
the [presigned upload flow](/platform/guides/customer-api#upload-a-file), not
inline in the PATCH body. Before you submit selfie or identity-document
images, display the required biometric consent disclosure to the customer —
see [Biometric consent for selfie and document images](/platform/guides/terms-acceptance#biometric-consent-for-selfie-and-document-images).
| Document type | API value | Sides required |
| ---------------------- | ---------------------- | -------------- |
| Passport | `passport` | Front only |
| Driving licence | `drivingLicence` | Front and back |
| National identity card | `nationalIdentityCard` | Front and back |
| Residence permit | `residencePermit` | Front and back |
Accepted document types vary by country.
### Acceptance by country
The table below shows which documents MoonPay accepts per country.
| Country | Passport | Driving licence | National ID card | Residence permit |
| ------------------------------ | -------- | --------------- | ---------------- | ---------------- |
| Afghanistan | ✓ | — | ✓ | — |
| Albania | ✓ | ✓ | ✓ | — |
| Algeria | ✓ | — | ✓ | — |
| American Samoa | ✓ | ✓ | ✓ | — |
| Andorra | ✓ | ✓ | — | — |
| Angola | ✓ | ✓ | ✓ | — |
| Anguilla | ✓ | — | — | — |
| Antigua and Barbuda | ✓ | — | — | — |
| Argentina | ✓ | ✓ | ✓ | — |
| Armenia | ✓ | ✓ | ✓ | — |
| Australia | ✓ | ✓ | ✓ | ✓ |
| Austria | ✓ | ✓ | ✓ | ✓ |
| Azerbaijan | ✓ | ✓ | ✓ | ✓ |
| Bahamas | ✓ | ✓ | — | — |
| Bahrain | ✓ | ✓ | ✓ | — |
| Bangladesh | ✓ | ✓ | ✓ | — |
| Barbados | ✓ | ✓ | ✓ | — |
| Belarus | ✓ | ✓ | ✓ | ✓ |
| Belgium | ✓ | ✓ | ✓ | ✓ |
| Belize | ✓ | — | — | — |
| Benin | ✓ | ✓ | ✓ | — |
| Bermuda | ✓ | ✓ | — | — |
| Bhutan | ✓ | ✓ | — | — |
| Bolivia | ✓ | ✓ | ✓ | ✓ |
| Bosnia and Herzegovina | ✓ | ✓ | ✓ | — |
| Botswana | ✓ | ✓ | ✓ | — |
| Brazil | ✓ | ✓ | ✓ | ✓ |
| Brunei Darussalam | ✓ | ✓ | ✓ | ✓ |
| Bulgaria | ✓ | ✓ | ✓ | ✓ |
| Burkina Faso | ✓ | — | ✓ | — |
| Burundi | ✓ | — | — | — |
| Cabo Verde | ✓ | — | — | — |
| Cambodia | ✓ | ✓ | ✓ | — |
| Cameroon | ✓ | ✓ | ✓ | — |
| Canada | ✓ | ✓ | ✓ | — |
| Cayman Islands | ✓ | ✓ | — | — |
| Central African Republic | ✓ | — | — | — |
| Chad | ✓ | — | — | — |
| Chile | ✓ | ✓ | ✓ | — |
| China | ✓ | ✓ | ✓ | — |
| Colombia | ✓ | ✓ | ✓ | ✓ |
| Comoros | ✓ | — | — | — |
| Congo | ✓ | ✓ | ✓ | — |
| Congo (Democratic Republic) | ✓ | — | — | — |
| Costa Rica | ✓ | ✓ | ✓ | ✓ |
| Croatia | ✓ | ✓ | ✓ | ✓ |
| Curaçao | ✓ | ✓ | ✓ | — |
| Cyprus | ✓ | ✓ | ✓ | ✓ |
| Czechia | ✓ | ✓ | ✓ | ✓ |
| Denmark | ✓ | ✓ | — | ✓ |
| Djibouti | ✓ | — | — | — |
| Dominica | ✓ | ✓ | ✓ | — |
| Dominican Republic | ✓ | ✓ | ✓ | — |
| Ecuador | ✓ | ✓ | ✓ | — |
| Egypt | ✓ | ✓ | ✓ | ✓ |
| El Salvador | ✓ | ✓ | ✓ | — |
| Equatorial Guinea | ✓ | — | ✓ | — |
| Eritrea | ✓ | — | — | — |
| Eswatini | ✓ | — | — | — |
| Estonia | ✓ | ✓ | ✓ | ✓ |
| Ethiopia | ✓ | ✓ | ✓ | ✓ |
| Faroe Islands | ✓ | ✓ | — | — |
| Fiji | ✓ | ✓ | — | — |
| Finland | ✓ | ✓ | ✓ | ✓ |
| France | ✓ | ✓ | ✓ | ✓ |
| Gabon | ✓ | — | — | — |
| Gambia | ✓ | — | ✓ | — |
| Georgia | ✓ | ✓ | ✓ | — |
| Germany | ✓ | ✓ | ✓ | ✓ |
| Ghana | ✓ | ✓ | ✓ | — |
| Gibraltar | ✓ | ✓ | ✓ | — |
| Greece | ✓ | ✓ | ✓ | ✓ |
| Greenland | ✓ | — | — | — |
| Grenada | ✓ | ✓ | ✓ | — |
| Guam | ✓ | ✓ | ✓ | — |
| Guatemala | ✓ | ✓ | ✓ | ✓ |
| Guernsey | ✓ | ✓ | ✓ | — |
| Guinea | ✓ | — | — | — |
| Guinea-Bissau | ✓ | — | — | — |
| Guyana | ✓ | ✓ | ✓ | — |
| Haiti | ✓ | ✓ | ✓ | — |
| Honduras | ✓ | ✓ | ✓ | — |
| Hong Kong | ✓ | ✓ | ✓ | — |
| Hungary | ✓ | ✓ | ✓ | ✓ |
| Iceland | ✓ | ✓ | ✓ | ✓ |
| India | ✓ | ✓ | ✓ | — |
| Indonesia | ✓ | ✓ | ✓ | — |
| Iraq | ✓ | — | ✓ | — |
| Ireland | ✓ | ✓ | ✓ | ✓ |
| Isle of Man | ✓ | ✓ | — | — |
| Israel | ✓ | ✓ | ✓ | — |
| Italy | ✓ | ✓ | ✓ | ✓ |
| Jamaica | ✓ | ✓ | ✓ | — |
| Japan | ✓ | ✓ | — | ✓ |
| Jersey | ✓ | ✓ | — | — |
| Jordan | ✓ | ✓ | ✓ | — |
| Kazakhstan | ✓ | ✓ | ✓ | — |
| Kenya | ✓ | ✓ | ✓ | ✓ |
| Kiribati | ✓ | — | — | — |
| Kosovo | ✓ | ✓ | ✓ | ✓ |
| Kuwait | ✓ | ✓ | ✓ | — |
| Kyrgyzstan | ✓ | ✓ | ✓ | — |
| Laos | ✓ | — | ✓ | — |
| Latvia | ✓ | ✓ | ✓ | ✓ |
| Lebanon | ✓ | ✓ | ✓ | — |
| Lesotho | ✓ | — | — | — |
| Liberia | ✓ | — | — | — |
| Libya | ✓ | — | — | — |
| Liechtenstein | ✓ | ✓ | ✓ | ✓ |
| Lithuania | ✓ | ✓ | ✓ | ✓ |
| Luxembourg | ✓ | ✓ | ✓ | ✓ |
| Macao | ✓ | ✓ | ✓ | ✓ |
| Madagascar | ✓ | ✓ | — | — |
| Malawi | ✓ | — | ✓ | — |
| Malaysia | ✓ | ✓ | ✓ | ✓ |
| Maldives | ✓ | — | — | — |
| Mali | ✓ | — | — | — |
| Malta | ✓ | ✓ | ✓ | ✓ |
| Marshall Islands | ✓ | — | — | — |
| Mauritania | ✓ | — | — | — |
| Mauritius | ✓ | — | ✓ | — |
| Mexico | ✓ | ✓ | ✓ | ✓ |
| Micronesia | ✓ | — | — | — |
| Moldova | ✓ | ✓ | ✓ | ✓ |
| Monaco | ✓ | — | ✓ | ✓ |
| Mongolia | ✓ | ✓ | ✓ | — |
| Montenegro | ✓ | ✓ | ✓ | — |
| Montserrat | ✓ | — | — | — |
| Morocco | ✓ | ✓ | ✓ | ✓ |
| Mozambique | ✓ | — | ✓ | — |
| Myanmar | ✓ | ✓ | — | — |
| Namibia | ✓ | — | ✓ | — |
| Nauru | ✓ | — | — | — |
| Nepal | ✓ | ✓ | ✓ | — |
| Netherlands | ✓ | ✓ | ✓ | ✓ |
| New Zealand | ✓ | ✓ | ✓ | — |
| Nicaragua | ✓ | ✓ | — | — |
| Niger | ✓ | — | — | — |
| Nigeria | ✓ | ✓ | — | — |
| North Macedonia | ✓ | ✓ | ✓ | ✓ |
| Norway | ✓ | ✓ | ✓ | ✓ |
| Oman | ✓ | ✓ | ✓ | ✓ |
| Pakistan | ✓ | ✓ | ✓ | — |
| Palau | ✓ | — | — | — |
| Palestine | ✓ | ✓ | ✓ | — |
| Panama | ✓ | ✓ | ✓ | ✓ |
| Papua New Guinea | ✓ | — | — | — |
| Paraguay | ✓ | ✓ | ✓ | — |
| Peru | ✓ | ✓ | ✓ | ✓ |
| Philippines | ✓ | ✓ | ✓ | — |
| Poland | ✓ | ✓ | ✓ | ✓ |
| Portugal | ✓ | ✓ | ✓ | ✓ |
| Puerto Rico | ✓ | ✓ | ✓ | — |
| Qatar | ✓ | ✓ | ✓ | ✓ |
| Romania | ✓ | ✓ | ✓ | ✓ |
| Rwanda | ✓ | ✓ | ✓ | — |
| Saint Kitts and Nevis | ✓ | — | — | — |
| Saint Lucia | ✓ | ✓ | ✓ | — |
| Saint Martin (French part) | ✓ | ✓ | — | — |
| Samoa | ✓ | ✓ | — | — |
| San Marino | ✓ | — | ✓ | — |
| Sao Tome and Principe | ✓ | — | — | — |
| Saudi Arabia | ✓ | ✓ | ✓ | ✓ |
| Senegal | ✓ | ✓ | ✓ | — |
| Serbia | ✓ | ✓ | ✓ | — |
| Seychelles | ✓ | — | ✓ | — |
| Sierra Leone | ✓ | — | — | — |
| Singapore | ✓ | ✓ | ✓ | — |
| Sint Maarten | — | — | ✓ | — |
| Slovakia | ✓ | ✓ | ✓ | ✓ |
| Slovenia | ✓ | ✓ | ✓ | ✓ |
| Solomon Islands | ✓ | — | — | — |
| Somalia | ✓ | — | ✓ | — |
| South Africa | ✓ | ✓ | ✓ | — |
| South Korea | ✓ | ✓ | ✓ | ✓ |
| South Sudan | ✓ | — | — | — |
| Spain | ✓ | ✓ | ✓ | ✓ |
| Sri Lanka | ✓ | ✓ | ✓ | — |
| St. Vincent and the Grenadines | ✓ | — | ✓ | — |
| Sudan | ✓ | — | — | — |
| Suriname | ✓ | — | ✓ | — |
| Sweden | ✓ | ✓ | ✓ | ✓ |
| Switzerland | ✓ | ✓ | ✓ | ✓ |
| Taiwan | ✓ | ✓ | ✓ | ✓ |
| Tajikistan | ✓ | — | — | — |
| Tanzania | ✓ | ✓ | ✓ | — |
| Thailand | ✓ | ✓ | ✓ | — |
| Timor-Leste | ✓ | — | — | — |
| Togo | ✓ | ✓ | ✓ | — |
| Tonga | ✓ | — | — | — |
| Trinidad and Tobago | ✓ | ✓ | ✓ | — |
| Tunisia | ✓ | ✓ | ✓ | — |
| Turkey | ✓ | ✓ | ✓ | ✓ |
| Turkmenistan | ✓ | — | — | — |
| Turks and Caicos Islands | ✓ | — | — | — |
| Tuvalu | ✓ | — | — | — |
| Uganda | ✓ | ✓ | ✓ | — |
| Ukraine | ✓ | ✓ | — | ✓ |
| United Arab Emirates | ✓ | ✓ | ✓ | — |
| United Kingdom | ✓ | ✓ | ✓ | ✓ |
| United States | ✓ | ✓ | ✓ | ✓ |
| Uruguay | ✓ | ✓ | ✓ | — |
| US Virgin Islands | ✓ | ✓ | ✓ | — |
| Uzbekistan | ✓ | ✓ | ✓ | — |
| Vanuatu | ✓ | — | — | — |
| Venezuela | ✓ | ✓ | ✓ | — |
| Vietnam | ✓ | ✓ | ✓ | — |
| Yemen | ✓ | ✓ | ✓ | — |
| Zambia | ✓ | — | ✓ | — |
| Zimbabwe | ✓ | ✓ | ✓ | — |
## Proof of address
When `proofOfAddress` is `incomplete` in `kyc.requirements`, the customer must
submit a document confirming their residential address. Submit it through the
[presigned upload flow](/platform/guides/customer-api#upload-a-file).
**Accepted documents**
* Utility bills (electricity, gas, internet, landline, or water)
* Bank or credit card statements
* Mortgage statement
* Certificate of voter registration
* Government correspondence (for example, from a tax authority or licensing
body)
* Lease agreements or rent receipts
* Insurance documents
* Tax documents
* Official letters from schools, employers, or government agencies
* Employer's certificate (payslips are not accepted)
* Certificate of vehicle registration
**Recency**
Documents must be dated within the last **90 days**. Documents outside this
window are rejected.
**Country coverage**
The accepted document list applies globally with no country-specific
restrictions.
## Selfie and liveness check
Details on selfie and liveness check requirements are being finalized. Contact
your MoonPay account team if you need this information sooner.
# Android
Source: https://dev.moonpay.com/platform/guides/manual-integration/android
Manual frame integration for Android using WebView and Kotlin.
Use `WebView` to embed frames in native Android applications. The WebView communicates with frames via JavaScript interfaces and `evaluateJavascript`.
Read the [manual integration
overview](/platform/guides/manual-integration/overview) for core concepts
before you continue.
## Setup
### Dependencies
The examples below use [Tink](https://github.com/google/tink) for cryptographic operations, but you can use any library that supports X25519 and AES-GCM.
```kotlin theme={null}
// build.gradle.kts (app level)
dependencies {
implementation("com.google.crypto.tink:tink-android:1.12.0")
}
```
### Key generation
```kotlin theme={null}
import com.google.crypto.tink.subtle.X25519
data class MoonPayKeyPair(
val privateKey: ByteArray,
val publicKeyHex: String
)
object MoonPayCrypto {
fun generateKeyPair(): MoonPayKeyPair {
val privateKey = X25519.generatePrivateKey()
val publicKey = X25519.publicFromPrivate(privateKey)
val publicKeyHex = publicKey.joinToString("") { "%02x".format(it) }
return MoonPayKeyPair(privateKey, publicKeyHex)
}
}
```
### Decryption utility
```kotlin theme={null}
import android.util.Base64
import com.google.crypto.tink.subtle.Hkdf
import com.google.crypto.tink.subtle.X25519
import org.json.JSONObject
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
object MoonPayDecryptor {
fun decrypt(encryptedValue: String, privateKey: ByteArray): String {
// The frame returns the encrypted payload as a base64-encoded
// JSON string. Decode the base64 first, then parse the JSON.
val decodedJson = String(Base64.decode(encryptedValue, Base64.DEFAULT), Charsets.UTF_8)
val encrypted = JSONObject(decodedJson)
val iv = encrypted.getString("iv").hexToByteArray()
val ephemeralPublicKey = encrypted.getString("ephemeralPublicKey").hexToByteArray()
val ciphertext = encrypted.getString("ciphertext").hexToByteArray()
// Derive shared secret using X25519
val sharedSecret = X25519.computeSharedSecret(privateKey, ephemeralPublicKey)
// Derive AES key using HKDF
val aesKey = Hkdf.computeHkdf(
"HMACSHA256",
sharedSecret,
ByteArray(0),
ByteArray(0),
32
)
// Decrypt using AES-GCM
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val keySpec = SecretKeySpec(aesKey, "AES")
val gcmSpec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec)
val decrypted = cipher.doFinal(ciphertext)
return String(decrypted, Charsets.UTF_8)
}
private fun String.hexToByteArray(): ByteArray {
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
}
```
### Base frame fragment
Create a reusable base fragment for frame communication:
```kotlin theme={null}
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.fragment.app.Fragment
import org.json.JSONObject
import java.util.UUID
abstract class MoonPayFrameFragment : Fragment() {
protected lateinit var webView: WebView
protected val channelId: String = UUID.randomUUID().toString()
private val handler = Handler(Looper.getMainLooper())
companion object {
const val FRAME_ORIGIN = "https://platform.moonpay.com"
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
webView = WebView(requireContext()).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
addJavascriptInterface(MoonPayBridge(), "MoonPayBridge")
// Required for the test-mode Apple Pay flow — see "Handle
// JavaScript dialogs" below.
webChromeClient = MoonPayChromeClient(this@MoonPayFrameFragment)
}
return webView
}
protected fun loadFrame(path: String, params: Map) {
val queryString = params.entries.joinToString("&") { "${it.key}=${it.value}" }
val url = "$FRAME_ORIGIN$path?$queryString"
webView.loadUrl(url)
}
protected fun sendMessage(kind: String, payload: JSONObject? = null) {
val message = JSONObject().apply {
put("version", 2)
put("meta", JSONObject().put("channelId", channelId))
put("kind", kind)
payload?.let { put("payload", it) }
}
// Re-encode the JSON as a string literal. The frame's bridge
// listens for `MessageEvent`s whose `data` is a string and
// ignores anything else, so we must post a string — not an
// object literal.
val stringLiteral = JSONObject.quote(message.toString())
val script = "window.postMessage($stringLiteral, '*');"
webView.evaluateJavascript(script, null)
}
private fun handleMessage(data: JSONObject) {
val meta = data.optJSONObject("meta") ?: return
if (meta.optString("channelId") != channelId) return
val kind = data.optString("kind")
if (kind == "handshake") {
sendMessage("ack")
onFrameHandshakeComplete()
}
onFrameMessage(kind, data.optJSONObject("payload"))
}
// Abstract methods for subclasses
protected abstract fun onFrameMessage(kind: String, payload: JSONObject?)
protected abstract fun onFrameHandshakeComplete()
override fun onDestroyView() {
super.onDestroyView()
webView.destroy()
}
inner class MoonPayBridge {
@JavascriptInterface
fun postMessage(data: String) {
handler.post {
try {
val json = JSONObject(data)
handleMessage(json)
} catch (e: Exception) {
// Ignore malformed messages
}
}
}
}
}
```
### Handle JavaScript dialogs
In [test mode](/overview/test-mode#apple-pay), the Apple Pay frame renders a mock button and uses `window.confirm` to simulate the Apple Pay payment sheet. Android's `WebView` returns `false` for `window.confirm`, `alert`, and `prompt` unless you attach a `WebChromeClient` that handles them — so without this, every test transaction silently comes back with `status: "failed"`.
Surface the simulated payment sheet with an `AlertDialog`:
```kotlin theme={null}
import android.app.AlertDialog
import android.webkit.JsResult
import android.webkit.WebChromeClient
import android.webkit.WebView
import androidx.fragment.app.Fragment
class MoonPayChromeClient(private val fragment: Fragment) : WebChromeClient() {
override fun onJsConfirm(
view: WebView?,
url: String?,
message: String?,
result: JsResult
): Boolean {
val context = fragment.context ?: return false
AlertDialog.Builder(context)
.setTitle("Test Mode")
.setMessage(message?.takeIf { it.isNotEmpty() } ?: "Simulate Apple Pay?")
.setPositiveButton("OK") { _, _ -> result.confirm() }
.setNegativeButton("Cancel") { _, _ -> result.cancel() }
.setOnCancelListener { result.cancel() }
.show()
return true
}
}
```
`OK` simulates a successful test transaction (the frame emits `complete` with a non-failed status); `Cancel` simulates a failed transaction (the frame emits `complete` with `status: "failed"`).
Attach the `WebChromeClient` even if you only plan to ship live mode. The
default `WebView` behaviour applies to any `window.confirm`, `alert`, or
`prompt` the frame might surface, and makes test-mode debugging impossible
without it.
***
## Check frame
The check frame verifies whether a customer already has an active connection. It's headless — no UI is rendered. Use it to skip the connect flow for returning customers. See [check frame reference](/platform/frames/check) for event details.
### Check fragment
```kotlin theme={null}
import org.json.JSONObject
interface CheckFrameListener {
fun onCheckActive(accessToken: String, clientToken: String, expiresAt: String)
fun onCheckConnectionRequired(accessToken: String, clientToken: String)
fun onCheckFailed(status: String, reason: String?)
fun onCheckError(code: String, message: String)
}
class MoonPayCheckFragment : MoonPayFrameFragment() {
private lateinit var keyPair: MoonPayKeyPair
private var sessionToken: String? = null
var listener: CheckFrameListener? = null
companion object {
private const val ARG_SESSION_TOKEN = "sessionToken"
fun newInstance(sessionToken: String): MoonPayCheckFragment {
return MoonPayCheckFragment().apply {
arguments = Bundle().apply {
putString(ARG_SESSION_TOKEN, sessionToken)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionToken = arguments?.getString(ARG_SESSION_TOKEN)
keyPair = MoonPayCrypto.generateKeyPair()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadFrame("/platform/v1/check-connection", mapOf(
"sessionToken" to sessionToken!!,
"publicKey" to keyPair.publicKeyHex,
"channelId" to channelId
))
}
override fun onFrameMessage(kind: String, payload: JSONObject?) {
when (kind) {
"complete" -> handleComplete(payload)
"error" -> {
listener?.onCheckError(
payload?.optString("code") ?: "unknown",
payload?.optString("message") ?: "Unknown error"
)
}
}
}
override fun onFrameHandshakeComplete() {
// Handshake complete, checking connection status
}
private fun handleComplete(payload: JSONObject?) {
val status = payload?.optString("status") ?: return
when (status) {
"active" -> {
val credentials = payload.optString("credentials")
val expiresAt = payload.optString("expiresAt")
val customer = payload.optJSONObject("customer")
val country = customer?.optString("country")
val administrativeArea = customer?.optString("administrativeArea")
val area = customer?.optString("area")
// payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.
try {
val decryptedPayload = MoonPayDecryptor.decrypt(credentials, keyPair.privateKey)
val credentials = JSONObject(decryptedPayload)
val accessToken = credentials.getString("accessToken")
val clientToken = credentials.getString("clientToken")
listener?.onCheckActive(accessToken, clientToken, expiresAt)
} catch (e: Exception) {
listener?.onCheckError("decryption", "Failed to decrypt credentials")
}
}
"connectionRequired" -> {
val encryptedCredentials = payload.optString("credentials")
// payload.optBoolean("mismatch") is true when the session's email and phone resolve to different customers — route through the connect flow first.
try {
val decryptedPayload = MoonPayDecryptor.decrypt(encryptedCredentials, keyPair.privateKey)
val anonymousCredentials = JSONObject(decryptedPayload)
val accessToken = anonymousCredentials.getString("accessToken")
val clientToken = anonymousCredentials.getString("clientToken")
listener?.onCheckConnectionRequired(accessToken, clientToken)
} catch (e: Exception) {
listener?.onCheckError("decryption", "Failed to decrypt anonymous credentials")
}
}
"pending", "unavailable", "failed" -> {
listener?.onCheckFailed(status, payload.optString("reason"))
}
}
}
}
```
### Usage
```kotlin theme={null}
class SplashActivity : AppCompatActivity(), CheckFrameListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_frame_container)
val fragment = MoonPayCheckFragment.newInstance("your-session-token")
fragment.listener = this
supportFragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment)
.commit()
}
override fun onCheckActive(accessToken: String, clientToken: String, expiresAt: String) {
// Customer is already connected — skip to payment
CredentialsManager.accessToken = accessToken
CredentialsManager.clientToken = clientToken
startActivity(Intent(this, PaymentActivity::class.java))
finish()
}
override fun onCheckConnectionRequired(accessToken: String, clientToken: String) {
// Store both tokens in memory, then show connect frame with the anonymous clientToken
CredentialsManager.accessToken = accessToken
CredentialsManager.clientToken = clientToken
val intent = Intent(this, ConnectActivity::class.java).apply {
putExtra("clientToken", clientToken)
}
startActivity(intent)
finish()
}
override fun onCheckFailed(status: String, reason: String?) {
// Handle terminal statuses (pending, unavailable, failed)
}
override fun onCheckError(code: String, message: String) {
// Handle check errors
}
}
```
***
## Connect frame
The connect frame establishes a customer connection to your application. See [connect frame reference](/platform/frames/connect) for event details.
### Connect fragment
```kotlin theme={null}
import org.json.JSONObject
interface ConnectFrameListener {
fun onConnectComplete(accessToken: String, clientToken: String, expiresAt: String)
fun onConnectFailed(status: String, reason: String?)
fun onConnectError(code: String, message: String)
}
class MoonPayConnectFragment : MoonPayFrameFragment() {
private lateinit var keyPair: MoonPayKeyPair
private var clientToken: String? = null
var listener: ConnectFrameListener? = null
companion object {
private const val ARG_CLIENT_TOKEN = "clientToken"
fun newInstance(clientToken: String): MoonPayConnectFragment {
return MoonPayConnectFragment().apply {
arguments = Bundle().apply {
putString(ARG_CLIENT_TOKEN, clientToken)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
keyPair = MoonPayCrypto.generateKeyPair()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadFrame("/platform/v1/connect", mapOf(
"clientToken" to clientToken!!,
"publicKey" to keyPair.publicKeyHex,
"channelId" to channelId
))
}
override fun onFrameMessage(kind: String, payload: JSONObject?) {
when (kind) {
"complete" -> handleComplete(payload)
"error" -> {
listener?.onConnectError(
payload?.optString("code") ?: "unknown",
payload?.optString("message") ?: "Unknown error"
)
}
}
}
override fun onFrameHandshakeComplete() {
// Handshake complete, waiting for customer interaction
}
private fun handleComplete(payload: JSONObject?) {
val status = payload?.optString("status") ?: return
when (status) {
"active" -> {
val credentials = payload.optString("credentials")
val expiresAt = payload.optString("expiresAt")
val customer = payload.optJSONObject("customer")
val country = customer?.optString("country")
val administrativeArea = customer?.optString("administrativeArea")
val area = customer?.optString("area")
// payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.
try {
val decryptedPayload = MoonPayDecryptor.decrypt(credentials, keyPair.privateKey)
val credentials = JSONObject(decryptedPayload)
val accessToken = credentials.getString("accessToken")
val clientToken = credentials.getString("clientToken")
listener?.onConnectComplete(accessToken, clientToken, expiresAt)
} catch (e: Exception) {
listener?.onConnectError("decryption", "Failed to decrypt credentials")
}
}
"pending", "unavailable", "failed" -> {
listener?.onConnectFailed(status, payload.optString("reason"))
}
}
}
}
```
### Usage with Activity
```kotlin theme={null}
class ConnectActivity : AppCompatActivity(), ConnectFrameListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_frame_container)
// The clientToken is the anonymous token passed from the check frame's
// `connectionRequired` response.
val clientToken = intent.getStringExtra("clientToken")!!
val fragment = MoonPayConnectFragment.newInstance(clientToken)
fragment.listener = this
supportFragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment)
.commit()
}
// ConnectFrameListener implementation
override fun onConnectComplete(accessToken: String, clientToken: String, expiresAt: String) {
// Store credentials in memory
CredentialsManager.accessToken = accessToken
CredentialsManager.clientToken = clientToken
Toast.makeText(this, "Connected!", Toast.LENGTH_SHORT).show()
// Navigate to next screen
startActivity(Intent(this, PaymentActivity::class.java))
finish()
}
override fun onConnectFailed(status: String, reason: String?) {
AlertDialog.Builder(this)
.setTitle("Connection $status")
.setMessage(reason ?: "Please try again later.")
.setPositiveButton("OK") { _, _ -> finish() }
.show()
}
override fun onConnectError(code: String, message: String) {
AlertDialog.Builder(this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK") { _, _ -> finish() }
.show()
}
}
```
***
## Add Card frame
The add card frame lets a customer save a new card to their account. See [add card frame reference](/platform/frames/add-card) for event details.
### What you'll need
Before you initialize the add card frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
### Add Card fragment
```kotlin theme={null}
import org.json.JSONObject
interface AddCardFrameListener {
fun onAddCardComplete(cardId: String, brand: String, last4: String)
fun onAddCardError(code: String, message: String)
}
class MoonPayAddCardFragment : MoonPayFrameFragment() {
private var clientToken: String? = null
var listener: AddCardFrameListener? = null
companion object {
private const val ARG_CLIENT_TOKEN = "clientToken"
fun newInstance(clientToken: String): MoonPayAddCardFragment {
return MoonPayAddCardFragment().apply {
arguments = Bundle().apply {
putString(ARG_CLIENT_TOKEN, clientToken)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadFrame("/platform/v1/add-card", mapOf(
"clientToken" to clientToken!!,
"channelId" to channelId
))
}
override fun onFrameMessage(kind: String, payload: JSONObject?) {
when (kind) {
"complete" -> {
val card = payload?.optJSONObject("card")
listener?.onAddCardComplete(
card?.optString("id") ?: "",
card?.optString("brand") ?: "",
card?.optString("last4") ?: ""
)
}
"error" -> {
listener?.onAddCardError(
payload?.optString("code") ?: "unknown",
payload?.optString("message") ?: "Unknown error"
)
}
}
}
override fun onFrameHandshakeComplete() {
// Handshake complete
}
}
```
### Usage with Activity
```kotlin theme={null}
class SaveCardActivity : AppCompatActivity(), AddCardFrameListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_frame_container)
val fragment = MoonPayAddCardFragment.newInstance(CredentialsManager.clientToken!!)
fragment.listener = this
supportFragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment)
.commit()
}
override fun onAddCardComplete(cardId: String, brand: String, last4: String) {
Toast.makeText(this, "Card saved: $brand •••• $last4", Toast.LENGTH_SHORT).show()
// Proceed to payment with the saved card
finish()
}
override fun onAddCardError(code: String, message: String) {
AlertDialog.Builder(this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK") { _, _ -> finish() }
.show()
}
}
```
***
## Buy frame
The buy frame processes a card or bank-transfer payment for a quote. It is headless — rendered at zero size — while the customer completes payment. For cards, if 3-D Secure is required, the frame emits a `challenge` event with a URL you open in a separate challenge frame. For bank transfers (SEPA, EUR), quote with `paymentMethod.type` set to `"sepa"`; the `complete` event returns a transaction that stays `pending` and carries a `bankTransferDepositInfo` object you render natively so the customer can send the deposit. See the [buy frame reference](/platform/frames/buy) for event details, and [Pay with bank transfer](/platform/guides/pay-with-bank-transfer) for the full bank-transfer walkthrough.
For bank transfers, the customer must include the payment `reference` from
`bankTransferDepositInfo` with their transfer, or it is rejected. Render the
deposit details, including the reference, in your own UI. See the [transaction
object](/api-reference/platform/objects-and-types/transaction#bank-transfer-deposit-info).
### What you'll need
Before you initialize the buy frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Buy fragment
```kotlin theme={null}
import org.json.JSONObject
interface BuyFrameListener {
fun onBuyComplete(transactionId: String, status: String)
fun onBuyChallenge(url: String)
fun onBuyError(code: String, message: String)
}
class MoonPayBuyFragment : MoonPayFrameFragment() {
private var clientToken: String? = null
private var quoteSignature: String? = null
private var externalTransactionId: String? = null
var listener: BuyFrameListener? = null
companion object {
private const val ARG_CLIENT_TOKEN = "clientToken"
private const val ARG_QUOTE_SIGNATURE = "quoteSignature"
private const val ARG_EXTERNAL_TRANSACTION_ID = "externalTransactionId"
fun newInstance(
clientToken: String,
quoteSignature: String,
externalTransactionId: String? = null,
): MoonPayBuyFragment {
return MoonPayBuyFragment().apply {
arguments = Bundle().apply {
putString(ARG_CLIENT_TOKEN, clientToken)
putString(ARG_QUOTE_SIGNATURE, quoteSignature)
putString(ARG_EXTERNAL_TRANSACTION_ID, externalTransactionId)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
quoteSignature = arguments?.getString(ARG_QUOTE_SIGNATURE)
externalTransactionId = arguments?.getString(ARG_EXTERNAL_TRANSACTION_ID)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadFrame("/platform/v1/buy", buildMap {
put("clientToken", clientToken!!)
put("channelId", channelId)
put("signature", quoteSignature!!)
externalTransactionId?.let { put("externalTransactionId", it) }
})
}
fun updateQuote(signature: String) {
quoteSignature = signature
sendMessage("setQuote", JSONObject().put("quote", JSONObject().put("signature", signature)))
}
override fun onFrameMessage(kind: String, payload: JSONObject?) {
when (kind) {
"complete" -> {
val transaction = payload?.optJSONObject("transaction")
listener?.onBuyComplete(
transaction?.optString("id") ?: "",
transaction?.optString("status") ?: ""
)
}
"challenge" -> {
val url = payload?.optString("url") ?: ""
listener?.onBuyChallenge(url)
}
"error" -> {
listener?.onBuyError(
payload?.optString("code") ?: "unknown",
payload?.optString("message") ?: "Unknown error"
)
}
}
}
override fun onFrameHandshakeComplete() {
// Handshake complete
}
}
```
### Challenge handling
When the buy fragment receives a challenge URL, add a `MoonPayChallengeFragment` to present the 3-D Secure flow. On completion, cancellation, or error, remove both the challenge and buy fragments:
```kotlin theme={null}
import org.json.JSONObject
interface ChallengeFrameListener {
fun onChallengeComplete(transactionId: String, status: String)
fun onChallengeCancelled()
fun onChallengeError(code: String, message: String)
}
class MoonPayChallengeFragment : MoonPayFrameFragment() {
private var challengeUrl: String? = null
private var clientToken: String? = null
var listener: ChallengeFrameListener? = null
companion object {
private const val ARG_CHALLENGE_URL = "challengeUrl"
private const val ARG_CLIENT_TOKEN = "clientToken"
fun newInstance(challengeUrl: String, clientToken: String): MoonPayChallengeFragment {
return MoonPayChallengeFragment().apply {
arguments = Bundle().apply {
putString(ARG_CHALLENGE_URL, challengeUrl)
putString(ARG_CLIENT_TOKEN, clientToken)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
challengeUrl = arguments?.getString(ARG_CHALLENGE_URL)
clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val urlWithParams = Uri.parse(challengeUrl)
.buildUpon()
.appendQueryParameter("clientToken", clientToken)
.appendQueryParameter("channelId", channelId)
.toString()
webView.loadUrl(urlWithParams)
}
override fun onFrameMessage(kind: String, payload: JSONObject?) {
when (kind) {
"complete" -> {
val transaction = payload?.optJSONObject("transaction")
listener?.onChallengeComplete(
transaction?.optString("id") ?: "",
transaction?.optString("status") ?: ""
)
}
"cancelled" -> {
listener?.onChallengeCancelled()
}
"error" -> {
listener?.onChallengeError(
payload?.optString("code") ?: "unknown",
payload?.optString("message") ?: "Unknown error"
)
}
}
}
override fun onFrameHandshakeComplete() {
// Handshake complete
}
}
```
### Usage with Activity
```kotlin theme={null}
class BuyPaymentActivity : AppCompatActivity(), BuyFrameListener, ChallengeFrameListener {
private var buyFragment: MoonPayBuyFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_frame_container)
val fragment = MoonPayBuyFragment.newInstance(
clientToken = CredentialsManager.clientToken!!,
quoteSignature = currentQuote.signature
)
fragment.listener = this
supportFragmentManager.beginTransaction()
.add(R.id.frame_container, fragment, "buy")
.commit()
buyFragment = fragment
}
// BuyFrameListener implementation
override fun onBuyComplete(transactionId: String, status: String) {
Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
// Navigate to transaction status screen or poll for updates
}
override fun onBuyChallenge(url: String) {
val challengeFragment = MoonPayChallengeFragment.newInstance(
url,
CredentialsManager.clientToken!!
)
challengeFragment.listener = this
supportFragmentManager.beginTransaction()
.add(R.id.frame_container, challengeFragment, "challenge")
.commit()
}
override fun onBuyError(code: String, message: String) {
AlertDialog.Builder(this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK", null)
.show()
}
// ChallengeFrameListener implementation
override fun onChallengeComplete(transactionId: String, status: String) {
removeFragments()
Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
// Navigate to transaction status screen or poll for updates
}
override fun onChallengeCancelled() {
removeFragments()
}
override fun onChallengeError(code: String, message: String) {
removeFragments()
AlertDialog.Builder(this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK", null)
.show()
}
private fun removeFragments() {
val transaction = supportFragmentManager.beginTransaction()
supportFragmentManager.findFragmentByTag("challenge")?.let { transaction.remove(it) }
supportFragmentManager.findFragmentByTag("buy")?.let { transaction.remove(it) }
transaction.commit()
}
}
```
***
## Google Pay frame
The Google Pay frame renders the Google Pay button and handles the payment flow. See [Google Pay frame reference](/platform/frames/google-pay) for event details.
### Enable the Payment Request API
Google Pay relies on the [Payment Request API](https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API), which is **disabled by default** in Android WebView. Enable it before loading the frame.
The embedding requirements (minimum versions, the `androidx.webkit:webkit:1.14.0` dependency, and the `AndroidManifest.xml` `` entries) live on the [Google Pay frame reference](/platform/frames/google-pay#android-webview). Meet those first.
Enable the Payment Request API on the WebView in your base fragment's `onCreateView`:
```kotlin theme={null}
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
// Inside onCreateView, after settings.javaScriptEnabled = true
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
WebSettingsCompat.setPaymentRequestEnabled(webView.settings, true)
}
```
### What you'll need
Before you initialize the Google Pay frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Google Pay fragment
```kotlin theme={null}
import org.json.JSONObject
interface GooglePayFrameListener {
fun onGooglePayReady()
fun onGooglePayComplete(transactionId: String, status: String)
fun onGooglePayFailed(reason: String)
fun onGooglePayError(code: String, message: String)
fun onGooglePayChallenge(url: String)
}
class MoonPayGooglePayFragment : MoonPayFrameFragment() {
private var clientToken: String? = null
private var quoteSignature: String? = null
private var externalTransactionId: String? = null
var listener: GooglePayFrameListener? = null
companion object {
private const val ARG_CLIENT_TOKEN = "clientToken"
private const val ARG_QUOTE_SIGNATURE = "quoteSignature"
private const val ARG_EXTERNAL_TRANSACTION_ID = "externalTransactionId"
fun newInstance(
clientToken: String,
quoteSignature: String,
externalTransactionId: String? = null,
): MoonPayGooglePayFragment {
return MoonPayGooglePayFragment().apply {
arguments = Bundle().apply {
putString(ARG_CLIENT_TOKEN, clientToken)
putString(ARG_QUOTE_SIGNATURE, quoteSignature)
putString(ARG_EXTERNAL_TRANSACTION_ID, externalTransactionId)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
quoteSignature = arguments?.getString(ARG_QUOTE_SIGNATURE)
externalTransactionId = arguments?.getString(ARG_EXTERNAL_TRANSACTION_ID)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadFrame("/platform/v1/google-pay", buildMap {
put("clientToken", clientToken!!)
put("channelId", channelId)
put("signature", quoteSignature!!)
externalTransactionId?.let { put("externalTransactionId", it) }
})
}
fun updateQuote(signature: String) {
quoteSignature = signature
sendMessage("setQuote", JSONObject().put("quote", JSONObject().put("signature", signature)))
}
override fun onFrameMessage(kind: String, payload: JSONObject?) {
when (kind) {
"ready" -> {
listener?.onGooglePayReady()
}
"complete" -> {
val transaction = payload?.optJSONObject("transaction")
val status = transaction?.optString("status") ?: ""
if (status == "failed") {
listener?.onGooglePayFailed(
transaction?.optString("failureReason") ?: "Transaction failed"
)
} else {
listener?.onGooglePayComplete(
transaction?.optString("id") ?: "",
status
)
}
}
"challenge" -> {
val url = payload?.optString("url") ?: ""
listener?.onGooglePayChallenge(url)
}
"error" -> {
listener?.onGooglePayError(
payload?.optString("code") ?: "unknown",
payload?.optString("message") ?: "Unknown error"
)
}
}
}
override fun onFrameHandshakeComplete() {
// Handshake complete
}
}
```
### Usage with Activity
```kotlin theme={null}
class GooglePayActivity : AppCompatActivity(), GooglePayFrameListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_frame_container)
val fragment = MoonPayGooglePayFragment.newInstance(
CredentialsManager.clientToken!!,
currentQuote.signature
)
fragment.listener = this
supportFragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment)
.commit()
}
override fun onGooglePayReady() {
// Google Pay button is visible and ready
}
override fun onGooglePayComplete(transactionId: String, status: String) {
Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
// Navigate to transaction status screen
}
override fun onGooglePayFailed(reason: String) {
AlertDialog.Builder(this)
.setTitle("Payment Failed")
.setMessage(reason)
.setPositiveButton("OK", null)
.show()
}
override fun onGooglePayError(code: String, message: String) {
if (code == "quoteExpired") {
// Fetch new quote and update the fragment
return
}
AlertDialog.Builder(this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK") { _, _ -> finish() }
.show()
}
override fun onGooglePayChallenge(url: String) {
// Open the challenge frame — see "Challenge handling" section
}
}
```
***
## Widget frame
Apple Pay is not available on Android. Use the [widget frame](/platform/frames/widget) instead to render the full MoonPay buy experience — including payment collection and transaction confirmation — for credit/debit card, Google Pay, bank transfers, and more. See [pay with widget](/platform/guides/pay-with-widget) for a full walkthrough.
### What you'll need
Before you initialize the widget frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Widget fragment
```kotlin theme={null}
import org.json.JSONObject
interface WidgetFrameListener {
fun onWidgetReady()
fun onWidgetTransactionCreated(transactionId: String, status: String)
fun onWidgetComplete(transactionId: String, status: String)
fun onWidgetFailed(failureReason: String)
fun onWidgetError(code: String, message: String)
}
class MoonPayWidgetFragment : MoonPayFrameFragment() {
private var clientToken: String? = null
private var quoteSignature: String? = null
private var externalTransactionId: String? = null
var listener: WidgetFrameListener? = null
companion object {
private const val ARG_CLIENT_TOKEN = "clientToken"
private const val ARG_QUOTE_SIGNATURE = "quoteSignature"
private const val ARG_EXTERNAL_TRANSACTION_ID = "externalTransactionId"
fun newInstance(
clientToken: String,
quoteSignature: String,
externalTransactionId: String? = null,
): MoonPayWidgetFragment {
return MoonPayWidgetFragment().apply {
arguments = Bundle().apply {
putString(ARG_CLIENT_TOKEN, clientToken)
putString(ARG_QUOTE_SIGNATURE, quoteSignature)
putString(ARG_EXTERNAL_TRANSACTION_ID, externalTransactionId)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
quoteSignature = arguments?.getString(ARG_QUOTE_SIGNATURE)
externalTransactionId = arguments?.getString(ARG_EXTERNAL_TRANSACTION_ID)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadFrame("/platform/v1/widget", buildMap {
put("flow", "buy")
put("clientToken", clientToken!!)
put("quoteSignature", quoteSignature!!)
put("channelId", channelId)
externalTransactionId?.let { put("externalTransactionId", it) }
})
}
override fun onFrameMessage(kind: String, payload: JSONObject?) {
when (kind) {
"ready" -> {
listener?.onWidgetReady()
}
"transactionCreated" -> {
val transaction = payload?.optJSONObject("transaction")
listener?.onWidgetTransactionCreated(
transaction?.optString("id") ?: "",
transaction?.optString("status") ?: ""
)
}
"complete" -> handleComplete(payload)
"error" -> {
listener?.onWidgetError(
payload?.optString("code") ?: "unknown",
payload?.optString("message") ?: "Unknown error"
)
}
}
}
override fun onFrameHandshakeComplete() {
// Handshake complete, widget loading
}
private fun handleComplete(payload: JSONObject?) {
val transaction = payload?.optJSONObject("transaction") ?: return
val status = transaction.optString("status")
if (status == "failed") {
val reason = transaction.optString("failureReason", "Transaction failed")
listener?.onWidgetFailed(reason)
} else {
val transactionId = transaction.optString("id")
listener?.onWidgetComplete(transactionId, status)
}
}
}
```
### Usage with Activity
```kotlin theme={null}
class PaymentActivity : AppCompatActivity(), WidgetFrameListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_frame_container)
val fragment = MoonPayWidgetFragment.newInstance(
clientToken = CredentialsManager.clientToken!!,
quoteSignature = currentQuote.signature
)
fragment.listener = this
supportFragmentManager.beginTransaction()
.replace(R.id.frame_container, fragment)
.commit()
}
// WidgetFrameListener implementation
override fun onWidgetReady() {
// Widget is loaded and visible
}
override fun onWidgetTransactionCreated(transactionId: String, status: String) {
// Transaction initiated — customer may need to complete 3-D Secure
Toast.makeText(this, "Transaction created: $transactionId", Toast.LENGTH_SHORT).show()
}
override fun onWidgetComplete(transactionId: String, status: String) {
Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
// Navigate to transaction status screen or poll for updates
}
override fun onWidgetFailed(failureReason: String) {
AlertDialog.Builder(this)
.setTitle("Payment Failed")
.setMessage(failureReason)
.setPositiveButton("OK", null)
.show()
}
override fun onWidgetError(code: String, message: String) {
AlertDialog.Builder(this)
.setTitle("Error")
.setMessage(message)
.setPositiveButton("OK", null)
.show()
}
}
```
# Flutter
Source: https://dev.moonpay.com/platform/guides/manual-integration/flutter
Manual frame integration for Flutter using webview_flutter.
Use `webview_flutter` to embed frames in Flutter applications. The WebView communicates with your Dart code through JavaScript channels.
Read the [manual integration
overview](/platform/guides/manual-integration/overview) for core concepts
before you continue.
## Setup
### Dependencies
The examples below use [cryptography](https://pub.dev/packages/cryptography) for X25519 key exchange, but you can use any library that supports X25519 and AES-GCM. Add the required packages to your `pubspec.yaml`:
```yaml theme={null}
dependencies:
webview_flutter: ^4.13.0
cryptography: ^2.7.0
convert: ^3.1.1
```
For platform-specific setup, add the implementation packages:
```yaml theme={null}
dependencies:
webview_flutter_android: ^4.3.0 # Android
webview_flutter_wkwebview: ^3.18.0 # iOS/macOS
```
Run `flutter pub get` to install the packages.
### Platform configuration
Add the following to your `ios/Runner/Info.plist`:
```xml theme={null}
io.flutter.embedded_views_preview
```
Set the minimum SDK version in `android/app/build.gradle`:
```groovy theme={null}
android {
defaultConfig {
minSdkVersion 21
}
}
```
### Key generation
Create a utility class for X25519 key generation and decryption:
```dart theme={null}
import 'dart:convert';
import 'dart:typed_data';
import 'package:cryptography/cryptography.dart';
import 'package:convert/convert.dart';
class MoonPayCrypto {
final SimpleKeyPair _keyPair;
final String publicKeyHex;
MoonPayCrypto._(this._keyPair, this.publicKeyHex);
/// Generate a new X25519 keypair for frame communication
static Future generate() async {
final algorithm = X25519();
final keyPair = await algorithm.newKeyPair();
final publicKey = await keyPair.extractPublicKey();
final publicKeyHex = hex.encode(publicKey.bytes);
return MoonPayCrypto._(keyPair, publicKeyHex);
}
/// Decrypt an encrypted payload from the frame
Future decryptPayload(String encryptedValue) async {
// The frame returns the encrypted payload as a base64-encoded JSON
// string. Decode the base64 first, then parse the JSON.
final decodedJson = utf8.decode(base64Decode(encryptedValue));
final encrypted = jsonDecode(decodedJson) as Map;
final ephemeralPublicKeyBytes = hex.decode(encrypted['ephemeralPublicKey'] as String);
final iv = hex.decode(encrypted['iv'] as String);
final ciphertext = hex.decode(encrypted['ciphertext'] as String);
// Create shared secret using X25519
final algorithm = X25519();
final ephemeralPublicKey = SimplePublicKey(
Uint8List.fromList(ephemeralPublicKeyBytes),
type: KeyPairType.x25519,
);
final sharedSecret = await algorithm.sharedSecretKey(
keyPair: _keyPair,
remotePublicKey: ephemeralPublicKey,
);
// Derive AES key using HKDF
final hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32);
final derivedKey = await hkdf.deriveKey(
secretKey: sharedSecret,
info: Uint8List(0),
nonce: Uint8List(0),
);
// Decrypt using AES-GCM
final aesGcm = AesGcm.with256bits();
final secretBox = SecretBox(
ciphertext,
nonce: iv,
mac: Mac.empty, // MAC is appended to ciphertext
);
final decrypted = await aesGcm.decrypt(
secretBox,
secretKey: derivedKey,
);
return utf8.decode(decrypted);
}
}
/// Generate a unique channel ID for frame communication
String generateChannelId() {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final random = (DateTime.now().microsecond * 1000).toRadixString(36);
return 'ch_${timestamp}_$random';
}
```
### Base WebView widget
Create a reusable widget for frame communication:
```dart theme={null}
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
const String frameOrigin = 'https://platform.moonpay.com';
class FrameMessage {
final int version;
final String channelId;
final String kind;
final Map? payload;
FrameMessage({
required this.version,
required this.channelId,
required this.kind,
this.payload,
});
factory FrameMessage.fromJson(Map json) {
return FrameMessage(
version: json['version'] as int,
channelId: (json['meta'] as Map)['channelId'] as String,
kind: json['kind'] as String,
payload: json['payload'] as Map?,
);
}
Map toJson() => {
'version': version,
'meta': {'channelId': channelId},
'kind': kind,
if (payload != null) 'payload': payload,
};
}
class MoonPayWebView extends StatefulWidget {
final String url;
final String channelId;
final void Function(FrameMessage) onMessage;
final VoidCallback onHandshake;
final double? height;
const MoonPayWebView({
super.key,
required this.url,
required this.channelId,
required this.onMessage,
required this.onHandshake,
this.height,
});
@override
State createState() => _MoonPayWebViewState();
}
class _MoonPayWebViewState extends State {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'MoonPayBridge',
onMessageReceived: _handleMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) => _injectMessageBridge(),
),
)
..loadRequest(Uri.parse(widget.url));
}
void _injectMessageBridge() {
_controller.runJavaScript('''
(function() {
window.addEventListener('message', function(e) {
if (MoonPayBridge && MoonPayBridge.postMessage) {
MoonPayBridge.postMessage(typeof e.data === 'string' ? e.data : JSON.stringify(e.data));
}
});
})();
''');
}
void _handleMessage(JavaScriptMessage jsMessage) {
try {
final data = jsonDecode(jsMessage.message) as Map;
final meta = data['meta'] as Map?;
if (meta?['channelId'] != widget.channelId) return;
final message = FrameMessage.fromJson(data);
if (message.kind == 'handshake') {
sendMessage('ack');
widget.onHandshake();
}
widget.onMessage(message);
} catch (_) {
// Ignore malformed messages
}
}
void sendMessage(String kind, [Map? payload]) {
final message = FrameMessage(
version: 2,
channelId: widget.channelId,
kind: kind,
payload: payload,
);
final jsonString = jsonEncode(message.toJson());
// Re-encode the JSON as a string literal. The frame's bridge listens
// for `MessageEvent`s whose `data` is a string and ignores anything
// else, so we must post a string — not an object literal.
final stringLiteral = jsonEncode(jsonString);
_controller.runJavaScript('''
window.postMessage($stringLiteral, '*');
''');
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.height ?? double.infinity,
child: WebViewWidget(controller: _controller),
);
}
}
```
### Handle JavaScript dialogs
In [test mode](/overview/test-mode#apple-pay), the Apple Pay frame renders a mock button and uses `window.confirm` to simulate the Apple Pay payment sheet (`OK` = success, `Cancel` = failed).
`webview_flutter` does not surface `window.confirm` (or `alert` / `prompt`) by default — the underlying `WKWebView` on iOS and `WebView` on Android both require explicit dialog handlers. Without one, the call returns `false` with no UI shown, the frame interprets that as the customer cancelling, and every test transaction comes back with `status: "failed"`.
This WebView dialog requirement is captured on the [Apple Pay frame reference](/platform/frames/apple-pay#wkwebview), which is the source of truth.
Wire dialog handling via the platform-specific implementations. On iOS use `WebKitWebViewController`'s UI delegate hooks; on Android use `AndroidWebViewController` to attach a `WebChromeClient` whose `onJsConfirm` returns the customer's choice. Surface the prompt with a Flutter `AlertDialog` so it matches the rest of your UI, and call back into the controller with the result.
Wire dialog handling even if you only plan to ship live mode. It applies to
any `window.confirm`, `alert`, or `prompt` the frame might surface, and makes
test-mode debugging impossible without it.
***
## Check frame
The check frame verifies whether a customer already has an active connection. It's headless — no UI is rendered. Use it to skip the connect flow for returning customers. See [check frame reference](/platform/frames/check) for event details.
### Check widget
```dart theme={null}
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
class MoonPayCheckFrame extends StatefulWidget {
final String sessionToken;
final void Function(ConnectCredentials) onActive;
final void Function(ConnectCredentials) onConnectionRequired;
final void Function(ConnectError) onError;
final VoidCallback? onPending;
final VoidCallback? onUnavailable;
const MoonPayCheckFrame({
super.key,
required this.sessionToken,
required this.onActive,
required this.onConnectionRequired,
required this.onError,
this.onPending,
this.onUnavailable,
});
@override
State createState() => _MoonPayCheckFrameState();
}
class _MoonPayCheckFrameState extends State {
late final String _channelId;
late final Future _cryptoFuture;
@override
void initState() {
super.initState();
_channelId = generateChannelId();
_cryptoFuture = MoonPayCrypto.generate();
}
String _buildFrameUrl(String publicKeyHex) {
final params = {
'sessionToken': widget.sessionToken,
'publicKey': publicKeyHex,
'channelId': _channelId,
};
return '$frameOrigin/platform/v1/check-connection?${Uri(queryParameters: params).query}';
}
Future _handleMessage(FrameMessage message, MoonPayCrypto crypto) async {
switch (message.kind) {
case 'complete':
final payload = message.payload!;
final status = payload['status'] as String;
switch (status) {
case 'active':
final decryptedPayload = await crypto.decryptPayload(payload['credentials'] as String);
final credentials = jsonDecode(decryptedPayload) as Map;
final customer = payload['customer'] as Map?;
final country = customer?['country'] as String?;
final administrativeArea = customer?['administrativeArea'] as String?;
final area = customer?['area'] as String?;
// payload['capabilities']['ramps']['requirements']['paymentDisclosures'] is deprecated.
widget.onActive(ConnectCredentials(
accessToken: credentials['accessToken'] as String,
clientToken: credentials['clientToken'] as String,
));
break;
case 'connectionRequired':
final decryptedPayload = await crypto.decryptPayload(payload['credentials'] as String);
final anonymousCredentials = jsonDecode(decryptedPayload) as Map;
// payload['mismatch'] is true when the session's email and phone resolve to different customers — route through the connect flow first.
widget.onConnectionRequired(ConnectCredentials(
accessToken: anonymousCredentials['accessToken'] as String,
clientToken: anonymousCredentials['clientToken'] as String,
));
break;
case 'pending':
widget.onPending?.call();
break;
case 'unavailable':
widget.onUnavailable?.call();
break;
case 'failed':
widget.onError(ConnectError(
code: 'failed',
message: payload['reason'] as String? ?? 'Check failed',
));
break;
}
break;
case 'error':
final payload = message.payload!;
widget.onError(ConnectError(
code: payload['code'] as String,
message: payload['message'] as String,
));
break;
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _cryptoFuture,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final crypto = snapshot.data!;
return MoonPayWebView(
url: _buildFrameUrl(crypto.publicKeyHex),
channelId: _channelId,
onMessage: (msg) => _handleMessage(msg, crypto),
onHandshake: () {},
);
},
);
}
}
```
### Usage
```dart theme={null}
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: MoonPayCheckFrame(
sessionToken: 'your-session-token',
onActive: (credentials) {
// Customer is already connected — skip to payment
debugPrint('Already connected! Access token: ${credentials.accessToken}');
Navigator.of(context).pushReplacementNamed('/payment');
},
onConnectionRequired: (credentials) {
// Store both tokens in memory, then pass the clientToken to the connect frame
CredentialsStore.instance.set(
accessToken: credentials.accessToken,
clientToken: credentials.clientToken,
);
Navigator.of(context).pushReplacementNamed(
'/connect',
arguments: credentials.clientToken,
);
},
onError: (error) {
debugPrint('Check failed: ${error.message}');
},
),
);
}
}
```
***
## Connect frame
The connect frame establishes a customer connection to your application. See [connect frame reference](/platform/frames/connect) for event details.
### Connect widget
```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
class ConnectCredentials {
final String accessToken;
final String clientToken;
ConnectCredentials({required this.accessToken, required this.clientToken});
}
class ConnectError {
final String code;
final String message;
ConnectError({required this.code, required this.message});
}
class MoonPayConnectFrame extends StatefulWidget {
final String clientToken;
final void Function(ConnectCredentials) onComplete;
final void Function(ConnectError) onError;
final VoidCallback? onPending;
final VoidCallback? onUnavailable;
const MoonPayConnectFrame({
super.key,
required this.clientToken,
required this.onComplete,
required this.onError,
this.onPending,
this.onUnavailable,
});
@override
State createState() => _MoonPayConnectFrameState();
}
class _MoonPayConnectFrameState extends State {
late final String _channelId;
late final Future _cryptoFuture;
@override
void initState() {
super.initState();
_channelId = generateChannelId();
_cryptoFuture = MoonPayCrypto.generate();
}
String _buildFrameUrl(String publicKeyHex) {
final params = {
'clientToken': widget.clientToken,
'publicKey': publicKeyHex,
'channelId': _channelId,
};
return '$frameOrigin/platform/v1/connect?${Uri(queryParameters: params).query}';
}
Future _handleMessage(FrameMessage message, MoonPayCrypto crypto) async {
switch (message.kind) {
case 'complete':
final payload = message.payload!;
final status = payload['status'] as String;
switch (status) {
case 'active':
final decryptedPayload = await crypto.decryptPayload(payload['credentials'] as String);
final credentials = jsonDecode(decryptedPayload) as Map;
final customer = payload['customer'] as Map?;
final country = customer?['country'] as String?;
final administrativeArea = customer?['administrativeArea'] as String?;
final area = customer?['area'] as String?;
// payload['capabilities']['ramps']['requirements']['paymentDisclosures'] is deprecated.
widget.onComplete(ConnectCredentials(
accessToken: credentials['accessToken'] as String,
clientToken: credentials['clientToken'] as String,
));
break;
case 'pending':
widget.onPending?.call();
break;
case 'unavailable':
widget.onUnavailable?.call();
break;
case 'failed':
widget.onError(ConnectError(
code: 'failed',
message: payload['reason'] as String? ?? 'Connection failed',
));
break;
}
break;
case 'error':
final payload = message.payload!;
widget.onError(ConnectError(
code: payload['code'] as String,
message: payload['message'] as String,
));
break;
}
}
void _handleHandshake() {
// Handshake complete
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _cryptoFuture,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final crypto = snapshot.data!;
return MoonPayWebView(
url: _buildFrameUrl(crypto.publicKeyHex),
channelId: _channelId,
onMessage: (msg) => _handleMessage(msg, crypto),
onHandshake: _handleHandshake,
);
},
);
}
}
```
### Usage
```dart theme={null}
class ConnectScreen extends StatelessWidget {
const ConnectScreen({super.key});
@override
Widget build(BuildContext context) {
// The clientToken is the anonymous token passed from the check frame's
// `connectionRequired` response.
final clientToken = ModalRoute.of(context)!.settings.arguments as String;
return Scaffold(
appBar: AppBar(title: const Text('Connect')),
body: MoonPayConnectFrame(
clientToken: clientToken,
onComplete: (credentials) {
// Replace the anonymous credentials with the authenticated ones and
// store them in memory (e.g., Provider or Riverpod).
debugPrint('Connected! Access token: ${credentials.accessToken}');
Navigator.of(context).pushReplacementNamed('/home');
},
onError: (error) {
debugPrint('Connection failed: ${error.message}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${error.message}')),
);
},
onPending: () => debugPrint('Connection pending'),
onUnavailable: () => debugPrint('Connection unavailable'),
),
);
}
}
```
***
## Apple Pay frame
The Apple Pay frame renders the Apple Pay button and handles the payment flow. See [Apple Pay frame reference](/platform/frames/apple-pay) for event details.
Apple Pay only works on iOS devices with Apple Pay configured.
### Apple Pay widget
```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
class TransactionResult {
final String id;
final String status;
TransactionResult({required this.id, required this.status});
}
class MoonPayApplePayFrame extends StatefulWidget {
final String clientToken;
final String quoteSignature;
final String? externalTransactionId;
final void Function(TransactionResult) onComplete;
final void Function(String url) onChallenge;
final void Function(ConnectError) onError;
final VoidCallback onQuoteExpired;
final VoidCallback? onReady;
const MoonPayApplePayFrame({
super.key,
required this.clientToken,
required this.quoteSignature,
this.externalTransactionId,
required this.onComplete,
required this.onChallenge,
required this.onError,
required this.onQuoteExpired,
this.onReady,
});
@override
State createState() => _MoonPayApplePayFrameState();
}
class _MoonPayApplePayFrameState extends State {
late final String _channelId;
late String _currentQuote;
final GlobalKey<_MoonPayWebViewState> _webViewKey = GlobalKey();
@override
void initState() {
super.initState();
_channelId = generateChannelId();
_currentQuote = widget.quoteSignature;
}
@override
void didUpdateWidget(MoonPayApplePayFrame oldWidget) {
super.didUpdateWidget(oldWidget);
// Send updated quote to frame
if (widget.quoteSignature != _currentQuote) {
_currentQuote = widget.quoteSignature;
_webViewKey.currentState?.sendMessage('setQuote', {
'quote': {'signature': _currentQuote},
});
}
}
String _buildFrameUrl() {
final params = {
'clientToken': widget.clientToken,
'signature': widget.quoteSignature,
'channelId': _channelId,
if (widget.externalTransactionId != null)
'externalTransactionId': widget.externalTransactionId!,
};
return '$frameOrigin/platform/v1/apple-pay?${Uri(queryParameters: params).query}';
}
void _handleMessage(FrameMessage message) {
switch (message.kind) {
case 'ready':
widget.onReady?.call();
break;
case 'complete':
final payload = message.payload!;
final transaction = payload['transaction'] as Map;
if (transaction['status'] == 'failed') {
widget.onError(ConnectError(
code: 'transactionFailed',
message: transaction['failureReason'] as String,
));
} else {
widget.onComplete(TransactionResult(
id: transaction['id'] as String,
status: transaction['status'] as String,
));
}
break;
case 'challenge':
final payload = message.payload!;
final url = payload['url'] as String;
widget.onChallenge(url);
break;
case 'error':
final payload = message.payload!;
final code = payload['code'] as String;
if (code == 'quoteExpired') {
widget.onQuoteExpired();
} else {
widget.onError(ConnectError(
code: code,
message: payload['message'] as String,
));
}
break;
}
}
@override
Widget build(BuildContext context) {
return MoonPayWebView(
key: _webViewKey,
url: _buildFrameUrl(),
channelId: _channelId,
onMessage: _handleMessage,
onHandshake: () {}, // Handled in _handleMessage
height: 56, // Apple Pay button height
);
}
}
```
### Usage
```dart theme={null}
class PaymentScreen extends StatefulWidget {
final String clientToken;
final String initialQuoteSignature;
const PaymentScreen({
super.key,
required this.clientToken,
required this.initialQuoteSignature,
});
@override
State createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State {
late String _quoteSignature;
@override
void initState() {
super.initState();
_quoteSignature = widget.initialQuoteSignature;
}
Future _fetchNewQuote() async {
// Fetch a new quote from your API
final newQuote = await yourApi.getQuote();
setState(() => _quoteSignature = newQuote.signature);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pay with Apple Pay')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Your payment summary UI
const Spacer(),
MoonPayApplePayFrame(
clientToken: widget.clientToken,
quoteSignature: _quoteSignature,
onComplete: (transaction) {
debugPrint('Transaction initiated: ${transaction.id}');
Navigator.of(context).pushNamed('/transaction-status');
},
onChallenge: (url) => debugPrint('Challenge required: $url'),
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Payment error: ${error.message}')),
);
},
onQuoteExpired: _fetchNewQuote,
onReady: () => debugPrint('Apple Pay button ready'),
),
],
),
),
);
}
}
```
***
## Google Pay frame
The Google Pay frame renders the Google Pay button and handles the payment flow. See [Google Pay frame reference](/platform/frames/google-pay) for event details.
### Google Pay widget
```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
class MoonPayGooglePayFrame extends StatefulWidget {
final String clientToken;
final String quoteSignature;
final String? externalTransactionId;
final void Function(TransactionResult) onComplete;
final void Function(String url) onChallenge;
final void Function(ConnectError) onError;
final VoidCallback onQuoteExpired;
final VoidCallback? onReady;
const MoonPayGooglePayFrame({
super.key,
required this.clientToken,
required this.quoteSignature,
this.externalTransactionId,
required this.onComplete,
required this.onChallenge,
required this.onError,
required this.onQuoteExpired,
this.onReady,
});
@override
State createState() => _MoonPayGooglePayFrameState();
}
class _MoonPayGooglePayFrameState extends State {
late final String _channelId;
late String _currentQuote;
final GlobalKey<_MoonPayWebViewState> _webViewKey = GlobalKey();
@override
void initState() {
super.initState();
_channelId = generateChannelId();
_currentQuote = widget.quoteSignature;
}
@override
void didUpdateWidget(MoonPayGooglePayFrame oldWidget) {
super.didUpdateWidget(oldWidget);
// Send updated quote to frame
if (widget.quoteSignature != _currentQuote) {
_currentQuote = widget.quoteSignature;
_webViewKey.currentState?.sendMessage('setQuote', {
'quote': {'signature': _currentQuote},
});
}
}
String _buildFrameUrl() {
final params = {
'clientToken': widget.clientToken,
'signature': widget.quoteSignature,
'channelId': _channelId,
if (widget.externalTransactionId != null)
'externalTransactionId': widget.externalTransactionId!,
};
return '$frameOrigin/platform/v1/google-pay?${Uri(queryParameters: params).query}';
}
void _handleMessage(FrameMessage message) {
switch (message.kind) {
case 'ready':
widget.onReady?.call();
break;
case 'complete':
final payload = message.payload!;
final transaction = payload['transaction'] as Map;
if (transaction['status'] == 'failed') {
widget.onError(ConnectError(
code: 'transactionFailed',
message: transaction['failureReason'] as String,
));
} else {
widget.onComplete(TransactionResult(
id: transaction['id'] as String,
status: transaction['status'] as String,
));
}
break;
case 'challenge':
final payload = message.payload!;
final url = payload['url'] as String;
widget.onChallenge(url);
break;
case 'error':
final payload = message.payload!;
final code = payload['code'] as String;
if (code == 'quoteExpired') {
widget.onQuoteExpired();
} else {
widget.onError(ConnectError(
code: code,
message: payload['message'] as String,
));
}
break;
}
}
@override
Widget build(BuildContext context) {
return MoonPayWebView(
key: _webViewKey,
url: _buildFrameUrl(),
channelId: _channelId,
onMessage: _handleMessage,
onHandshake: () {}, // Handled in _handleMessage
height: 56, // Google Pay button height
);
}
}
```
### Usage
```dart theme={null}
class GooglePayScreen extends StatefulWidget {
final String clientToken;
final String initialQuoteSignature;
const GooglePayScreen({
super.key,
required this.clientToken,
required this.initialQuoteSignature,
});
@override
State createState() => _GooglePayScreenState();
}
class _GooglePayScreenState extends State {
late String _quoteSignature;
@override
void initState() {
super.initState();
_quoteSignature = widget.initialQuoteSignature;
}
Future _fetchNewQuote() async {
// Fetch a new quote from your API
final newQuote = await yourApi.getQuote();
setState(() => _quoteSignature = newQuote.signature);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pay with Google Pay')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// Your payment summary UI
const Spacer(),
MoonPayGooglePayFrame(
clientToken: widget.clientToken,
quoteSignature: _quoteSignature,
onComplete: (transaction) {
debugPrint('Transaction initiated: ${transaction.id}');
Navigator.of(context).pushNamed('/transaction-status');
},
onChallenge: (url) => debugPrint('Challenge required: $url'),
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Payment error: ${error.message}')),
);
},
onQuoteExpired: _fetchNewQuote,
onReady: () => debugPrint('Google Pay button ready'),
),
],
),
),
);
}
}
```
***
## Add Card frame
The add card frame lets a customer save a new card to their account. See [add card frame reference](/platform/frames/add-card) for event details.
### What you'll need
Before you initialize the add card frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
### Add Card widget
```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
class CardResult {
final String id;
final String brand;
final String last4;
final String cardType;
final int expirationMonth;
final int expirationYear;
final bool active;
CardResult({
required this.id,
required this.brand,
required this.last4,
required this.cardType,
required this.expirationMonth,
required this.expirationYear,
required this.active,
});
}
class MoonPayAddCardFrame extends StatefulWidget {
final String clientToken;
final void Function(CardResult) onComplete;
final void Function(ConnectError) onError;
final VoidCallback? onReady;
const MoonPayAddCardFrame({
super.key,
required this.clientToken,
required this.onComplete,
required this.onError,
this.onReady,
});
@override
State createState() => _MoonPayAddCardFrameState();
}
class _MoonPayAddCardFrameState extends State {
late final String _channelId;
@override
void initState() {
super.initState();
_channelId = generateChannelId();
}
String _buildFrameUrl() {
final params = {
'clientToken': widget.clientToken,
'channelId': _channelId,
};
return '$frameOrigin/platform/v1/add-card?${Uri(queryParameters: params).query}';
}
void _handleMessage(FrameMessage message) {
switch (message.kind) {
case 'ready':
widget.onReady?.call();
break;
case 'complete':
final payload = message.payload!;
final card = payload['card'] as Map;
final availability = card['availability'] as Map;
widget.onComplete(CardResult(
id: card['id'] as String,
brand: card['brand'] as String,
last4: card['last4'] as String,
cardType: card['cardType'] as String,
expirationMonth: card['expirationMonth'] as int,
expirationYear: card['expirationYear'] as int,
active: availability['active'] as bool,
));
break;
case 'error':
final payload = message.payload!;
widget.onError(ConnectError(
code: payload['code'] as String,
message: payload['message'] as String,
));
break;
}
}
@override
Widget build(BuildContext context) {
return MoonPayWebView(
url: _buildFrameUrl(),
channelId: _channelId,
onMessage: _handleMessage,
onHandshake: () {},
);
}
}
```
### Usage
```dart theme={null}
class SaveCardScreen extends StatelessWidget {
final String clientToken;
const SaveCardScreen({super.key, required this.clientToken});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Add Card')),
body: MoonPayAddCardFrame(
clientToken: clientToken,
onComplete: (card) {
debugPrint('Card saved: ${card.brand} •••• ${card.last4}');
Navigator.of(context).pop();
},
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${error.message}')),
);
},
onReady: () => debugPrint('Add card frame ready'),
),
);
}
}
```
***
## Buy frame
The buy frame processes a card or bank-transfer payment for a quote. It is headless — rendered at zero size — while the customer completes payment. For cards, if 3-D Secure is required, the frame emits a `challenge` event with a URL you open in a separate challenge frame. For bank transfers (SEPA, EUR), quote with `paymentMethod.type` set to `"sepa"`; the `complete` event returns a transaction that stays `pending` and carries a `bankTransferDepositInfo` object you render natively so the customer can send the deposit. See the [buy frame reference](/platform/frames/buy) for event details, and [Pay with bank transfer](/platform/guides/pay-with-bank-transfer) for the full bank-transfer walkthrough.
For bank transfers, the customer must include the payment `reference` from
`bankTransferDepositInfo` with their transfer, or it is rejected. Render the
deposit details, including the reference, in your own UI. See the [transaction
object](/api-reference/platform/objects-and-types/transaction#bank-transfer-deposit-info).
### What you'll need
Before you initialize the buy frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Buy widget
```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
class MoonPayBuyFrame extends StatefulWidget {
final String clientToken;
final String quoteSignature;
final String? externalTransactionId;
final void Function(TransactionResult) onComplete;
final void Function(String url) onChallenge;
final void Function(ConnectError) onError;
final VoidCallback? onQuoteExpired;
const MoonPayBuyFrame({
super.key,
required this.clientToken,
required this.quoteSignature,
this.externalTransactionId,
required this.onComplete,
required this.onChallenge,
required this.onError,
this.onQuoteExpired,
});
@override
State createState() => _MoonPayBuyFrameState();
}
class _MoonPayBuyFrameState extends State {
late final String _channelId;
late String _currentQuote;
final GlobalKey<_MoonPayWebViewState> _webViewKey = GlobalKey();
@override
void initState() {
super.initState();
_channelId = generateChannelId();
_currentQuote = widget.quoteSignature;
}
@override
void didUpdateWidget(MoonPayBuyFrame oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.quoteSignature != _currentQuote) {
_currentQuote = widget.quoteSignature;
_webViewKey.currentState?.sendMessage('setQuote', {
'quote': {'signature': _currentQuote},
});
}
}
String _buildFrameUrl() {
final params = {
'clientToken': widget.clientToken,
'channelId': _channelId,
'signature': widget.quoteSignature,
if (widget.externalTransactionId != null)
'externalTransactionId': widget.externalTransactionId!,
};
return '$frameOrigin/platform/v1/buy?${Uri(queryParameters: params).query}';
}
void _handleMessage(FrameMessage message) {
switch (message.kind) {
case 'complete':
final payload = message.payload!;
final transaction = payload['transaction'] as Map;
widget.onComplete(TransactionResult(
id: transaction['id'] as String,
status: transaction['status'] as String,
));
break;
case 'challenge':
final payload = message.payload!;
widget.onChallenge(payload['url'] as String);
break;
case 'error':
final payload = message.payload!;
final code = payload['code'] as String;
if (code == 'quoteExpired') {
widget.onQuoteExpired?.call();
} else {
widget.onError(ConnectError(
code: code,
message: payload['message'] as String,
));
}
break;
}
}
@override
Widget build(BuildContext context) {
return SizedBox.shrink(
child: MoonPayWebView(
key: _webViewKey,
url: _buildFrameUrl(),
channelId: _channelId,
onMessage: _handleMessage,
onHandshake: () {},
height: 0,
),
);
}
}
```
### Challenge widget
```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
class MoonPayChallengeFrame extends StatefulWidget {
final String challengeUrl;
final String clientToken;
final void Function(TransactionResult) onComplete;
final VoidCallback onCancelled;
final void Function(ConnectError) onError;
const MoonPayChallengeFrame({
super.key,
required this.challengeUrl,
required this.clientToken,
required this.onComplete,
required this.onCancelled,
required this.onError,
});
@override
State createState() => _MoonPayChallengeFrameState();
}
class _MoonPayChallengeFrameState extends State {
late final String _channelId;
@override
void initState() {
super.initState();
_channelId = generateChannelId();
}
void _handleMessage(FrameMessage message) {
switch (message.kind) {
case 'complete':
final payload = message.payload!;
final transaction = payload['transaction'] as Map;
widget.onComplete(TransactionResult(
id: transaction['id'] as String,
status: transaction['status'] as String,
));
break;
case 'cancelled':
widget.onCancelled();
break;
case 'error':
final payload = message.payload!;
widget.onError(ConnectError(
code: payload['code'] as String,
message: payload['message'] as String,
));
break;
}
}
String get _frameUrl {
final uri = Uri.parse(widget.challengeUrl);
return uri
.replace(queryParameters: {
...uri.queryParameters,
'clientToken': widget.clientToken,
'channelId': _channelId,
})
.toString();
}
@override
Widget build(BuildContext context) {
return MoonPayWebView(
url: _frameUrl,
channelId: _channelId,
onMessage: _handleMessage,
onHandshake: () {},
);
}
}
```
### Usage
```dart theme={null}
class BuyPaymentScreen extends StatefulWidget {
final String clientToken;
final String initialQuoteSignature;
const BuyPaymentScreen({
super.key,
required this.clientToken,
required this.initialQuoteSignature,
});
@override
State createState() => _BuyPaymentScreenState();
}
class _BuyPaymentScreenState extends State {
late String _quoteSignature;
String? _challengeUrl;
@override
void initState() {
super.initState();
_quoteSignature = widget.initialQuoteSignature;
}
Future _fetchNewQuote() async {
final newQuote = await yourApi.getQuote();
setState(() => _quoteSignature = newQuote.signature);
}
void _handleComplete(TransactionResult transaction) {
setState(() => _challengeUrl = null);
debugPrint('Transaction initiated: ${transaction.id}');
Navigator.of(context).pushNamed('/transaction-status');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Buy Crypto')),
body: Stack(
children: [
// Your payment summary UI
MoonPayBuyFrame(
clientToken: widget.clientToken,
quoteSignature: _quoteSignature,
onComplete: _handleComplete,
onChallenge: (url) => setState(() => _challengeUrl = url),
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${error.message}')),
);
},
onQuoteExpired: _fetchNewQuote,
),
if (_challengeUrl != null)
Positioned.fill(
child: MoonPayChallengeFrame(
challengeUrl: _challengeUrl!,
clientToken: widget.clientToken,
onComplete: _handleComplete,
onCancelled: () => setState(() => _challengeUrl = null),
onError: (error) {
setState(() => _challengeUrl = null);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${error.message}')),
);
},
),
),
],
),
);
}
}
```
***
## Widget frame
For payment methods beyond Apple Pay — including credit/debit cards, Google Pay, bank transfers, and more — use the [widget frame](/platform/frames/widget). It renders the full MoonPay buy experience inside a WebView, including payment collection and transaction confirmation. See [pay with widget](/platform/guides/pay-with-widget) for a full walkthrough.
### Widget widget
```dart theme={null}
import 'dart:async';
import 'package:flutter/material.dart';
class MoonPayWidgetFrame extends StatefulWidget {
final String clientToken;
final String quoteSignature;
final String? externalTransactionId;
final void Function(TransactionResult) onComplete;
final void Function(ConnectError) onError;
final void Function(String transactionId, String status)? onTransactionCreated;
final VoidCallback? onReady;
const MoonPayWidgetFrame({
super.key,
required this.clientToken,
required this.quoteSignature,
this.externalTransactionId,
required this.onComplete,
required this.onError,
this.onTransactionCreated,
this.onReady,
});
@override
State createState() => _MoonPayWidgetFrameState();
}
class _MoonPayWidgetFrameState extends State {
late final String _channelId;
@override
void initState() {
super.initState();
_channelId = generateChannelId();
}
String _buildFrameUrl() {
final params = {
'flow': 'buy',
'clientToken': widget.clientToken,
'quoteSignature': widget.quoteSignature,
'channelId': _channelId,
if (widget.externalTransactionId != null)
'externalTransactionId': widget.externalTransactionId!,
};
return '$frameOrigin/platform/v1/widget?${Uri(queryParameters: params).query}';
}
void _handleMessage(FrameMessage message) {
switch (message.kind) {
case 'ready':
widget.onReady?.call();
break;
case 'transactionCreated':
final payload = message.payload!;
final transaction = payload['transaction'] as Map;
widget.onTransactionCreated?.call(
transaction['id'] as String,
transaction['status'] as String,
);
break;
case 'complete':
final payload = message.payload!;
final transaction = payload['transaction'] as Map;
if (transaction['status'] == 'failed') {
widget.onError(ConnectError(
code: 'transactionFailed',
message: transaction['failureReason'] as String,
));
} else {
widget.onComplete(TransactionResult(
id: transaction['id'] as String,
status: transaction['status'] as String,
));
}
break;
case 'error':
final payload = message.payload!;
widget.onError(ConnectError(
code: payload['code'] as String,
message: payload['message'] as String,
));
break;
}
}
@override
Widget build(BuildContext context) {
return MoonPayWebView(
url: _buildFrameUrl(),
channelId: _channelId,
onMessage: _handleMessage,
onHandshake: () {},
);
}
}
```
### Usage
```dart theme={null}
class WidgetPaymentScreen extends StatelessWidget {
final String clientToken;
final String quoteSignature;
const WidgetPaymentScreen({
super.key,
required this.clientToken,
required this.quoteSignature,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Buy Crypto')),
body: MoonPayWidgetFrame(
clientToken: clientToken,
quoteSignature: quoteSignature,
onReady: () => debugPrint('Widget loaded'),
onTransactionCreated: (id, status) {
debugPrint('Transaction created: $id ($status)');
},
onComplete: (transaction) {
debugPrint('Transaction complete: ${transaction.id}');
Navigator.of(context).pushNamed('/transaction-status');
},
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Widget error: ${error.message}')),
);
},
),
);
}
}
```
# iOS
Source: https://dev.moonpay.com/platform/guides/manual-integration/ios
Manual frame integration for iOS using WKWebView and Swift.
Use `WKWebView` to embed frames in native iOS applications. The WebView communicates with frames via JavaScript message handlers and script injection.
Read the [manual integration
overview](/platform/guides/manual-integration/overview) for core concepts
before you continue.
## Setup
### Dependencies
The examples below use [swift-crypto](https://github.com/apple/swift-crypto) for X25519 key exchange, but you can use any library that supports X25519 and AES-GCM.
```swift theme={null}
// Package.swift or via Xcode's Package Dependencies
dependencies: [
.package(url: "https://github.com/apple/swift-crypto.git", from: "3.0.0"),
]
```
### Key generation
```swift theme={null}
import Crypto
import Foundation
struct MoonPayKeyPair {
let privateKey: Curve25519.KeyAgreement.PrivateKey
let publicKeyHex: String
init() {
self.privateKey = Curve25519.KeyAgreement.PrivateKey()
self.publicKeyHex = privateKey.publicKey.rawRepresentation
.map { String(format: "%02x", $0) }
.joined()
}
}
```
### Decryption utility
```swift theme={null}
import CryptoKit
import Foundation
struct MoonPayDecryptor {
struct EncryptedPayload: Codable {
let iv: String
let ephemeralPublicKey: String
let ciphertext: String
}
static func decrypt(_ encryptedValue: String, privateKey: Curve25519.KeyAgreement.PrivateKey) -> String? {
// The frame returns the encrypted payload as a base64-encoded
// JSON string. Decode the base64 first, then parse the JSON.
guard let jsonData = Data(base64Encoded: encryptedValue),
let encrypted = try? JSONDecoder().decode(EncryptedPayload.self, from: jsonData),
let ephemeralPublicKeyData = Data(hexString: encrypted.ephemeralPublicKey),
let ivData = Data(hexString: encrypted.iv),
let ciphertextData = Data(hexString: encrypted.ciphertext),
let ephemeralPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: ephemeralPublicKeyData),
let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: ephemeralPublicKey) else {
return nil
}
// Derive AES key using HKDF
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: Data(),
sharedInfo: Data(),
outputByteCount: 32
)
// AES-GCM: ciphertext includes the auth tag at the end (last 16 bytes)
guard ciphertextData.count > 16,
let sealedBox = try? AES.GCM.SealedBox(
nonce: AES.GCM.Nonce(data: ivData),
ciphertext: ciphertextData.dropLast(16),
tag: ciphertextData.suffix(16)
),
let decryptedData = try? AES.GCM.open(sealedBox, using: symmetricKey) else {
return nil
}
return String(data: decryptedData, encoding: .utf8)
}
}
extension Data {
init?(hexString: String) {
let len = hexString.count / 2
var data = Data(capacity: len)
var index = hexString.startIndex
for _ in 0.. Void) {
let alert = UIAlertController(
title: "Test Mode",
message: message.isEmpty ? "Simulate Apple Pay?" : message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
completionHandler(false)
})
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
completionHandler(true)
})
present(alert, animated: true)
}
}
```
`OK` simulates a successful test transaction (the frame emits `complete` with a non-failed status); `Cancel` simulates a failed transaction (the frame emits `complete` with `status: "failed"`).
Wire the UI delegate even if you only plan to ship live mode. The WKWebView
default behaviour applies to any `window.confirm`, `alert`, or `prompt` the
frame might surface, and it makes test-mode debugging impossible without it.
***
## Check frame
The check frame verifies whether a customer already has an active connection. It's headless — no UI is rendered. Use it to skip the connect flow for returning customers. See [check frame reference](/platform/frames/check) for event details.
### Check controller
```swift theme={null}
import UIKit
protocol CheckFrameDelegate: AnyObject {
func checkDidFindActiveConnection(accessToken: String, clientToken: String, expiresAt: String)
func checkDidRequireConnection(accessToken: String, clientToken: String)
func checkDidFail(status: String, reason: String?)
func checkDidError(code: String, message: String)
}
class MoonPayCheckViewController: MoonPayFrameViewController {
private var keyPair: MoonPayKeyPair!
private var sessionToken: String!
weak var checkDelegate: CheckFrameDelegate?
func configure(sessionToken: String) {
self.sessionToken = sessionToken
self.keyPair = MoonPayKeyPair()
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
guard sessionToken != nil else {
fatalError("Call configure(sessionToken:) before presenting")
}
loadFrame(path: "/platform/v1/check-connection", params: [
"sessionToken": sessionToken,
"publicKey": keyPair.publicKeyHex,
"channelId": channelId,
])
}
}
// MARK: - MoonPayFrameDelegate
extension MoonPayCheckViewController: MoonPayFrameDelegate {
func frameDidReceiveMessage(_ message: [String: Any]) {
guard let kind = message["kind"] as? String else { return }
switch kind {
case "complete":
handleComplete(message["payload"] as? [String: Any] ?? [:])
case "error":
let payload = message["payload"] as? [String: Any] ?? [:]
checkDelegate?.checkDidError(
code: payload["code"] as? String ?? "unknown",
message: payload["message"] as? String ?? "Unknown error"
)
default:
break
}
}
func frameDidCompleteHandshake() {
// Handshake complete — checking connection status
}
private func handleComplete(_ payload: [String: Any]) {
guard let status = payload["status"] as? String else { return }
switch status {
case "active":
guard let credentials = payload["credentials"] as? String,
let expiresAt = payload["expiresAt"] as? String,
let decryptedPayload = MoonPayDecryptor.decrypt(credentials, privateKey: keyPair.privateKey),
let credentialsData = decryptedPayload.data(using: .utf8),
let credentials = try? JSONSerialization.jsonObject(with: credentialsData) as? [String: String],
let accessToken = credentials["accessToken"],
let clientToken = credentials["clientToken"] else {
checkDelegate?.checkDidError(code: "decryption", message: "Failed to decrypt credentials")
return
}
let customer = payload["customer"] as? [String: Any]
let country = customer?["country"] as? String
let administrativeArea = customer?["administrativeArea"] as? String
let area = customer?["area"] as? String
// payload["capabilities"]["ramps"]["requirements"]["paymentDisclosures"] is deprecated.
checkDelegate?.checkDidFindActiveConnection(accessToken: accessToken, clientToken: clientToken, expiresAt: expiresAt)
case "connectionRequired":
// payload["mismatch"] is true when the session's email and phone resolve to different customers — route through the connect flow first.
guard let credentials = payload["credentials"] as? String,
let decryptedPayload = MoonPayDecryptor.decrypt(credentials, privateKey: keyPair.privateKey),
let credentialsData = decryptedPayload.data(using: .utf8),
let anonymousCredentials = try? JSONSerialization.jsonObject(with: credentialsData) as? [String: String],
let accessToken = anonymousCredentials["accessToken"],
let clientToken = anonymousCredentials["clientToken"] else {
checkDelegate?.checkDidError(code: "decryption", message: "Failed to decrypt anonymous credentials")
return
}
checkDelegate?.checkDidRequireConnection(accessToken: accessToken, clientToken: clientToken)
case "pending", "unavailable", "failed":
checkDelegate?.checkDidFail(status: status, reason: payload["reason"] as? String)
default:
break
}
}
}
```
### Usage
```swift theme={null}
class SplashViewController: UIViewController, CheckFrameDelegate {
func checkConnection() {
let checkVC = MoonPayCheckViewController()
checkVC.configure(sessionToken: "your-session-token")
checkVC.checkDelegate = self
// The check frame is headless — add as a child without visible UI
addChild(checkVC)
checkVC.view.frame = .zero
view.addSubview(checkVC.view)
checkVC.didMove(toParent: self)
}
// MARK: - CheckFrameDelegate
func checkDidFindActiveConnection(accessToken: String, clientToken: String, expiresAt: String) {
// Customer is already connected — skip to payment
CredentialsManager.shared.accessToken = accessToken
CredentialsManager.shared.clientToken = clientToken
let paymentVC = PaymentViewController()
navigationController?.pushViewController(paymentVC, animated: true)
}
func checkDidRequireConnection(accessToken: String, clientToken: String) {
// Store both tokens in memory, then show connect frame with the anonymous clientToken
CredentialsManager.shared.accessToken = accessToken
CredentialsManager.shared.clientToken = clientToken
let connectVC = MoonPayConnectViewController()
connectVC.configure(clientToken: clientToken)
navigationController?.pushViewController(connectVC, animated: true)
}
func checkDidFail(status: String, reason: String?) {
print("Check \(status): \(reason ?? "No reason provided")")
}
func checkDidError(code: String, message: String) {
print("Check error: \(code) — \(message)")
}
}
```
***
## Connect frame
The connect frame establishes a customer connection to your application. See [connect frame reference](/platform/frames/connect) for event details.
### Connect controller
```swift theme={null}
import UIKit
protocol ConnectFrameDelegate: AnyObject {
func connectDidComplete(accessToken: String, clientToken: String, expiresAt: String)
func connectDidFail(status: String, reason: String?)
func connectDidError(code: String, message: String)
}
class MoonPayConnectViewController: MoonPayFrameViewController {
private var keyPair: MoonPayKeyPair!
private var clientToken: String!
weak var connectDelegate: ConnectFrameDelegate?
func configure(clientToken: String) {
self.clientToken = clientToken
self.keyPair = MoonPayKeyPair()
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
guard clientToken != nil else {
fatalError("Call configure(clientToken:) before presenting")
}
loadFrame(path: "/platform/v1/connect", params: [
"clientToken": clientToken,
"publicKey": keyPair.publicKeyHex,
"channelId": channelId,
])
}
}
// MARK: - MoonPayFrameDelegate
extension MoonPayConnectViewController: MoonPayFrameDelegate {
func frameDidReceiveMessage(_ message: [String: Any]) {
guard let kind = message["kind"] as? String else { return }
switch kind {
case "complete":
handleComplete(message["payload"] as? [String: Any] ?? [:])
case "error":
let payload = message["payload"] as? [String: Any] ?? [:]
connectDelegate?.connectDidError(
code: payload["code"] as? String ?? "unknown",
message: payload["message"] as? String ?? "Unknown error"
)
default:
break
}
}
func frameDidCompleteHandshake() {
// Handshake complete — waiting for customer interaction
}
private func handleComplete(_ payload: [String: Any]) {
guard let status = payload["status"] as? String else { return }
switch status {
case "active":
guard let credentials = payload["credentials"] as? String,
let expiresAt = payload["expiresAt"] as? String,
let decryptedPayload = MoonPayDecryptor.decrypt(credentials, privateKey: keyPair.privateKey),
let credentialsData = decryptedPayload.data(using: .utf8),
let credentials = try? JSONSerialization.jsonObject(with: credentialsData) as? [String: String],
let accessToken = credentials["accessToken"],
let clientToken = credentials["clientToken"] else {
connectDelegate?.connectDidError(code: "decryption", message: "Failed to decrypt credentials")
return
}
let customer = payload["customer"] as? [String: Any]
let country = customer?["country"] as? String
let administrativeArea = customer?["administrativeArea"] as? String
let area = customer?["area"] as? String
// payload["capabilities"]["ramps"]["requirements"]["paymentDisclosures"] is deprecated.
connectDelegate?.connectDidComplete(accessToken: accessToken, clientToken: clientToken, expiresAt: expiresAt)
case "pending", "unavailable", "failed":
connectDelegate?.connectDidFail(status: status, reason: payload["reason"] as? String)
default:
break
}
}
}
```
### Usage
```swift theme={null}
class MyViewController: UIViewController, ConnectFrameDelegate {
// The clientToken is the anonymous token obtained from the check frame's
// `connectionRequired` response.
func showConnect(clientToken: String) {
let connectVC = MoonPayConnectViewController()
connectVC.configure(clientToken: clientToken)
connectVC.connectDelegate = self
connectVC.modalPresentationStyle = .fullScreen
present(connectVC, animated: true)
}
// MARK: - ConnectFrameDelegate
func connectDidComplete(accessToken: String, clientToken: String, expiresAt: String) {
dismiss(animated: true)
// Store credentials in memory
CredentialsManager.shared.accessToken = accessToken
CredentialsManager.shared.clientToken = clientToken
print("Connected! Expires: \(expiresAt)")
}
func connectDidFail(status: String, reason: String?) {
dismiss(animated: true)
showAlert(title: "Connection \(status)", message: reason ?? "Please try again later.")
}
func connectDidError(code: String, message: String) {
dismiss(animated: true)
showAlert(title: "Error", message: message)
}
}
```
***
## Apple Pay frame
The Apple Pay frame renders a native Apple Pay button and handles the payment flow. See [Apple Pay frame reference](/platform/frames/apple-pay) for event details.
### Apple Pay controller
```swift theme={null}
import UIKit
protocol ApplePayFrameDelegate: AnyObject {
func applePayDidComplete(transactionId: String, status: String)
func applePayDidFail(reason: String)
func applePayDidChallenge(url: String)
func applePayDidError(code: String, message: String)
func applePayDidBecomeReady()
}
class MoonPayApplePayViewController: MoonPayFrameViewController {
private var clientToken: String!
private var quoteSignature: String!
private var externalTransactionId: String?
weak var applePayDelegate: ApplePayFrameDelegate?
func configure(clientToken: String, quoteSignature: String, externalTransactionId: String? = nil) {
self.clientToken = clientToken
self.quoteSignature = quoteSignature
self.externalTransactionId = externalTransactionId
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
guard clientToken != nil, quoteSignature != nil else {
fatalError("Call configure(clientToken:quoteSignature:) before presenting")
}
var params: [String: String] = [
"clientToken": clientToken,
"channelId": channelId,
"signature": quoteSignature,
]
if let externalTransactionId = externalTransactionId {
params["externalTransactionId"] = externalTransactionId
}
loadFrame(path: "/platform/v1/apple-pay", params: params)
}
func updateQuote(signature: String) {
quoteSignature = signature
sendMessage(kind: "setQuote", payload: [
"quote": ["signature": signature]
])
}
}
// MARK: - MoonPayFrameDelegate
extension MoonPayApplePayViewController: MoonPayFrameDelegate {
func frameDidReceiveMessage(_ message: [String: Any]) {
guard let kind = message["kind"] as? String else { return }
switch kind {
case "ready":
applePayDelegate?.applePayDidBecomeReady()
case "complete":
handleComplete(message["payload"] as? [String: Any] ?? [:])
case "challenge":
let payload = message["payload"] as? [String: Any] ?? [:]
let url = payload["url"] as? String ?? ""
applePayDelegate?.applePayDidChallenge(url: url)
case "error":
let payload = message["payload"] as? [String: Any] ?? [:]
let code = payload["code"] as? String ?? "unknown"
let message = payload["message"] as? String ?? "Unknown error"
applePayDelegate?.applePayDidError(code: code, message: message)
default:
break
}
}
func frameDidCompleteHandshake() {
// Handshake complete
}
private func handleComplete(_ payload: [String: Any]) {
guard let transaction = payload["transaction"] as? [String: Any],
let status = transaction["status"] as? String else { return }
if status == "failed" {
let reason = transaction["failureReason"] as? String ?? "Transaction failed"
applePayDelegate?.applePayDidFail(reason: reason)
} else if let transactionId = transaction["id"] as? String {
applePayDelegate?.applePayDidComplete(transactionId: transactionId, status: status)
}
}
}
```
### Usage
```swift theme={null}
class PaymentViewController: UIViewController, ApplePayFrameDelegate {
private var applePayContainer: UIView!
private var applePayVC: MoonPayApplePayViewController?
override func viewDidLoad() {
super.viewDidLoad()
setupApplePayFrame()
}
private func setupApplePayFrame() {
applePayContainer = UIView(frame: CGRect(x: 20, y: 200, width: view.bounds.width - 40, height: 50))
view.addSubview(applePayContainer)
let applePayVC = MoonPayApplePayViewController()
applePayVC.configure(
clientToken: CredentialsManager.shared.clientToken!,
quoteSignature: currentQuote.signature
)
applePayVC.applePayDelegate = self
addChild(applePayVC)
applePayVC.view.frame = applePayContainer.bounds
applePayContainer.addSubview(applePayVC.view)
applePayVC.didMove(toParent: self)
self.applePayVC = applePayVC
}
// MARK: - ApplePayFrameDelegate
func applePayDidComplete(transactionId: String, status: String) {
print("Transaction \(transactionId) status: \(status)")
// Navigate to transaction status screen
}
func applePayDidFail(reason: String) {
showAlert(title: "Payment Failed", message: reason)
}
func applePayDidChallenge(url: String) {
// Render the challenge frame at the provided URL
// See "Challenge handling" in the Buy frame section
let challengeVC = MoonPayChallengeViewController()
challengeVC.configure(challengeUrl: url, clientToken: CredentialsManager.shared.clientToken!)
challengeVC.challengeDelegate = self
challengeVC.modalPresentationStyle = .fullScreen
present(challengeVC, animated: true)
}
func applePayDidError(code: String, message: String) {
if code == "quoteExpired" {
// Fetch new quote and update
Task {
let newQuote = await fetchNewQuote()
applePayVC?.updateQuote(signature: newQuote.signature)
}
return
}
showAlert(title: "Error", message: message)
}
func applePayDidBecomeReady() {
print("Apple Pay button is ready")
}
}
```
***
## Add Card frame
The add card frame lets a customer save a new card to their account. See [add card frame reference](/platform/frames/add-card) for event details.
### Add Card controller
```swift theme={null}
import UIKit
protocol AddCardFrameDelegate: AnyObject {
func addCardDidComplete(card: [String: Any])
func addCardDidError(code: String, message: String)
}
class MoonPayAddCardViewController: MoonPayFrameViewController {
private var clientToken: String!
weak var addCardDelegate: AddCardFrameDelegate?
func configure(clientToken: String) {
self.clientToken = clientToken
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
guard clientToken != nil else {
fatalError("Call configure(clientToken:) before presenting")
}
loadFrame(path: "/platform/v1/add-card", params: [
"clientToken": clientToken,
"channelId": channelId,
])
}
}
// MARK: - MoonPayFrameDelegate
extension MoonPayAddCardViewController: MoonPayFrameDelegate {
func frameDidReceiveMessage(_ message: [String: Any]) {
guard let kind = message["kind"] as? String else { return }
switch kind {
case "ready":
break
case "complete":
let payload = message["payload"] as? [String: Any] ?? [:]
let card = payload["card"] as? [String: Any] ?? [:]
addCardDelegate?.addCardDidComplete(card: card)
case "error":
let payload = message["payload"] as? [String: Any] ?? [:]
addCardDelegate?.addCardDidError(
code: payload["code"] as? String ?? "unknown",
message: payload["message"] as? String ?? "Unknown error"
)
default:
break
}
}
func frameDidCompleteHandshake() {
// Handshake complete
}
}
```
### Usage
```swift theme={null}
class SaveCardViewController: UIViewController, AddCardFrameDelegate {
private var addCardVC: MoonPayAddCardViewController?
override func viewDidLoad() {
super.viewDidLoad()
setupAddCardFrame()
}
private func setupAddCardFrame() {
let addCardVC = MoonPayAddCardViewController()
addCardVC.configure(clientToken: CredentialsManager.shared.clientToken!)
addCardVC.addCardDelegate = self
addChild(addCardVC)
addCardVC.view.frame = view.bounds
addCardVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(addCardVC.view)
addCardVC.didMove(toParent: self)
self.addCardVC = addCardVC
}
// MARK: - AddCardFrameDelegate
func addCardDidComplete(card: [String: Any]) {
print("Card saved: \(card["id"] ?? "")")
// Proceed to payment with the saved card
}
func addCardDidError(code: String, message: String) {
showAlert(title: "Error", message: message)
}
}
```
***
## Buy frame
The buy frame processes a card or bank-transfer payment for a quote. It is headless — rendered at zero size — while the customer completes payment. For cards, if 3-D Secure is required, the frame emits a `challenge` event with a URL you open in a separate challenge frame. For bank transfers (SEPA, EUR), quote with `paymentMethod.type` set to `"sepa"`; the `complete` event returns a transaction that stays `pending` and carries a `bankTransferDepositInfo` object you render natively so the customer can send the deposit. See the [buy frame reference](/platform/frames/buy) for event details, and [Pay with bank transfer](/platform/guides/pay-with-bank-transfer) for the full bank-transfer walkthrough.
For bank transfers, the customer must include the payment `reference` from
`bankTransferDepositInfo` with their transfer, or it is rejected. Render the
deposit details, including the reference, in your own UI. See the [transaction
object](/api-reference/platform/objects-and-types/transaction#bank-transfer-deposit-info).
### What you'll need
Before you initialize the buy frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Buy controller
```swift theme={null}
import UIKit
protocol BuyFrameDelegate: AnyObject {
func buyDidComplete(transactionId: String, status: String)
func buyDidChallenge(url: String)
func buyDidError(code: String, message: String)
}
class MoonPayBuyViewController: MoonPayFrameViewController {
private var clientToken: String!
private var quoteSignature: String!
private var externalTransactionId: String?
weak var buyDelegate: BuyFrameDelegate?
func configure(clientToken: String, quoteSignature: String, externalTransactionId: String? = nil) {
self.clientToken = clientToken
self.quoteSignature = quoteSignature
self.externalTransactionId = externalTransactionId
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
guard clientToken != nil, quoteSignature != nil else {
fatalError("Call configure(clientToken:quoteSignature:) before presenting")
}
var params: [String: String] = [
"clientToken": clientToken,
"channelId": channelId,
"signature": quoteSignature,
]
if let externalTransactionId = externalTransactionId {
params["externalTransactionId"] = externalTransactionId
}
loadFrame(path: "/platform/v1/buy", params: params)
}
func updateQuote(signature: String) {
quoteSignature = signature
sendMessage(kind: "setQuote", payload: [
"quote": ["signature": signature]
])
}
func dispose() {
view.removeFromSuperview()
removeFromParent()
}
}
// MARK: - MoonPayFrameDelegate
extension MoonPayBuyViewController: MoonPayFrameDelegate {
func frameDidReceiveMessage(_ message: [String: Any]) {
guard let kind = message["kind"] as? String else { return }
switch kind {
case "ready":
break
case "complete":
let payload = message["payload"] as? [String: Any] ?? [:]
let transaction = payload["transaction"] as? [String: Any] ?? [:]
let transactionId = transaction["id"] as? String ?? ""
let status = transaction["status"] as? String ?? ""
buyDelegate?.buyDidComplete(transactionId: transactionId, status: status)
case "challenge":
let payload = message["payload"] as? [String: Any] ?? [:]
let url = payload["url"] as? String ?? ""
buyDelegate?.buyDidChallenge(url: url)
case "error":
let payload = message["payload"] as? [String: Any] ?? [:]
buyDelegate?.buyDidError(
code: payload["code"] as? String ?? "unknown",
message: payload["message"] as? String ?? "Unknown error"
)
default:
break
}
}
func frameDidCompleteHandshake() {
// Handshake complete
}
}
```
### Challenge handling
When the buy frame emits a `challenge` event, present a `MoonPayChallengeViewController` with the challenge URL. On completion, cancellation, or error, call `dispose()` on the buy view controller:
```swift theme={null}
protocol ChallengeFrameDelegate: AnyObject {
func challengeDidComplete(transactionId: String, status: String)
func challengeDidCancel()
func challengeDidError(code: String, message: String)
}
class MoonPayChallengeViewController: MoonPayFrameViewController {
private var challengeUrl: String!
private var clientToken: String!
weak var challengeDelegate: ChallengeFrameDelegate?
func configure(challengeUrl: String, clientToken: String) {
self.challengeUrl = challengeUrl
self.clientToken = clientToken
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
guard let rawUrl = challengeUrl,
var components = URLComponents(string: rawUrl) else {
fatalError("Call configure(challengeUrl:clientToken:) before presenting")
}
var queryItems = components.queryItems ?? []
queryItems.append(URLQueryItem(name: "clientToken", value: clientToken))
queryItems.append(URLQueryItem(name: "channelId", value: channelId))
components.queryItems = queryItems
guard let url = components.url else { return }
webView.load(URLRequest(url: url))
}
}
// MARK: - MoonPayFrameDelegate
extension MoonPayChallengeViewController: MoonPayFrameDelegate {
func frameDidReceiveMessage(_ message: [String: Any]) {
guard let kind = message["kind"] as? String else { return }
switch kind {
case "ready":
break
case "complete":
let payload = message["payload"] as? [String: Any] ?? [:]
let transaction = payload["transaction"] as? [String: Any] ?? [:]
let transactionId = transaction["id"] as? String ?? ""
let status = transaction["status"] as? String ?? ""
challengeDelegate?.challengeDidComplete(transactionId: transactionId, status: status)
case "cancelled":
challengeDelegate?.challengeDidCancel()
case "error":
let payload = message["payload"] as? [String: Any] ?? [:]
challengeDelegate?.challengeDidError(
code: payload["code"] as? String ?? "unknown",
message: payload["message"] as? String ?? "Unknown error"
)
default:
break
}
}
func frameDidCompleteHandshake() {
// Handshake complete
}
}
```
### Usage
```swift theme={null}
class BuyPaymentViewController: UIViewController, BuyFrameDelegate, ChallengeFrameDelegate {
private var buyVC: MoonPayBuyViewController?
override func viewDidLoad() {
super.viewDidLoad()
setupBuyFrame()
}
private func setupBuyFrame() {
let buyVC = MoonPayBuyViewController()
buyVC.configure(
clientToken: CredentialsManager.shared.clientToken!,
quoteSignature: currentQuote.signature
)
buyVC.buyDelegate = self
// The buy frame is headless — add as a zero-size child
addChild(buyVC)
buyVC.view.frame = .zero
view.addSubview(buyVC.view)
buyVC.didMove(toParent: self)
self.buyVC = buyVC
}
// MARK: - BuyFrameDelegate
func buyDidComplete(transactionId: String, status: String) {
print("Transaction \(transactionId) status: \(status)")
// Navigate to transaction status screen
}
func buyDidChallenge(url: String) {
let challengeVC = MoonPayChallengeViewController()
challengeVC.configure(challengeUrl: url, clientToken: CredentialsManager.shared.clientToken!)
challengeVC.challengeDelegate = self
challengeVC.modalPresentationStyle = .fullScreen
present(challengeVC, animated: true)
}
func buyDidError(code: String, message: String) {
if code == "quoteExpired" {
Task {
let newQuote = await fetchNewQuote()
buyVC?.updateQuote(signature: newQuote.signature)
}
return
}
showAlert(title: "Error", message: message)
}
// MARK: - ChallengeFrameDelegate
func challengeDidComplete(transactionId: String, status: String) {
dismiss(animated: true)
buyVC?.dispose()
print("Transaction \(transactionId) status: \(status)")
// Navigate to transaction status screen
}
func challengeDidCancel() {
dismiss(animated: true)
buyVC?.dispose()
}
func challengeDidError(code: String, message: String) {
dismiss(animated: true)
buyVC?.dispose()
showAlert(title: "Error", message: message)
}
}
```
***
## Widget frame
For payment methods beyond Apple Pay — including credit/debit cards, Google Pay, bank transfers, and more — use the [widget frame](/platform/frames/widget). It renders the full MoonPay buy experience inside a WKWebView, including payment collection and transaction confirmation. See [pay with widget](/platform/guides/pay-with-widget) for a full walkthrough.
### Widget controller
```swift theme={null}
import UIKit
protocol WidgetFrameDelegate: AnyObject {
func widgetDidBecomeReady()
func widgetDidCreateTransaction(transactionId: String, status: String)
func widgetDidComplete(transactionId: String, status: String)
func widgetDidFail(reason: String)
func widgetDidError(code: String, message: String)
}
class MoonPayWidgetViewController: MoonPayFrameViewController {
private var clientToken: String!
private var quoteSignature: String!
private var externalTransactionId: String?
weak var widgetDelegate: WidgetFrameDelegate?
func configure(clientToken: String, quoteSignature: String, externalTransactionId: String? = nil) {
self.clientToken = clientToken
self.quoteSignature = quoteSignature
self.externalTransactionId = externalTransactionId
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
guard clientToken != nil, quoteSignature != nil else {
fatalError("Call configure(clientToken:quoteSignature:) before presenting")
}
var params: [String: String] = [
"flow": "buy",
"clientToken": clientToken,
"quoteSignature": quoteSignature,
"channelId": channelId,
]
if let externalTransactionId = externalTransactionId {
params["externalTransactionId"] = externalTransactionId
}
loadFrame(path: "/platform/v1/widget", params: params)
}
}
// MARK: - MoonPayFrameDelegate
extension MoonPayWidgetViewController: MoonPayFrameDelegate {
func frameDidReceiveMessage(_ message: [String: Any]) {
guard let kind = message["kind"] as? String else { return }
switch kind {
case "ready":
widgetDelegate?.widgetDidBecomeReady()
case "transactionCreated":
let payload = message["payload"] as? [String: Any] ?? [:]
let transaction = payload["transaction"] as? [String: Any] ?? [:]
widgetDelegate?.widgetDidCreateTransaction(
transactionId: transaction["id"] as? String ?? "",
status: transaction["status"] as? String ?? ""
)
case "complete":
handleComplete(message["payload"] as? [String: Any] ?? [:])
case "error":
let payload = message["payload"] as? [String: Any] ?? [:]
widgetDelegate?.widgetDidError(
code: payload["code"] as? String ?? "unknown",
message: payload["message"] as? String ?? "Unknown error"
)
default:
break
}
}
func frameDidCompleteHandshake() {
// Handshake complete — widget loading
}
private func handleComplete(_ payload: [String: Any]) {
guard let transaction = payload["transaction"] as? [String: Any],
let status = transaction["status"] as? String else { return }
if status == "failed" {
let reason = transaction["failureReason"] as? String ?? "Transaction failed"
widgetDelegate?.widgetDidFail(reason: reason)
} else if let transactionId = transaction["id"] as? String {
widgetDelegate?.widgetDidComplete(transactionId: transactionId, status: status)
}
}
}
```
### Usage
```swift theme={null}
class WidgetPaymentViewController: UIViewController, WidgetFrameDelegate {
private var widgetVC: MoonPayWidgetViewController?
override func viewDidLoad() {
super.viewDidLoad()
setupWidget()
}
private func setupWidget() {
let widgetVC = MoonPayWidgetViewController()
widgetVC.configure(
clientToken: CredentialsManager.shared.clientToken!,
quoteSignature: currentQuote.signature
)
widgetVC.widgetDelegate = self
addChild(widgetVC)
widgetVC.view.frame = view.bounds
widgetVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(widgetVC.view)
widgetVC.didMove(toParent: self)
self.widgetVC = widgetVC
}
// MARK: - WidgetFrameDelegate
func widgetDidBecomeReady() {
print("Widget loaded and visible")
}
func widgetDidCreateTransaction(transactionId: String, status: String) {
print("Transaction created: \(transactionId) (\(status))")
// Customer may still need to complete 3-D Secure
}
func widgetDidComplete(transactionId: String, status: String) {
print("Transaction \(transactionId) status: \(status)")
// Navigate to transaction status screen
}
func widgetDidFail(reason: String) {
showAlert(title: "Payment Failed", message: reason)
}
func widgetDidError(code: String, message: String) {
showAlert(title: "Error", message: message)
}
}
```
# Overview
Source: https://dev.moonpay.com/platform/guides/manual-integration/overview
Integrate MoonPay frames without using the SDK.
The MoonPay SDK handles frame lifecycle, `postMessage` communication, and encryption automatically. If you're building for a platform where the SDK isn't available or you prefer a direct integration, use the guides in this section.
Before you start, familiarize yourself with how [frames](/platform/frames) work in this integration including their lifecycle, messaging patterns, and events.
## When to integrate manually
You should integrate manually when:
* **You prefer not to bundle third-party SDKs.** If your application has strict requirements around third-party dependencies, you can integrate directly using platform-standard protocols and APIs.
* **You're building for a platform without SDK support.** We currently provide SDKs for [web](/platform/sdk-reference/web/overview) and [React Native](/platform/sdk-reference/react-native/overview). For native iOS (Swift), Android (Kotlin), and Flutter, you can integrate directly.
**Need an SDK for your platform?** SDK support is expanding. Contact us if you
need an SDK for iOS, Android, Flutter, or another platform.
## Platform guides
The guides below are **reference implementations**, not production-ready starter projects. They illustrate the messaging protocol, encryption flow, and frame lifecycle for each platform. Review and adapt the code to fit your app's architecture, error handling, and security requirements before shipping.
For web and React Native, the SDK is strongly recommended, but you can follow the guides below for a direct integration.
Check out the platform-specific guides:
Use iframes in desktop and mobile browsers
Use `react-native-webview`
Use `WKWebView` in Swift
Use `WebView` in Kotlin
# React Native
Source: https://dev.moonpay.com/platform/guides/manual-integration/react-native
Manual frame integration for React Native using react-native-webview.
Use `react-native-webview` to embed frames in React Native applications. The WebView communicates with your app through the `ReactNativeWebView` JavaScript interface.
Read the [manual integration
overview](/platform/guides/manual-integration/overview) for core concepts
before you continue.
## Setup
The `react-native-webview` library automatically injects the
`ReactNativeWebView` JavaScript interface into the WebView. The frame detects
this interface and uses it for communication between the frame and your app.
You don't need to inject a custom JavaScript bridge.
### Dependencies
Install `react-native-webview` to render frames in your app.
```bash pnpm theme={null}
pnpm i react-native-webview
```
```bash bun theme={null}
bun add react-native-webview
```
```bash npm theme={null}
npm i react-native-webview
```
If targeting iOS, you may also need to run:
```bash theme={null}
cd ios && pod install
```
```bash theme={null}
npx expo install react-native-webview
```
See the [Expo WebView docs](https://docs.expo.dev/versions/latest/sdk/webview/) for additional platform configuration.
The connect and check frames require X25519 key exchange to encrypt client credentials. The examples below use [noble-curves](https://github.com/paulmillr/noble-curves), but you can use any library that supports X25519 and AES-GCM. You also need a [polyfill for `getRandomValues`](https://github.com/LinusU/react-native-get-random-values) ([MDN](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)) which is only available in browsers.
```bash pnpm theme={null}
pnpm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```bash bun theme={null}
bun add react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
```bash npm theme={null}
npm i react-native-get-random-values @noble/curves @noble/hashes @noble/ciphers
```
### Base WebView component
Create a reusable base component for frame communication:
```tsx twoslash MoonPayWebView.tsx theme={null}
import React, {
useRef,
useCallback,
useImperativeHandle,
forwardRef,
} from "react";
import { View, ViewStyle, StyleSheet } from "react-native";
import { WebView, WebViewMessageEvent } from "react-native-webview";
type MoonPayWebViewProps = {
url: string;
channelId: string;
onMessage: (data: FrameMessage) => void;
onHandshake: () => void;
style?: ViewStyle;
};
export type MoonPayWebViewRef = {
sendMessage: (kind: string, payload?: object) => void;
};
export type FrameMessage = {
version: number;
meta: { channelId: string };
kind: string;
payload?: unknown;
};
export const MoonPayWebView = forwardRef<
MoonPayWebViewRef,
MoonPayWebViewProps
>(({ url, channelId, onMessage, onHandshake, style }, ref) => {
const webViewRef = useRef(null);
const sendMessage = useCallback(
(kind: string, payload?: object) => {
const message = {
version: 2,
meta: { channelId },
kind,
...(payload && { payload }),
};
webViewRef.current?.postMessage(JSON.stringify(message));
},
[channelId],
);
useImperativeHandle(ref, () => ({ sendMessage }), [sendMessage]);
const handleMessage = useCallback(
(event: WebViewMessageEvent) => {
try {
const data: FrameMessage = JSON.parse(event.nativeEvent.data);
if (data.meta?.channelId !== channelId) return;
if (data.kind === "handshake") {
sendMessage("ack");
onHandshake();
}
onMessage(data);
} catch {
// Ignore malformed messages
}
},
[channelId, sendMessage, onMessage, onHandshake],
);
return (
);
});
const styles = StyleSheet.create({
container: { flex: 1 },
webview: { flex: 1 },
});
```
### Encryption utility
When using the connect or check frame, you generate an X25519 keypair and provide the public key to the frame. The frame encrypts the returned client credentials with this key.
#### Key generation
Generate an X25519 keypair for secure communication:
```ts crypto.ts theme={null}
import { x25519 } from "@noble/curves/ed25519";
import { bytesToHex } from "@noble/hashes/utils";
import "react-native-get-random-values"; // Polyfill for crypto.getRandomValues
export function generateKeyPair() {
const { secretKey, publicKey } = x25519.keygen();
return {
privateKeyHex: bytesToHex(secretKey),
publicKeyHex: bytesToHex(publicKey),
};
}
export function generateChannelId() {
return `ch_${Date.now()}_${Math.random().toString(36).slice(2)}`;
}
```
#### Decryption utility
Decrypt the encrypted credentials returned from connect/check frames:
```ts decrypt.ts theme={null}
import { x25519 } from "@noble/curves/ed25519";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { gcm } from "@noble/ciphers/aes";
import { hexToBytes } from "@noble/hashes/utils";
interface ClientCredentials {
accessToken: string;
clientToken: string;
}
export function decryptClientCredentials(
encryptedValue: string,
privateKeyHex: string,
): ClientCredentials {
const encrypted = JSON.parse(encryptedValue) as {
ephemeralPublicKey: string;
iv: string;
ciphertext: string;
};
const sharedSecret = x25519.getSharedSecret(
hexToBytes(privateKeyHex),
hexToBytes(encrypted.ephemeralPublicKey),
);
const derivedKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const aes = gcm(derivedKey, hexToBytes(encrypted.iv));
const decrypted = aes.decrypt(hexToBytes(encrypted.ciphertext));
const text = new TextDecoder().decode(decrypted);
return JSON.parse(text) as ClientCredentials;
}
```
***
## Check frame
The check frame verifies whether a customer already has an active connection. It's headless — no UI is rendered. See [check frame reference](/platform/frames/check) for event details.
### Check component
```tsx theme={null}
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import {
generateKeyPair,
generateChannelId,
decryptClientCredentials,
} from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface CheckFrameProps {
sessionToken: string;
onActive: (credentials: { accessToken: string; clientToken: string }) => void;
onConnectionRequired: (credentials: {
accessToken: string;
clientToken: string;
}) => void;
onError: (error: { code: string; message: string }) => void;
onPending?: () => void;
onUnavailable?: () => void;
}
export function MoonPayCheckFrame({
sessionToken,
onActive,
onConnectionRequired,
onError,
onPending,
onUnavailable,
}: CheckFrameProps) {
const [channelId] = useState(generateChannelId);
const [keyPair] = useState(generateKeyPair);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/check-connection?${new URLSearchParams(
{
sessionToken,
publicKey: keyPair.publicKeyHex,
channelId,
},
).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
const payload = data.payload as {
status: string;
credentials?: string;
expiresAt?: string;
customer?: {
id: string;
country: string;
administrativeArea?: string;
area?: string;
};
/** Present only when the session's email and phone number resolve to different MoonPay customers. */
mismatch?: boolean;
reason?: string;
};
switch (payload.status) {
case "active":
const credentials = decryptClientCredentials(
payload.credentials!,
keyPair.privateKeyHex,
);
// Read payload.customer.country, payload.customer.administrativeArea,
// and payload.customer.area to determine geo-based disclosure requirements.
// payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.
onActive(credentials);
break;
case "connectionRequired":
const anonymousCredentials = decryptClientCredentials(
payload.credentials!,
keyPair.privateKeyHex,
);
// payload.mismatch is true when the session's email and phone resolve to different customers — route through the connect flow first.
onConnectionRequired(anonymousCredentials);
break;
case "pending":
onPending?.();
break;
case "unavailable":
onUnavailable?.();
break;
case "failed":
onError({
code: "failed",
message: payload.reason || "Check failed",
});
break;
}
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[
keyPair,
onActive,
onConnectionRequired,
onError,
onPending,
onUnavailable,
],
);
return (
{}}
/>
);
}
```
### Usage
```tsx theme={null}
import { MoonPayCheckFrame } from "./MoonPayCheckFrame";
function SplashScreen({ navigation }) {
return (
{
// Customer is already connected — skip to payment
console.log("Already connected!", { accessToken, clientToken });
navigation.navigate("Payment");
}}
onConnectionRequired={({ accessToken, clientToken }) => {
// Store both tokens in memory, then pass clientToken to the connect frame
CredentialsStore.set({ accessToken, clientToken });
navigation.navigate("Connect", { clientToken });
}}
onError={(error) => {
console.error("Check failed:", error);
}}
/>
);
}
```
***
## Connect frame
The connect frame establishes a customer connection to your application. See [connect frame reference](/platform/frames/connect) for event details.
### Connect component
```tsx theme={null}
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import {
generateKeyPair,
generateChannelId,
decryptClientCredentials,
} from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface ConnectFrameProps {
clientToken: string;
onComplete: (credentials: {
accessToken: string;
clientToken: string;
}) => void;
onError: (error: { code: string; message: string }) => void;
onPending?: () => void;
onUnavailable?: () => void;
}
export function MoonPayConnectFrame({
clientToken,
onComplete,
onError,
onPending,
onUnavailable,
}: ConnectFrameProps) {
const [channelId] = useState(generateChannelId);
const [keyPair] = useState(generateKeyPair);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/connect?${new URLSearchParams({
clientToken,
publicKey: keyPair.publicKeyHex,
channelId,
}).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
const payload = data.payload as {
status: string;
credentials?: string;
expiresAt?: string;
customer?: {
id: string;
country: string;
administrativeArea?: string;
area?: string;
};
reason?: string;
};
switch (payload.status) {
case "active":
const credentials = decryptClientCredentials(
payload.credentials!,
keyPair.privateKeyHex,
);
// Read payload.customer.country, payload.customer.administrativeArea,
// and payload.customer.area to determine geo-based disclosure requirements.
// payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.
onComplete(credentials);
break;
case "pending":
onPending?.();
break;
case "unavailable":
onUnavailable?.();
break;
case "failed":
onError({
code: "failed",
message: payload.reason || "Connection failed",
});
break;
}
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[keyPair, onComplete, onError, onPending, onUnavailable],
);
return (
{}}
/>
);
}
```
### Usage
```tsx theme={null}
import { MoonPayConnectFrame } from "./MoonPayConnectFrame";
function ConnectScreen({ route, navigation }) {
// The clientToken is the anonymous token passed from the check frame's
// `connectionRequired` response.
const { clientToken } = route.params;
const handleComplete = ({ accessToken, clientToken }) => {
// Replace the anonymous credentials with the authenticated ones and store
// them in memory (e.g., React Context or state management).
console.log("Connected!", { accessToken, clientToken });
navigation.navigate("Home");
};
const handleError = (error) => {
console.error("Connection failed:", error);
// Show error UI
};
return (
console.log("Connection pending")}
onUnavailable={() => console.log("Connection unavailable")}
/>
);
}
```
***
## Apple Pay frame
The Apple Pay frame renders the Apple Pay button and handles the payment flow. See [Apple Pay frame reference](/platform/frames/apple-pay) for event details.
### Apple Pay component
```tsx theme={null}
import React, {
useState,
useCallback,
useRef,
useImperativeHandle,
forwardRef,
} from "react";
import {
MoonPayWebView,
MoonPayWebViewRef,
FrameMessage,
} from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface ApplePayFrameProps {
clientToken: string;
quoteSignature: string;
externalTransactionId?: string;
onComplete: (transaction: { id: string; status: string }) => void;
onChallenge: (url: string) => void;
onError: (error: { code: string; message: string }) => void;
onQuoteExpired: () => void;
onReady?: () => void;
}
export type ApplePayFrameRef = {
updateQuote: (signature: string) => void;
};
export const MoonPayApplePayFrame = forwardRef<
ApplePayFrameRef,
ApplePayFrameProps
>(
(
{
clientToken,
quoteSignature,
externalTransactionId,
onComplete,
onChallenge,
onError,
onQuoteExpired,
onReady,
},
ref,
) => {
const frameRef = useRef(null);
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/apple-pay?${new URLSearchParams(
{
clientToken,
channelId,
signature: quoteSignature,
...(externalTransactionId && { externalTransactionId }),
},
).toString()}`;
useImperativeHandle(
ref,
() => ({
updateQuote: (signature: string) => {
frameRef.current?.sendMessage("setQuote", {
quote: { signature },
});
},
}),
[],
);
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "ready":
onReady?.();
break;
case "complete":
const payload = data.payload as {
transaction:
| { id: string; status: string }
| { status: "failed"; failureReason: string };
};
if (payload.transaction.status === "failed") {
onError({
code: "transactionFailed",
message: (payload.transaction as { failureReason: string })
.failureReason,
});
} else {
onComplete(payload.transaction as { id: string; status: string });
}
break;
case "challenge":
const challengePayload = data.payload as {
kind: string;
url: string;
};
onChallenge(challengePayload.url);
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
onQuoteExpired();
} else {
onError(error);
}
break;
}
},
[onComplete, onChallenge, onError, onQuoteExpired, onReady],
);
return (
{}}
style={{ height: 56 }}
/>
);
},
);
```
### Usage
```tsx theme={null}
import { useRef } from "react";
import { MoonPayApplePayFrame, ApplePayFrameRef } from "./MoonPayApplePayFrame";
function PaymentScreen({ clientToken, quoteSignature }) {
const applePayRef = useRef(null);
const handleQuoteExpired = async () => {
// Fetch a new quote and send it to the frame
const newQuote = await fetchNewQuote();
applePayRef.current?.updateQuote(newQuote.signature);
};
return (
console.log("Transaction initiated:", tx.id)}
onChallenge={(url) => console.log("Challenge required:", url)}
onError={(error) => console.error("Payment error:", error)}
onQuoteExpired={handleQuoteExpired}
onReady={() => console.log("Apple Pay button ready")}
/>
);
}
```
***
## Google Pay frame
The Google Pay frame renders the Google Pay button and handles the payment flow. See [Google Pay frame reference](/platform/frames/google-pay) for event details.
### Google Pay component
```tsx theme={null}
import React, {
useState,
useCallback,
useRef,
useImperativeHandle,
forwardRef,
} from "react";
import {
MoonPayWebView,
MoonPayWebViewRef,
FrameMessage,
} from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface GooglePayFrameProps {
clientToken: string;
quoteSignature: string;
externalTransactionId?: string;
onComplete: (transaction: { id: string; status: string }) => void;
onChallenge: (url: string) => void;
onError: (error: { code: string; message: string }) => void;
onQuoteExpired: () => void;
onReady?: () => void;
}
export type GooglePayFrameRef = {
updateQuote: (signature: string) => void;
};
export const MoonPayGooglePayFrame = forwardRef<
GooglePayFrameRef,
GooglePayFrameProps
>(
(
{
clientToken,
quoteSignature,
externalTransactionId,
onComplete,
onChallenge,
onError,
onQuoteExpired,
onReady,
},
ref,
) => {
const frameRef = useRef(null);
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/google-pay?${new URLSearchParams(
{
clientToken,
channelId,
signature: quoteSignature,
...(externalTransactionId && { externalTransactionId }),
},
).toString()}`;
useImperativeHandle(
ref,
() => ({
updateQuote: (signature: string) => {
frameRef.current?.sendMessage("setQuote", {
quote: { signature },
});
},
}),
[],
);
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "ready":
onReady?.();
break;
case "complete":
const payload = data.payload as {
transaction:
| { id: string; status: string }
| { status: "failed"; failureReason: string };
};
if (payload.transaction.status === "failed") {
onError({
code: "transactionFailed",
message: (payload.transaction as { failureReason: string })
.failureReason,
});
} else {
onComplete(payload.transaction as { id: string; status: string });
}
break;
case "challenge":
const challengePayload = data.payload as {
kind: string;
url: string;
};
onChallenge(challengePayload.url);
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
onQuoteExpired();
} else {
onError(error);
}
break;
}
},
[onComplete, onChallenge, onError, onQuoteExpired, onReady],
);
return (
{}}
style={{ height: 56 }}
/>
);
},
);
```
### Usage
```tsx theme={null}
import { useRef } from "react";
import {
MoonPayGooglePayFrame,
GooglePayFrameRef,
} from "./MoonPayGooglePayFrame";
function PaymentScreen({ clientToken, quoteSignature }) {
const googlePayRef = useRef(null);
const handleQuoteExpired = async () => {
// Fetch a new quote and send it to the frame
const newQuote = await fetchNewQuote();
googlePayRef.current?.updateQuote(newQuote.signature);
};
return (
console.log("Transaction initiated:", tx.id)}
onChallenge={(url) => console.log("Challenge required:", url)}
onError={(error) => console.error("Payment error:", error)}
onQuoteExpired={handleQuoteExpired}
onReady={() => console.log("Google Pay button ready")}
/>
);
}
```
***
## Add Card frame
The add card frame lets a customer save a new card to their account. See [add card frame reference](/platform/frames/add-card) for event details.
### What you'll need
Before you initialize the add card frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
### Add Card component
```tsx theme={null}
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface CardResult {
id: string;
brand: string;
last4: string;
cardType: string;
expirationMonth: number;
expirationYear: number;
availability: { active: boolean };
}
interface AddCardFrameProps {
clientToken: string;
onComplete: (card: CardResult) => void;
onError: (error: { code: string; message: string }) => void;
onReady?: () => void;
}
export function MoonPayAddCardFrame({
clientToken,
onComplete,
onError,
onReady,
}: AddCardFrameProps) {
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/add-card?${new URLSearchParams({
clientToken,
channelId,
}).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "ready":
onReady?.();
break;
case "complete":
const payload = data.payload as { card: CardResult };
onComplete(payload.card);
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[onComplete, onError, onReady],
);
return (
{}}
style={{ flex: 1 }}
/>
);
}
```
### Usage
```tsx theme={null}
import { MoonPayAddCardFrame } from "./MoonPayAddCardFrame";
function SaveCardScreen({ clientToken, navigation }) {
return (
{
console.log("Card saved:", card.id, card.brand, card.last4);
navigation.navigate("Payment");
}}
onError={(error) => {
console.error("Add card error:", error);
}}
onReady={() => console.log("Add card frame ready")}
/>
);
}
```
***
## Buy frame
The buy frame processes a card or bank-transfer payment for a quote. It is headless — rendered at zero size — while the customer completes payment. For cards, if 3-D Secure is required, the frame emits a `challenge` event with a URL you open in a separate challenge frame. For bank transfers (SEPA, EUR), quote with `paymentMethod.type` set to `"sepa"`; the `complete` event returns a transaction that stays `pending` and carries a `bankTransferDepositInfo` object you render natively so the customer can send the deposit. See the [buy frame reference](/platform/frames/buy) for event details, and [Pay with bank transfer](/platform/guides/pay-with-bank-transfer) for the full bank-transfer walkthrough.
For bank transfers, the customer must include the payment `reference` from
`bankTransferDepositInfo` with their transfer, or it is rejected. Render the
deposit details, including the reference, in your own UI. See the [transaction
object](/api-reference/platform/objects-and-types/transaction#bank-transfer-deposit-info).
### What you'll need
Before you initialize the buy frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Buy component
```tsx theme={null}
import React, {
useState,
useCallback,
useRef,
useImperativeHandle,
forwardRef,
} from "react";
import { StyleSheet } from "react-native";
import {
MoonPayWebView,
MoonPayWebViewRef,
FrameMessage,
} from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface BuyFrameProps {
clientToken: string;
quoteSignature: string;
externalTransactionId?: string;
onComplete: (transaction: { id: string; status: string }) => void;
onChallenge: (url: string) => void;
onError: (error: { code: string; message: string }) => void;
onQuoteExpired?: () => void;
}
export type BuyFrameRef = {
updateQuote: (signature: string) => void;
};
export const MoonPayBuyFrame = forwardRef(
(
{
clientToken,
quoteSignature,
externalTransactionId,
onComplete,
onChallenge,
onError,
onQuoteExpired,
},
ref,
) => {
const frameRef = useRef(null);
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/buy?${new URLSearchParams({
clientToken,
channelId,
signature: quoteSignature,
...(externalTransactionId && { externalTransactionId }),
}).toString()}`;
useImperativeHandle(
ref,
() => ({
updateQuote: (signature: string) => {
frameRef.current?.sendMessage("setQuote", {
quote: { signature },
});
},
}),
[],
);
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
onComplete(
(data.payload as { transaction: { id: string; status: string } })
.transaction,
);
break;
case "challenge":
const challenge = data.payload as { kind: string; url: string };
onChallenge(challenge.url);
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
onQuoteExpired?.();
} else {
onError(error);
}
break;
}
},
[onComplete, onChallenge, onError, onQuoteExpired],
);
return (
{}}
style={StyleSheet.absoluteFillObject}
/>
);
},
);
```
### Challenge component
```tsx theme={null}
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface ChallengeFrameProps {
challengeUrl: string;
clientToken: string;
onComplete: (transaction: { id: string; status: string }) => void;
onCancelled: () => void;
onError: (error: { code: string; message: string }) => void;
}
export function MoonPayChallengeFrame({
challengeUrl,
clientToken,
onComplete,
onCancelled,
onError,
}: ChallengeFrameProps) {
const [channelId] = useState(generateChannelId);
const frameUrl = useMemo(() => {
const url = new URL(challengeUrl);
url.searchParams.set("clientToken", clientToken);
url.searchParams.set("channelId", channelId);
return url.toString();
}, [challengeUrl, clientToken, channelId]);
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "complete":
const payload = data.payload as {
flow: "buy";
transaction: { id: string; status: string };
};
onComplete(payload.transaction);
break;
case "cancelled":
onCancelled();
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[onComplete, onCancelled, onError],
);
return (
{}}
/>
);
}
```
### Usage
```tsx theme={null}
import { useRef, useState } from "react";
import { View, StyleSheet } from "react-native";
import { MoonPayBuyFrame, BuyFrameRef } from "./MoonPayBuyFrame";
import { MoonPayChallengeFrame } from "./MoonPayChallengeFrame";
function BuyPaymentScreen({ clientToken, quoteSignature, navigation }) {
const buyRef = useRef(null);
const [challengeUrl, setChallengeUrl] = useState(null);
const handleComplete = (transaction: { id: string; status: string }) => {
setChallengeUrl(null);
console.log("Transaction initiated:", transaction.id);
navigation.navigate("TransactionStatus", { transactionId: transaction.id });
};
const handleChallengeResult = (transaction: {
id: string;
status: string;
}) => {
setChallengeUrl(null);
console.log("Transaction initiated:", transaction.id);
navigation.navigate("TransactionStatus", { transactionId: transaction.id });
};
const handleQuoteExpired = async () => {
const newQuote = await fetchNewQuote();
buyRef.current?.updateQuote(newQuote.signature);
};
return (
setChallengeUrl(url)}
onError={(error) => console.error("Buy error:", error)}
onQuoteExpired={handleQuoteExpired}
/>
{challengeUrl && (
setChallengeUrl(null)}
onError={(error) => {
console.error("Challenge error:", error);
setChallengeUrl(null);
}}
/>
)}
);
}
```
***
## Widget frame
For payment methods beyond Apple Pay — including credit/debit cards, Google Pay, bank transfers, and more — use the [widget frame](/platform/frames/widget). It renders the full MoonPay buy experience inside a WebView, including payment collection and transaction confirmation. See [pay with widget](/platform/guides/pay-with-widget) for a full walkthrough.
### Widget component
```tsx theme={null}
import React, { useState, useCallback } from "react";
import { MoonPayWebView, FrameMessage } from "./MoonPayWebView";
import { generateChannelId } from "./crypto";
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface WidgetFrameProps {
clientToken: string;
quoteSignature: string;
externalTransactionId?: string;
onTransactionCreated?: (transaction: { id: string; status: string }) => void;
onComplete: (transaction: { id: string; status: string }) => void;
onError: (error: { code: string; message: string }) => void;
onReady?: () => void;
}
export function MoonPayWidgetFrame({
clientToken,
quoteSignature,
externalTransactionId,
onTransactionCreated,
onComplete,
onError,
onReady,
}: WidgetFrameProps) {
const [channelId] = useState(generateChannelId);
const frameUrl = `${FRAME_ORIGIN}/platform/v1/widget?${new URLSearchParams({
flow: "buy",
clientToken,
quoteSignature,
channelId,
...(externalTransactionId && { externalTransactionId }),
}).toString()}`;
const handleMessage = useCallback(
(data: FrameMessage) => {
switch (data.kind) {
case "ready":
onReady?.();
break;
case "transactionCreated":
const created = data.payload as {
transaction: { id: string; status: string };
};
onTransactionCreated?.(created.transaction);
break;
case "complete":
const payload = data.payload as {
transaction:
| { id: string; status: string }
| { status: "failed"; failureReason: string };
};
if (payload.transaction.status === "failed") {
onError({
code: "transactionFailed",
message: (payload.transaction as { failureReason: string })
.failureReason,
});
} else {
onComplete(payload.transaction as { id: string; status: string });
}
break;
case "error":
onError(data.payload as { code: string; message: string });
break;
}
},
[onComplete, onError, onTransactionCreated, onReady],
);
return (
{}}
/>
);
}
```
### Usage
```tsx theme={null}
import { MoonPayWidgetFrame } from "./MoonPayWidgetFrame";
function WidgetPaymentScreen({ clientToken, quoteSignature }) {
return (
console.log("Widget loaded")}
onTransactionCreated={(tx) =>
console.log("Transaction created:", tx.id, tx.status)
}
onComplete={(tx) => console.log("Transaction complete:", tx.id)}
onError={(error) => console.error("Widget error:", error)}
/>
);
}
```
# Web
Source: https://dev.moonpay.com/platform/guides/manual-integration/web
Integrate MoonPay iframes directly in your application.
Use iframes and `postMessage` to embed frames directly in web applications without installing any MoonPay packages.
Read the [manual integration
overview](/platform/guides/manual-integration/overview) for core concepts
before you continue.
## Setup
### Encryption
The connect and check frames require X25519 key exchange to encrypt client credentials. The examples below use [@noble/curves](https://github.com/paulmillr/noble-curves), but you can use any library that supports X25519 and AES-GCM.
```sh pnpm theme={null}
pnpm i @noble/curves @noble/hashes @noble/ciphers
```
```sh bun theme={null}
bun add @noble/curves @noble/hashes @noble/ciphers
```
```sh npm theme={null}
npm i @noble/curves @noble/hashes @noble/ciphers
```
### Message utilities
Create helper functions for sending and receiving frame messages. All messages follow the [frames protocol](/platform/frames/overview#frames-protocol).
```ts messageUtils.ts theme={null}
const FRAME_ORIGIN = "https://platform.moonpay.com";
interface FrameMessage {
version: number;
meta: { channelId: string };
kind: string;
payload?: unknown;
}
function parseFrameMessage(
event: MessageEvent,
channelId: string,
): FrameMessage | null {
if (event.origin !== FRAME_ORIGIN) return null;
try {
const data: FrameMessage =
typeof event.data === "string" ? JSON.parse(event.data) : event.data;
if (data.meta?.channelId !== channelId) return null;
return data;
} catch {
return null;
}
}
function sendFrameMessage(
iframe: HTMLIFrameElement,
channelId: string,
kind: string,
payload?: object,
) {
const message: FrameMessage = {
version: 2,
meta: { channelId },
kind,
...(payload && { payload }),
};
iframe.contentWindow?.postMessage(JSON.stringify(message), FRAME_ORIGIN);
}
```
***
## Encryption utility
The connect and check frames return encrypted credentials. Generate an X25519 keypair and provide the public key to the frame.
### Key generation
```ts crypto.ts theme={null}
import { x25519 } from "@noble/curves/ed25519";
import { bytesToHex } from "@noble/hashes/utils";
function generateKeyPair() {
const { secretKey, publicKey } = x25519.keygen();
return {
privateKeyHex: bytesToHex(secretKey),
publicKeyHex: bytesToHex(publicKey),
};
}
```
### Decryption
```ts decrypt.ts theme={null}
import { x25519 } from "@noble/curves/ed25519";
import { hkdf } from "@noble/hashes/hkdf";
import { sha256 } from "@noble/hashes/sha256";
import { gcm } from "@noble/ciphers/aes";
import { hexToBytes } from "@noble/hashes/utils";
interface ClientCredentials {
accessToken: string;
clientToken: string;
}
function decryptClientCredentials(
encryptedBase64: string,
privateKeyHex: string,
): ClientCredentials {
const json = JSON.parse(atob(encryptedBase64)) as {
ephemeralPublicKey: string;
iv: string;
ciphertext: string;
};
const sharedSecret = x25519.getSharedSecret(
hexToBytes(privateKeyHex),
hexToBytes(json.ephemeralPublicKey),
);
const derivedKey = hkdf(sha256, sharedSecret, undefined, undefined, 32);
const aes = gcm(derivedKey, hexToBytes(json.iv));
const decrypted = aes.decrypt(hexToBytes(json.ciphertext));
const text = new TextDecoder().decode(decrypted);
return JSON.parse(text) as ClientCredentials;
}
```
***
## Check frame
The check frame verifies whether a customer already has an active connection. It's headless — no UI is rendered. Use it to skip the connect flow for returning customers. See [check frame reference](/platform/frames/check) for event details.
### Initialize the frame
```ts theme={null}
const FRAME_ORIGIN = "https://platform.moonpay.com";
function initializeCheckFrame(sessionToken: string) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const channelId = crypto.randomUUID();
const keyPair = generateKeyPair();
const params = new URLSearchParams({
sessionToken,
publicKey: keyPair.publicKeyHex,
channelId,
});
iframe.src = `${FRAME_ORIGIN}/platform/v1/check-connection?${params.toString()}`;
return { iframe, channelId, keyPair };
}
```
### Handle events
```ts theme={null}
interface CheckCompletePayload {
status:
| "active"
| "connectionRequired"
| "pending"
| "unavailable"
| "failed";
credentials?: string;
expiresAt?: string;
customer?: {
id: string;
country: string;
administrativeArea?: string;
area?: string;
};
capabilities?: {
ramps: {
requirements: {
/** @deprecated Use customer.country, customer.administrativeArea, and customer.area instead */
paymentDisclosures?: {
country: string;
administrativeArea?: string;
area?: string;
};
};
};
};
/** Present only when the session's email and phone number resolve to different MoonPay customers. */
mismatch?: boolean;
reason?: string;
}
function setupCheckListener(channelId: string, privateKeyHex: string) {
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, channelId);
if (!data) return;
const iframe = document.getElementById(
"moonpay-frame",
) as HTMLIFrameElement;
switch (data.kind) {
case "handshake":
sendFrameMessage(iframe, channelId, "ack");
break;
case "complete":
handleCheckComplete(
data.payload as CheckCompletePayload,
privateKeyHex,
);
break;
case "error":
const error = data.payload as { code: string; message: string };
console.error("Check error:", error.code, error.message);
break;
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}
function handleCheckComplete(
payload: CheckCompletePayload,
privateKeyHex: string,
) {
switch (payload.status) {
case "active":
const credentials = decryptClientCredentials(
payload.credentials!,
privateKeyHex,
);
// Store credentials in memory for subsequent API calls and frames
console.log("Already connected!");
// Read payload.customer.country, payload.customer.administrativeArea,
// and payload.customer.area to determine geo-based disclosure requirements.
// payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.
break;
case "connectionRequired":
const anonymousCredentials = decryptClientCredentials(
payload.credentials!,
privateKeyHex,
);
// Store both tokens in memory, then pass clientToken to the connect frame
console.log("No active connection — show connect frame");
// payload.mismatch is true when the session's email and phone resolve to different customers — route through the connect flow first.
break;
case "pending":
console.log("Connection pending — customer may need to complete KYC");
break;
case "unavailable":
console.log("Connection unavailable — likely geo-restricted");
break;
case "failed":
console.error("Check failed:", payload.reason);
break;
}
}
```
### Usage
```ts theme={null}
const { channelId, keyPair } = initializeCheckFrame("your-session-token");
const cleanup = setupCheckListener(channelId, keyPair.privateKeyHex);
// When done, clean up the listener
// cleanup();
```
***
## Connect frame
The connect frame establishes a customer connection to your application. See [connect frame reference](/platform/frames/connect) for event details.
### Initialize the frame
```ts theme={null}
const FRAME_ORIGIN = "https://platform.moonpay.com";
function initializeConnectFrame(clientToken: string) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const channelId = crypto.randomUUID();
const keyPair = generateKeyPair();
const params = new URLSearchParams({
clientToken,
publicKey: keyPair.publicKeyHex,
channelId,
});
iframe.src = `${FRAME_ORIGIN}/platform/v1/connect?${params.toString()}`;
return { iframe, channelId, keyPair };
}
```
### Handle events
```ts theme={null}
interface ConnectCompletePayload {
status: "active" | "pending" | "unavailable" | "failed";
credentials?: string;
expiresAt?: string;
customer?: {
id: string;
country: string;
administrativeArea?: string;
area?: string;
};
capabilities?: {
ramps: {
requirements: {
/** @deprecated Use customer.country, customer.administrativeArea, and customer.area instead */
paymentDisclosures?: {
country: string;
administrativeArea?: string;
area?: string;
};
};
};
};
reason?: string;
}
function setupConnectListener(channelId: string, privateKeyHex: string) {
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, channelId);
if (!data) return;
const iframe = document.getElementById(
"moonpay-frame",
) as HTMLIFrameElement;
switch (data.kind) {
case "handshake":
sendFrameMessage(iframe, channelId, "ack");
break;
case "complete":
handleConnectComplete(
data.payload as ConnectCompletePayload,
privateKeyHex,
);
break;
case "error":
const error = data.payload as { code: string; message: string };
console.error("Connect error:", error.code, error.message);
break;
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}
function handleConnectComplete(
payload: ConnectCompletePayload,
privateKeyHex: string,
) {
switch (payload.status) {
case "active":
const credentials = decryptClientCredentials(
payload.credentials!,
privateKeyHex,
);
// Store tokens in memory for subsequent API calls and frames
console.log("Connected!");
// Read payload.customer.country, payload.customer.administrativeArea,
// and payload.customer.area to determine geo-based disclosure requirements.
// payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.
break;
case "pending":
console.log("Connection pending — customer may need to complete KYC");
break;
case "unavailable":
console.log("Connection unavailable — likely geo-restricted");
break;
case "failed":
console.error("Connection failed:", payload.reason);
break;
}
}
```
### Usage
```ts theme={null}
// The clientToken is obtained from the check frame's `connectionRequired` response
const { channelId, keyPair } = initializeConnectFrame("your-client-token");
const cleanup = setupConnectListener(channelId, keyPair.privateKeyHex);
// When done, clean up the listener
// cleanup();
```
***
## Apple Pay frame
The Apple Pay frame renders the Apple Pay button and handles the payment flow. See [Apple Pay frame reference](/platform/frames/apple-pay) for event details.
Apple Pay only works on Safari (macOS and iOS). Check availability before
rendering.
### What you'll need
Before you initialize the Apple Pay frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Initialize the frame
```ts theme={null}
function initializeApplePayFrame(
clientToken: string,
quoteSignature: string,
externalTransactionId?: string,
) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const channelId = crypto.randomUUID();
const params = new URLSearchParams({
clientToken,
channelId,
signature: quoteSignature,
...(externalTransactionId && { externalTransactionId }),
});
iframe.src = `${FRAME_ORIGIN}/platform/v1/apple-pay?${params.toString()}`;
return { iframe, channelId };
}
```
### Handle events
```ts theme={null}
interface ApplePayCompletePayload {
transaction:
| { id: string; status: "complete" | "pending" }
| { status: "failed"; failureReason: string };
}
function setupApplePayListener(channelId: string) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, channelId);
if (!data) return;
switch (data.kind) {
case "handshake":
sendFrameMessage(iframe, channelId, "ack");
break;
case "ready":
console.log("Apple Pay button ready");
break;
case "complete":
const payload = data.payload as ApplePayCompletePayload;
if (payload.transaction.status === "failed") {
console.error(
"Transaction failed:",
payload.transaction.failureReason,
);
} else {
console.log("Transaction initiated:", payload.transaction.id);
// Poll for transaction status or wait for webhook
}
break;
case "challenge":
const challengePayload = data.payload as { kind: string; url: string };
// Open the challenge URL in a new iframe — see "Challenge handling" below
console.log("Challenge required:", challengePayload.url);
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
// Fetch a new quote and send it to the frame
console.log("Quote expired, fetching new quote...");
// updateApplePayQuote(iframe, channelId, newQuoteSignature);
} else {
console.error("Apple Pay error:", error.code, error.message);
}
break;
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}
```
### Update the quote
When the quote expires or changes, send a new quote to the frame:
```ts theme={null}
function updateApplePayQuote(
iframe: HTMLIFrameElement,
channelId: string,
newQuoteSignature: string,
) {
sendFrameMessage(iframe, channelId, "setQuote", {
quote: { signature: newQuoteSignature },
});
}
```
***
## Google Pay frame
The Google Pay frame renders the Google Pay button and handles the payment flow. See [Google Pay frame reference](/platform/frames/google-pay) for event details.
The Google Pay frame requires the `payment` [permission
policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy#iframes).
When using a sandboxed iframe, include `allow-scripts`, `allow-popups`,
`allow-same-origin`, and `allow-forms`. See [Google Pay inside sandboxed
iframe](https://developers.googleblog.com/google-pay-inside-sandboxed-iframe-for-pci-dss-v4-compliance/)
for details.
### What you'll need
Before you initialize the Google Pay frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Initialize the frame
```ts theme={null}
function initializeGooglePayFrame(
clientToken: string,
quoteSignature: string,
externalTransactionId?: string,
) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const channelId = crypto.randomUUID();
// Required for Google Pay — enables the Payment Request API inside the iframe
iframe.allow = "payment";
const params = new URLSearchParams({
clientToken,
channelId,
signature: quoteSignature,
...(externalTransactionId && { externalTransactionId }),
});
iframe.src = `${FRAME_ORIGIN}/platform/v1/google-pay?${params.toString()}`;
return { iframe, channelId };
}
```
### Handle events
```ts theme={null}
interface GooglePayCompletePayload {
transaction:
| { id: string; status: "complete" | "pending" }
| { status: "failed"; failureReason: string };
}
function setupGooglePayListener(channelId: string) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, channelId);
if (!data) return;
switch (data.kind) {
case "handshake":
sendFrameMessage(iframe, channelId, "ack");
break;
case "ready":
console.log("Google Pay button ready");
break;
case "complete":
const payload = data.payload as GooglePayCompletePayload;
if (payload.transaction.status === "failed") {
console.error(
"Transaction failed:",
payload.transaction.failureReason,
);
} else {
console.log("Transaction initiated:", payload.transaction.id);
// Poll for transaction status or wait for webhook
}
break;
case "challenge":
const challengePayload = data.payload as { kind: string; url: string };
// Open the challenge URL in a new iframe — see "Challenge handling" below
console.log("Challenge required:", challengePayload.url);
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
// Fetch a new quote and send it to the frame
console.log("Quote expired, fetching new quote...");
// updateGooglePayQuote(iframe, channelId, newQuoteSignature);
} else {
console.error("Google Pay error:", error.code, error.message);
}
break;
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}
```
### Update the quote
When the quote expires or changes, send a new quote to the frame:
```ts theme={null}
function updateGooglePayQuote(
iframe: HTMLIFrameElement,
channelId: string,
newQuoteSignature: string,
) {
sendFrameMessage(iframe, channelId, "setQuote", {
quote: { signature: newQuoteSignature },
});
}
```
***
## Add Card frame
The add card frame lets a customer save a new card to their account. See [add card frame reference](/platform/frames/add-card) for event details.
### What you'll need
Before you initialize the add card frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
### Initialize the frame
```ts theme={null}
function initializeAddCardFrame(clientToken: string) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const channelId = crypto.randomUUID();
const params = new URLSearchParams({
clientToken,
channelId,
});
iframe.src = `${FRAME_ORIGIN}/platform/v1/add-card?${params.toString()}`;
return { iframe, channelId };
}
```
### Handle events
```ts theme={null}
interface AddCardCompletePayload {
card: {
id: string;
brand: string;
last4: string;
cardType: string;
expirationMonth: number;
expirationYear: number;
availability: { active: boolean };
};
}
function setupAddCardListener(channelId: string) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, channelId);
if (!data) return;
switch (data.kind) {
case "handshake":
sendFrameMessage(iframe, channelId, "ack");
break;
case "ready":
console.log("Add card frame ready");
break;
case "complete":
const payload = data.payload as AddCardCompletePayload;
console.log("Card saved:", payload.card.id);
// Poll for card status or proceed to payment
break;
case "error":
const error = data.payload as { code: string; message: string };
console.error("Add card error:", error.code, error.message);
break;
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}
```
### Usage
```ts theme={null}
const { channelId } = initializeAddCardFrame("your-client-token");
const cleanup = setupAddCardListener(channelId);
// When done, clean up the listener
// cleanup();
```
***
## Buy frame
The buy frame processes a card or bank-transfer payment for a quote. It is headless — rendered at zero size — while the customer completes payment. For cards, if 3-D Secure is required, the frame emits a `challenge` event with a URL you open in a separate challenge frame. For bank transfers (SEPA, EUR), quote with `paymentMethod.type` set to `"sepa"`; the `complete` event returns a transaction that stays `pending` and carries a `bankTransferDepositInfo` object you render natively so the customer can send the deposit. See the [buy frame reference](/platform/frames/buy) for event details, and [Pay with bank transfer](/platform/guides/pay-with-bank-transfer) for the full bank-transfer walkthrough.
For bank transfers, the customer must include the payment `reference` from
`bankTransferDepositInfo` with their transfer, or it is rejected. Render the
deposit details, including the reference, in your own UI. See the [transaction
object](/api-reference/platform/objects-and-types/transaction#bank-transfer-deposit-info).
### What you'll need
Before you initialize the buy frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Initialize the frame
```ts theme={null}
function initializeBuyFrame(
clientToken: string,
quoteSignature: string,
externalTransactionId?: string,
) {
const channelId = crypto.randomUUID();
const params = new URLSearchParams({
clientToken,
channelId,
signature: quoteSignature,
...(externalTransactionId && { externalTransactionId }),
});
const iframe = document.createElement("iframe");
iframe.style.cssText =
"position: absolute; width: 0; height: 0; border: none; overflow: hidden;";
iframe.src = `${FRAME_ORIGIN}/platform/v1/buy?${params.toString()}`;
document.body.appendChild(iframe);
return { iframe, channelId };
}
```
### Handle events
```ts theme={null}
interface BuyCompletePayload {
transaction: { id: string; status: string };
}
interface BuyChallengePayload {
kind: string;
url: string;
}
function setupBuyListener(
channelId: string,
iframe: HTMLIFrameElement,
onComplete: (transaction: { id: string; status: string }) => void,
onChallenge: (url: string) => void,
) {
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, channelId);
if (!data) return;
switch (data.kind) {
case "handshake":
sendFrameMessage(iframe, channelId, "ack");
break;
case "ready":
console.log("Buy frame ready");
break;
case "complete":
const payload = data.payload as BuyCompletePayload;
console.log("Transaction complete:", payload.transaction.id);
onComplete(payload.transaction);
break;
case "challenge":
const challenge = data.payload as BuyChallengePayload;
onChallenge(challenge.url);
break;
case "error":
const error = data.payload as { code: string; message: string };
if (error.code === "quoteExpired") {
// Fetch a new quote and send it to the frame
console.log("Quote expired, fetching new quote...");
// updateBuyQuote(iframe, channelId, newQuoteSignature);
} else {
console.error("Buy error:", error.code, error.message);
}
break;
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}
```
### Update the quote
When the quote expires or changes, send a new quote to the frame:
```ts theme={null}
function updateBuyQuote(
iframe: HTMLIFrameElement,
channelId: string,
newQuoteSignature: string,
) {
sendFrameMessage(iframe, channelId, "setQuote", {
quote: { signature: newQuoteSignature },
});
}
```
### Challenge handling
When the buy frame emits a `challenge` event, open the challenge URL in a new iframe inside a modal. The challenge frame is self-driving after the handshake:
```ts theme={null}
interface ChallengeCompletePayload {
flow: "buy";
transaction: { id: string; status: string };
}
function setupChallengeListener(
challengeChannelId: string,
challengeIframe: HTMLIFrameElement,
buyIframe: HTMLIFrameElement,
buyCleanup: () => void,
onResult: (transaction: { id: string; status: string } | null) => void,
) {
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, challengeChannelId);
if (!data) return;
switch (data.kind) {
case "handshake":
sendFrameMessage(challengeIframe, challengeChannelId, "ack");
break;
case "ready":
console.log("Challenge frame ready");
break;
case "complete":
const payload = data.payload as ChallengeCompletePayload;
cleanup();
onResult(payload.transaction);
break;
case "cancelled":
cleanup();
onResult(null);
break;
case "error":
const error = data.payload as { code: string; message: string };
console.error("Challenge error:", error.code, error.message);
cleanup();
onResult(null);
break;
}
};
function cleanup() {
window.removeEventListener("message", handler);
buyCleanup();
challengeIframe.remove();
buyIframe.remove();
}
window.addEventListener("message", handler);
return cleanup;
}
function openChallengeFrame(
challengeUrl: string,
clientToken: string,
buyIframe: HTMLIFrameElement,
buyCleanup: () => void,
onResult: (transaction: { id: string; status: string } | null) => void,
) {
const modal = document.getElementById("challengeModal") as HTMLElement;
const challengeChannelId = crypto.randomUUID();
const urlWithParams = new URL(challengeUrl);
urlWithParams.searchParams.set("clientToken", clientToken);
urlWithParams.searchParams.set("channelId", challengeChannelId);
const challengeIframe = document.createElement("iframe");
challengeIframe.src = urlWithParams.toString();
modal.appendChild(challengeIframe);
return setupChallengeListener(
challengeChannelId,
challengeIframe,
buyIframe,
buyCleanup,
onResult,
);
}
```
### Usage
```ts theme={null}
const { iframe: buyIframe, channelId } = initializeBuyFrame(
"your-client-token",
"your-quote-signature",
);
const buyCleanup = setupBuyListener(
channelId,
buyIframe,
(transaction) => {
console.log("Transaction initiated:", transaction.id);
// Navigate to transaction status screen or poll for updates
},
(challengeUrl) => {
openChallengeFrame(
challengeUrl,
"your-client-token",
buyIframe,
buyCleanup,
(transaction) => {
if (transaction) {
console.log("Challenge complete:", transaction.id);
} else {
console.log("Challenge cancelled or failed");
}
},
);
},
);
// When done, clean up the listener
// buyCleanup();
```
***
## Widget frame
For payment methods beyond Apple Pay — including credit/debit cards, Google Pay, bank transfers, and more — use the [widget frame](/platform/frames/widget). It renders the full MoonPay buy experience inside an iframe, including payment collection and transaction confirmation. See [pay with widget](/platform/guides/pay-with-widget) for a full walkthrough.
### What you'll need
Before you initialize the widget frame, you need:
1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction
### Initialize the frame
The widget iframe requires the `payment` [permission
policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy#iframes)
to process payments.
```ts theme={null}
function initializeWidgetFrame(
clientToken: string,
quoteSignature: string,
externalTransactionId?: string,
) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const channelId = crypto.randomUUID();
// The widget requires the "payment" permission policy
iframe.allow = "payment";
const params = new URLSearchParams({
flow: "buy",
clientToken,
quoteSignature,
channelId,
...(externalTransactionId && { externalTransactionId }),
});
iframe.src = `${FRAME_ORIGIN}/platform/v1/widget?${params.toString()}`;
return { iframe, channelId };
}
```
### Handle events
```ts theme={null}
interface WidgetCompletePayload {
transaction:
| { id: string; status: "complete" | "pending" }
| { status: "failed"; failureReason: string };
}
function setupWidgetListener(channelId: string) {
const iframe = document.getElementById("moonpay-frame") as HTMLIFrameElement;
const handler = (event: MessageEvent) => {
const data = parseFrameMessage(event, channelId);
if (!data) return;
switch (data.kind) {
case "handshake":
sendFrameMessage(iframe, channelId, "ack");
break;
case "ready":
console.log("Widget loaded and visible");
break;
case "transactionCreated":
const created = data.payload as {
transaction: { id: string; status: string };
};
console.log("Transaction created:", created.transaction.id);
// Customer may still need to complete 3-D Secure
break;
case "complete":
const payload = data.payload as WidgetCompletePayload;
if (payload.transaction.status === "failed") {
console.error(
"Transaction failed:",
payload.transaction.failureReason,
);
} else {
console.log("Transaction initiated:", payload.transaction.id);
// Poll for transaction status or wait for webhook
}
break;
case "error":
const error = data.payload as { code: string; message: string };
console.error("Widget error:", error.code, error.message);
break;
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}
```
### Usage
```ts theme={null}
const { channelId } = initializeWidgetFrame(
"your-client-token",
"your-quote-signature",
);
const cleanup = setupWidgetListener(channelId);
// When done, clean up the listener
// cleanup();
```
# Choose an onboarding path
Source: https://dev.moonpay.com/platform/guides/onboarding-paths
Decide how customers onboard: hosted by MoonPay, via the Customer API, or deferred with guest checkout.
MoonPay always decides what verification a customer needs. The connection status, the customer's `kyc.status` and requirements, and the session's capabilities tell you exactly what is outstanding. You choose who renders the UI that captures it: MoonPay, your own screens, or no one until a purchase demands it.
| If you want to… | Choose | You build | MoonPay renders |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------- | ----------------------------------------------- |
| Ship fastest, with MoonPay handling onboarding UI | [Hosted onboarding](/platform/guides/connect-a-customer) (connect flow) | A session server and one embedded frame | Login and the full KYC flow |
| Own the onboarding UX end to end, or reuse KYC data you already capture | [Onboarding via API](/platform/guides/customer-api) (Customer API) | Your own KYC screens and API calls | An OTP login frame and fallback challenges only |
| Let new customers buy instantly with no upfront account | [Guest checkout](/platform/guides/guest-checkout) | Payment UI only | The payment sheet and step-up challenges |
## Every path shares the same backbone
Whichever path you choose, the integration follows the same sequence:
1. Create a session on your server with your secret key.
2. Check the connection in an invisible frame on your client.
3. Route on the returned status and capabilities. This is the only point where the paths diverge.
4. Accept payments with the [payment method](/platform/guides/payment-methods) that fits your app.
Every path also shares the same fallback. When MoonPay needs more from the customer than the current step captures, you receive a challenge URL and render the hosted [Challenge frame](/platform/frames/challenge). See [Handle challenges](/platform/guides/handling-challenges).
## Hosted onboarding
Hosted onboarding is the fastest path to production. When the connection check returns `connectionRequired`, you render the co-branded connect frame. MoonPay guides the customer through login and the full KYC flow. When the flow completes, you receive authenticated credentials for payment calls. You build no verification UI. Follow the [Hosted onboarding](/platform/guides/connect-a-customer) guide for the full walkthrough.
## Onboarding via API
You keep onboarding in your own screens and drive verification through the Customer API. The [Auth frame](/platform/frames/auth) handles login only: the customer verifies a one-time passcode (OTP) and you receive authenticated credentials, with no KYC UI. From there you read `kyc.status` and its requirements, capture the outstanding data in your own screens, and submit it with the API. The fields and documents each country requires live in [KYC data requirements](/platform/guides/kyc-data-requirements). Follow the [Onboarding via API](/platform/guides/customer-api) guide for the full walkthrough.
## Guest checkout
Guest checkout defers verification until a purchase requires it. You must pass the customer's email address, phone number, and terms acceptance when you create the session. Without all three, the `guestCheckout` capability does not appear on the connection. MoonPay creates a guest account at transaction time. When a purchase needs identity confirmation or more details, the payment frame emits a step-up challenge for you to render. Availability constraints apply: the `guestCheckout` capability appears on the connection only for enabled accounts and supported regions. Returning customers are recognized and connect instead. Follow [Guest checkout](/platform/guides/guest-checkout) for the full walkthrough.
## Combine paths
You are not locked into one path:
* Guests upgrade to full accounts through the [connect flow](/platform/guides/connect-a-customer). Limits lift on the same account, with no migration.
* The API-driven path falls back to a hosted challenge when the submitted data alone isn't enough. You render the challenge URL and the customer finishes verification in MoonPay's UI.
* You can run different paths on different surfaces of your app. For example, use hosted onboarding in your consumer app and onboarding via API in an embedded flow.
## Next steps
Build the hosted path: one co-branded frame for login and KYC.
Build the API-driven path: your screens, MoonPay's verification decisions.
Build the deferred path: instant purchases with step-up challenges.
# Generate API clients
Source: https://dev.moonpay.com/platform/guides/openapi-codegen
Generate type-safe API clients from the OpenAPI specification.
The MoonPay Developer Platform API is defined using an OpenAPI 3.1 specification. You can use this spec to generate typed clients for any language.
## Full specification
The OpenAPI 3.1 specification is served at **[https://api.moonpay.com/platform/openapi.json](https://api.moonpay.com/platform/openapi.json)**. Use this URL in your codegen config or download the file for offline use.
## Usage
### TypeScript / JavaScript
Use `@hey-api/openapi-ts` to generate TypeScript types:
```bash theme={null}
npm install -D @hey-api/openapi-ts
```
```ts openapi-ts.config.ts theme={null}
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "https://api.moonpay.com/platform/openapi.json",
output: {
path: "./src/gen",
},
plugins: [
{
name: "@hey-api/typescript",
enums: false,
},
],
});
```
```bash theme={null}
npx openapi-ts
```
### Dart / Flutter
Use `openapi_generator` with build\_runner:
```yaml theme={null}
# pubspec.yaml
dev_dependencies:
build_runner: ^2.4.0
openapi_generator: ^5.0.0
openapi_generator:
input_spec:
path: https://api.moonpay.com/platform/openapi.json
generator_name: dart
output_directory: lib/gen
```
```bash theme={null}
flutter pub run build_runner build
```
### Other Languages
Use the [OpenAPI Generator](https://openapi-generator.tech/) CLI to generate clients for 50+ languages:
```bash theme={null}
# Install
npm install @openapitools/openapi-generator-cli -g
# Generate (examples)
openapi-generator-cli generate -i https://api.moonpay.com/platform/openapi.json -g kotlin -o ./gen
openapi-generator-cli generate -i https://api.moonpay.com/platform/openapi.json -g swift5 -o ./gen
openapi-generator-cli generate -i https://api.moonpay.com/platform/openapi.json -g python -o ./gen
openapi-generator-cli generate -i https://api.moonpay.com/platform/openapi.json -g go -o ./gen
```
See the [full list of generators](https://openapi-generator.tech/docs/generators).
# Pay with Apple Pay
Source: https://dev.moonpay.com/platform/guides/pay-with-apple-pay
Allow customers to buy crypto headlessly with Apple Pay.
Use this guide to execute a transaction with Apple Pay after you have a [connected customer](/platform/guides/connect-a-customer). To let new customers buy with Apple Pay before they have a MoonPay account, see [Guest checkout](/platform/guides/guest-checkout).
See the [Going Live](/platform/overview/going-live) section for details on the requirements you must meet before you can take this integration to production.
## Prerequisites
* A connected customer (via `client.getConnection()` or `client.connect()`).
* A UI surface where you can render the [Apple Pay frame](/platform/frames/apple-pay).
You can test the full Apple Pay flow without a real Apple Pay account by using
[test mode](/platform/overview/test-mode#apple-pay). The frame renders a mock
Apple Pay button that simulates successful and failed transactions.
## Flow overview
```mermaid theme={null}
sequenceDiagram
autonumber
actor C as Customer
participant FE as Your frontend
participant API as MoonPay API
participant APF as Apple Pay frame
participant CF as Challenge frame
Note over C,CF: Prerequisite: customer is connected
FE->>API: GET /platform/v1/payment-methods
API-->>FE: [{ type: "apple_pay", availability }]
C->>FE: Enters amount
FE->>API: POST /platform/v1/quotes/buy
API-->>FE: { quote with signature }
FE->>APF: Render Apple Pay frame (signature)
APF-->>FE: ready
FE->>C: Reveals Apple Pay button
C->>APF: Taps button, approves in native sheet
alt Happy path
APF-->>FE: complete({ transaction: { id, status } })
else Verification required
APF-->>FE: challenge({ url })
FE->>CF: Render Challenge frame at URL
CF-->>FE: complete({ transaction: { id, status } })
end
FE->>API: GET /platform/v1/transactions/{id}
Note over FE: Poll for final status
```
## Device and browser support
Apple Pay is available in Safari on macOS and in every iOS browser. The customer also needs a card set up in Apple Pay.
In other browsers, the frame reports Apple Pay as unavailable and renders nothing. It does not offer Apple's cross-device flow, where the page shows a QR code that the customer scans with their iPhone to approve the payment. The [widget](/platform/guides/pay-with-widget) does support the cross-device flow.
In a native iOS app, you can embed the [Apple Pay frame](/platform/frames/apple-pay) in a `WKWebView`. By default, `WKWebView` silently dismisses the JavaScript dialogs the frame relies on, so you must handle them through `WKUIDelegate`. See the [frame requirements](/platform/frames/apple-pay#wkwebview) and the [iOS manual integration guide](/platform/guides/manual-integration/ios#apple-pay-frame).
When Apple Pay isn't available in the customer's environment, the frame emits an `unsupported` event. Use it to build the experience that fits your product — for example, hide the Apple Pay option up front, or route customers who select Apple Pay to the widget with your `apple_pay` quote. The code sample in [Execute the transaction](#execute-the-transaction) handles this event.
## Display payment methods
Use the SDK or API to fetch and display the payment methods that are available for the customer right now.
```ts List payment methods theme={null}
// After connecting, list available payment methods
const paymentMethodsResult = await client.getPaymentMethods();
if (!paymentMethodsResult.ok) {
// Handle error
}
console.log(paymentMethodsResult.value);
```
```ts Result theme={null}
[
{
type: "apple_pay",
capabilities: {
supportedCurrencies: ["USD", "EUR", "GBP"],
supportedTransactionTypes: ["buy"],
},
availability: {
active: true,
},
},
];
```
## Get quotes
Quotes provide real-time prices and fees for transactions. Show the contextual
quote summary above the Apple Pay button. The Apple Pay sheet
presents fees, so you do not need to render a fee breakdown. See
[Going Live](/platform/overview/going-live#quote-presentation) for the required
presentation and [fee behavior](/platform/overview/core-concepts#fee-behavior)
for how fees relate to the source amount.
Only quotes with `executable: true` can be used to execute a transaction. See the [quotes API reference](/api-reference/platform/endpoints/quotes/get) for the fields required to receive `executable: true`.
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" }, // The fiat currency and amount to pay
destination: { asset: { code: "ETH" } }, // The crypto the customer will receive
wallet: { address: "0x1234..." }, // The destination wallet address
paymentMethod: { type: "apple_pay" }, // The payment method type
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value);
```
```ts Result theme={null}
{
source: {
amount: "100.00",
asset: {
code: "USD",
name: "US Dollar",
precision: 2
}
},
destination: {
amount: "0.025",
asset: {
code: "ETH",
name: "Ethereum",
precision: 18
}
},
fees: {
network: {
amount: "2.50",
currencyCode: "USD"
},
moonpay: {
amount: "3.99",
currencyCode: "USD"
}
},
wallet: {
address: "0x1234..."
},
paymentMethod: {
type: "apple_pay"
},
expiresAt: "2026-01-12T14:45:00Z",
executable: true,
signature: "eyJhbGciOiJFUzI1NiIs..."
}
```
## Execute the transaction
To execute a transaction, set up the payment flow based on the quote. Different payment methods have different requirements—you can configure each as needed to control your experience. For the frame URL, size, permissions, and events, see the [Apple Pay frame](/platform/frames/apple-pay) reference.
Some transactions require a challenge — the Apple Pay frame emits a
`challenge` event when extra verification is needed. See [Handle
challenges](/platform/guides/handling-challenges) for the full flow.
```ts Apple Pay theme={null}
import type { ApplePayEvent } from "@moonpay/platform-sdk-web";
// Render the Apple Pay frame into your UI and handle callbacks
const applePayResult = await client.setupApplePay({
quote: quoteResult.value.signature, // The quote signature from getQuote
container: document.querySelector("#applePayContainer"), // DOM element to render the button
onEvent: (event: ApplePayEvent) => {
switch (event.kind) {
case "ready":
// The frame is ready. Use this event to reveal the button if needed.
break;
case "complete": {
const txn = event.payload.transaction;
if (txn.status === "failed") {
// The transaction failed. Branch on failureCode for programmatic handling.
switch (txn.failureCode) {
case "authorizationDeclined":
// Prompt the customer to try a different card.
break;
case "serviceUnavailable":
case "applePayMerchantUnavailable":
// Retry after a short delay.
break;
default:
// Show txn.failureReason to the customer.
break;
}
break;
}
// The transaction is executing. Track the final status via polling and/or webhooks.
console.log(txn);
// { id: "txn_01", status: "pending" }
break;
}
case "challenge":
// Verification required. Render the challenge frame at the provided URL.
// See: /platform/guides/handling-challenges
console.log(event.payload.url);
break;
case "quoteExpired":
// Fetch a new quote, then pass its signature into the frame
// const newQuote = await client.getQuote({...});
// event.payload.setQuote(newQuote.value.signature);
break;
case "error":
// Depending on the error, you can have the customer pick a different payment method or retry.
console.error(event.payload.message);
break;
case "unsupported":
// Apple Pay isn't supported in the current environment.
break;
}
},
});
if (!applePayResult.ok) {
// Handle error setting up Apple Pay
}
// You can update the quote or dispose the frame later
// applePayResult.value.setQuote(newQuoteSignature);
// applePayResult.value.dispose();
```
## Transaction statuses
Transactions have the following statuses:
* **Pending:** The transaction has been initiated and the payment accepted. The assets are being transferred.
* **Complete:** The transaction is finalized. The payment is complete and the assets have been delivered to their destination.
* **Failed:** The transaction has failed. The payment was not executed and funds were not transferred.
# Pay with bank transfer
Source: https://dev.moonpay.com/platform/guides/pay-with-bank-transfer
Let customers buy crypto with a SEPA bank transfer and render the deposit details natively.
Use this guide to execute a bank-transfer transaction after you have a
[connected customer](/platform/guides/connect-a-customer). You get a quote,
execute it through the same headless [Buy frame](/platform/frames/buy) you use
for cards, then render the deposit details so the customer can send funds from
their bank. MoonPay runs the payment pipeline; you own the purchase UI and the
deposit screen.
Bank transfers support SEPA (EUR) for centralized assets. They don't cover DeFi
assets — offer a card or wallet method for those. See the
[Going Live](/platform/overview/going-live) section for requirements you must
meet before taking this integration to production.
## Prerequisites
* A MoonPay account with bank transfers enabled. Contact your MoonPay account
team to enable it.
* A connected customer (via `client.getConnection()` or `client.connect()`).
* A UI surface where you can render MoonPay frames (iframe on web, or
[WebView](/platform/overview/requirements#webviews) on mobile).
* A destination wallet address for the purchased crypto.
## Flow overview
```mermaid theme={null}
sequenceDiagram
autonumber
actor C as Customer
participant FE as Your frontend
participant API as MoonPay API
participant BF as Buy frame
Note over C,BF: Prerequisite: customer is connected
FE->>API: GET /platform/v1/payment-methods
API-->>FE: { paymentMethodConfigs (includes sepa) }
C->>FE: Enters amount, selects bank transfer
FE->>API: POST /platform/v1/quotes/buy (paymentMethod.type: "sepa")
API-->>FE: { quote, exchangeRateType: "floating" }
C->>FE: Confirms purchase
FE->>BF: Render buy frame (signature)
BF-->>FE: complete({ transaction: { id, status: "pending" } })
FE->>API: GET /platform/v1/transactions/{id}
API-->>FE: { bankTransferDepositInfo }
FE->>C: Render deposit details (IBAN, BIC, reference)
Note over FE,C: Customer sends the transfer (minutes to days), including the reference
loop Poll until terminal status
FE->>API: GET /platform/v1/transactions/{id}
end
Note over FE,API: Or subscribe to the transaction-updated webhook
```
Fetch the customer's available payment method types and check for `sepa`.
```ts List payment methods theme={null}
const paymentMethodsResult = await client.getPaymentMethods();
if (!paymentMethodsResult.ok) {
// Handle error
}
const sepaAvailable = paymentMethodsResult.value.data.paymentMethodConfigs.some(
(config) =>
config.type === "sepa" &&
config.availability.active &&
!config.capabilities.requiresWidget,
);
```
```ts Result theme={null}
{
paymentMethodConfigs: [
{
type: "sepa",
capabilities: {
supportedCurrencies: ["EUR"],
supportedTransactionTypes: ["buy"],
allowsDeletion: false,
requiresWidget: false,
},
availability: { active: true },
},
],
paymentMethods: [],
}
```
Show bank transfer in your payment method picker only when a `sepa` config is
present, `availability.active` is `true`, and `capabilities.requiresWidget` is
`false`. A `requiresWidget: true` bank transfer isn't eligible for the headless
flow and must complete in the MoonPay widget instead.
SEPA doesn't create a stored payment method, so it never appears in
`paymentMethods`. That array still lists the customer's stored cards, so it can
be non-empty even when the customer pays by bank transfer.
Request a quote with `paymentMethod.type` set to `"sepa"`.
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "EUR" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
paymentMethod: { type: "sepa" },
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value.data);
```
```ts Result theme={null}
{
source: { amount: "100.00", asset: { code: "EUR" } },
destination: { amount: "0.0234", asset: { code: "ETH" } },
fees: {
network: { amount: "1.20", currencyCode: "EUR" },
moonpay: { amount: "3.50", currencyCode: "EUR" }
},
wallet: { address: "0x1234..." },
paymentMethod: { type: "sepa" },
expiresAt: "2026-04-29T15:45:00Z",
executable: true,
exchangeRate: "4273.50",
exchangeRateType: "floating",
signature: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
Bank transfers are a floating payment method: the quote returns
`exchangeRateType: "floating"` and the crypto amount is an estimate until the
customer's funds settle. Render the estimated amount with a tilde (for example,
`~0.0234 ETH`) and tell the customer the final amount is set at settlement. See
[Exchange rate type](/platform/sdk-reference/web/get-quote#exchange-rate-type).
Show the contextual quote summary and fee breakdown before the customer
confirms. See [Going Live](/platform/overview/going-live#quote-presentation) for
the canonical requirements. Monitor `expiresAt` and refresh the quote before it
expires.
Call `client.setupBuy()` with the quote signature, exactly as you do for cards.
The headless frame creates the transaction and emits `complete` with the
transaction `id`. For the frame URL, parameters, and events, see the
[Buy frame](/platform/frames/buy) reference.
```ts setupBuy theme={null}
import type { BuyEvent } from "@moonpay/platform-sdk-web";
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
container: document.querySelector("#buyContainer"),
onEvent: (event: BuyEvent) => {
switch (event.kind) {
case "complete":
if (event.payload.transaction.status !== "failed") {
// Read the deposit details and render them natively (Step 4).
showDepositDetails(event.payload.transaction.id);
}
break;
case "challenge":
// Verification required — render the challenge frame at the URL.
openChallengeFrame(event.payload.url, buyResult);
break;
case "error":
console.error(event.payload.message);
break;
}
},
});
if (!buyResult.ok) {
// Handle error
}
```
A bank transfer can still require a challenge, such as a KYC step-up. Handle it
exactly as for cards, but route the result back into the deposit flow: the
Challenge frame creates the transaction and emits it on `complete`, so call
`showDepositDetails` with that transaction `id`. See
[Handle challenges](/platform/guides/handling-challenges).
```ts Challenge handling theme={null}
import type { ChallengeEvent } from "@moonpay/platform-sdk-web";
async function openChallengeFrame(
challengeUrl: string,
buyResult: SetupBuyResult,
) {
const url = new URL(challengeUrl);
url.searchParams.set("channelId", crypto.randomUUID());
const challengeResult = await client.setupChallenge({
challengeUrl: url.toString(),
container: document.querySelector("#challengeModal"),
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "complete":
// The challenge frame created the transaction; read the deposit details.
showDepositDetails(event.payload.transaction.id);
teardown();
break;
case "cancelled":
case "error":
teardown();
break;
}
},
});
// Dispose both frame handles once the flow reaches a terminal state.
function teardown() {
if (challengeResult.ok) challengeResult.value.dispose();
buyResult.value.dispose();
}
}
```
Call `client.getTransaction()` with the `id`. For a bank transfer, the response
includes a `bankTransferDepositInfo` object with the account details and the
payment reference, plus the exact fiat amount to transfer in `source.amount` (the
gross amount, including fees). Render these fields in your own UI so the customer
can pay from their banking app. MoonPay does not render the deposit UI.
```ts Read deposit details theme={null}
async function showDepositDetails(transactionId: string) {
const result = await client.getTransaction(transactionId);
if (!result.ok) {
// Handle error
}
const transaction = result.value.data;
renderBankTransferScreen({
depositInfo: transaction.bankTransferDepositInfo,
// source.amount is the exact fiat amount, including fees, the customer transfers.
amount: transaction.source.amount,
currency: transaction.source.asset.code,
});
}
```
`bankTransferDepositInfo` contains:
| Field | Type | Required | Description |
| ------------------ | -------- | -------- | ------------------------------------------------------------------ |
| `reference` | `string` | ✅ | The payment reference the customer must include with the transfer. |
| `recipientName` | `string` | ✅ | The name of the recipient that receives the funds. |
| `recipientAddress` | `string` | ✅ | The address of the recipient. |
| `iban` | `string` | | The IBAN. Present for SEPA (EUR). |
| `bic` | `string` | | The BIC / SWIFT code. Present for SEPA (EUR). |
| `bankName` | `string` | | The name of the receiving bank. |
| `bankAddress` | `string` | | The address of the receiving bank. |
The customer must include the payment `reference` with their bank transfer.
Transfers sent without it are rejected. Surface it prominently, for example,
"Always include your payment reference or your transfer will be rejected."
MoonPay uses the reference to match the incoming transfer to this transaction.
A bank transfer can take from a few minutes to a few days, and the transaction
stays `pending` until settlement. While it's pending, the transaction's
`destination.amount` is a floating estimate, the same estimate the quote
returns, so you don't need to retain the original quote. Render
`destination.amount` with a tilde and make clear the crypto amount is only an
estimate until the transfer lands. At settlement it becomes the final amount
delivered.
Poll `client.getTransaction()` to track status. Because settlement can run for
days, you can also subscribe to the
[`transaction-updated`](/api-reference/widget/webhooks/transaction-updated)
webhook instead of long-lived polling.
```ts Track the transaction theme={null}
async function pollTransaction(transactionId: string) {
const terminal = new Set(["completed", "failed"]);
while (true) {
const res = await client.getTransaction(transactionId);
if (!res.ok) throw new Error(res.error.message);
if (terminal.has(res.value.data.status)) return res.value.data.status;
await new Promise((r) => setTimeout(r, 3000));
}
}
```
Bank-transfer transactions have the following statuses:
* **Pending:** The transaction is waiting for the customer's deposit, or the
deposit has arrived and is being processed.
* **Complete:** The funds have been received and the crypto delivered to the
destination wallet.
* **Failed:** The transfer timed out or was cancelled.
The `status` stays `pending` from the moment the transaction is created until it
settles, so it doesn't tell you whether the deposit has arrived yet. For
finer-grained progress, read the `stages` array: each stage has a `kind` and a
`status`. When the `waiting_payment` stage's `status` is `"success"`, the
customer's deposit has arrived. Use that to move your UI from awaiting-transfer
to processing before the transaction reaches a terminal status.
Requotes and cancellations for bank transfers are handled by MoonPay's hosted
flow, not the headless flow. If the settled amount differs from the estimate,
the customer is emailed and completes the requote there. A cancellation
surfaces to your app as a `failed` transaction when you poll
`client.getTransaction()`.
Polling is the documented way to track status. If you already consume MoonPay
webhooks, you can also use the existing
[`transaction-updated`](/api-reference/widget/webhooks/transaction-updated)
webhook. See the [webhooks overview](/api-reference/widget/webhooks/overview).
# Pay with the buy button
Source: https://dev.moonpay.com/platform/guides/pay-with-buy-button
Let customers buy crypto from a single express checkout button that handles payment-method selection and confirmation for you.
Use this guide to add an express checkout button to your app. The buy button is a
MoonPay-hosted button that, on tap, lets the customer pick a payment method
(Apple Pay, Google Pay, or card), confirm the purchase in a hosted sheet, and
complete the transaction — all from one piece of UI.
Set up the button after you have a [connected
customer](/platform/guides/connect-a-customer). MoonPay handles payment-method
selection, the confirmation sheet, and any verification challenges inside hosted
frames, so card data and sensitive verification never touch your domain.
See the [Going Live](/platform/overview/going-live) section for requirements you
must meet before taking this integration to production.
## When to use the buy button
MoonPay gives you three ways to run a buy. Pick the one that matches how much of
the experience you want to own.
| Approach | What you build | Best for |
| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| **Buy button** (`setupBuyButton()`) | A container. MoonPay renders the button, the payment-method picker, and the confirmation sheet. | A fast, low-effort express checkout where you want a single ready-made payment button. |
| **Headless buy** (`setupBuy()`) | Your own button, amount entry, and confirmation screen. The frame is headless and emits events. | Full control over the purchase UI while MoonPay runs the pipeline behind the scenes. |
| **Widget** ([`pay-with-widget`](/platform/guides/pay-with-widget)) | Nothing — MoonPay renders the entire flow, from amount entry to confirmation. | The quickest path to a complete buy experience when you do not need a custom UI. |
The key difference between the buy button and headless buy is the UI surface. The
buy button renders a visible button and confirmation sheet for you. Headless buy
renders no UI at all — you supply the button and confirmation screen and call
[`setupBuy()`](/platform/sdk-reference/web/setup-buy) when the customer confirms.
Both run the same buy pipeline and emit the same events.
## Prerequisites
* A connected customer (via `client.getConnection()` or `client.connect()`).
See [Connect a customer](/platform/guides/connect-a-customer).
* A UI surface where you can render the [buy button frame](/platform/frames/buy-button).
* A destination wallet address for the purchased crypto.
## Flow overview
```mermaid theme={null}
sequenceDiagram
autonumber
actor C as Customer
participant FE as Your frontend
participant API as MoonPay API
participant BB as Buy button frame
participant CF as Challenge frame
Note over C,CF: Prerequisite: customer is connected
C->>FE: Enters amount
FE->>API: getQuote(...)
API-->>FE: { quote with signature }
FE->>BB: setupBuyButton({ quote, container })
Note over BB: Button renders into your container
C->>BB: Taps the button, picks a method, confirms
alt Happy path
BB-->>FE: complete({ transaction: { id, status } })
else Verification required
BB-->>FE: challenge({ url })
FE->>CF: Render challenge frame at URL
CF-->>FE: complete({ transaction: { id, status } })
end
FE->>API: getTransaction(id)
Note over FE: Poll for final status
```
Request a quote for the amount the customer wants to spend. Pass the destination
wallet and the asset the customer will receive. You do not pass a payment method
here — the customer picks one inside the buy button.
Only quotes with `executable: true` can be used to execute a transaction. See the
[quotes API reference](/api-reference/platform/endpoints/quotes/get) for the
fields required to receive `executable: true`.
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" }, // The fiat currency and amount to pay
destination: { asset: { code: "ETH" } }, // The crypto the customer will receive
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" }, // The destination wallet address
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value);
```
```ts Result theme={null}
{
source: {
amount: "100.00",
asset: { code: "USD" }
},
destination: {
amount: "0.025",
asset: { code: "ETH" }
},
fees: {
network: { amount: "2.50", currencyCode: "USD" },
moonpay: { amount: "3.99", currencyCode: "USD" }
},
wallet: { address: "0x1234..." },
expiresAt: "2026-06-17T15:45:00Z",
executable: true,
signature: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
On the same confirmation view, before the buy button, you display the
contextual quote summary and fee breakdown. You remain responsible for this
presentation during the normal hosted flow. See [Going
Live](/platform/overview/going-live#quote-presentation) for the canonical
requirements and [fee behavior](/platform/overview/core-concepts#fee-behavior)
for how fees relate to the source amount. Monitor `expiresAt` and refresh the
quote before it expires. If the button is already rendered, call
`buyButton.setQuote(newSignature)` instead of re-creating it. See [Refresh an
expiring quote](#refresh-an-expiring-quote).
Pass the quote `signature` and a container element to
[`client.setupBuyButton()`](/platform/sdk-reference/web/setup-buy-button). The
frame renders a payment button — card, Apple Pay, or Google Pay, depending on
what's available to the customer — into your container. On tap, the frame opens
the confirmation sheet and runs the buy pipeline.
For the frame URL, parameters, and events, see the [buy button
frame](/platform/frames/buy-button) reference.
```ts Render the buy button theme={null}
import type { BuyButtonEvent } from "@moonpay/platform-sdk-web";
const buyButtonResult = await client.setupBuyButton({
quote: quoteResult.value.signature, // The quote signature from getQuote
container: document.querySelector("#buyButtonContainer"), // DOM element to render the button into
onEvent: (event: BuyButtonEvent) => {
switch (event.kind) {
case "ready":
// The button is rendered — hide any loading placeholder.
break;
case "buttonPressed":
// Customer tapped the button — show a loading state or fire analytics.
break;
case "complete": {
const txn = event.payload.transaction;
if (txn.status === "failed") {
// The transaction failed. Show txn.failureReason to the customer.
console.error(txn.failureReason);
break;
}
// The transaction is executing. Track the final status by polling.
pollTransaction(txn.id);
break;
}
case "challenge":
// Verification required. Render the challenge frame at the provided URL.
// See: /platform/guides/handling-challenges
openChallengeFrame(event.payload.url, buyButtonResult.value);
break;
case "error":
// Surface to logs and tear down the frame.
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!buyButtonResult.ok) {
// Handle error setting up the buy button
console.error(buyButtonResult.error.kind, buyButtonResult.error.message);
return;
}
const buyButton = buyButtonResult.value;
```
Like [`setupBuy()`](/platform/sdk-reference/web/setup-buy), the buy button
emits a `ready` event once the button is rendered and ready to tap. For Apple
Pay and Google Pay, it fires after the customer's device is confirmed to
support the wallet.
`onEvent` receives [`BuyButtonEvent`](/platform/sdk-reference/web/setup-buy-button#buybuttonevent)
events as the pipeline progresses. Handle each `kind`:
* **`ready`** — the button is rendered and ready for the customer to tap. Use it
to hide any loading placeholder.
* **`buttonPressed`** — the customer tapped the pay button, before the payment
sheet appears. An intent-to-buy signal with no payload. Use it to show a
loading state or fire analytics. It is not a purchase outcome — it still fires
if the customer then cancels, so listen for `complete` for the result.
* **`complete`** — the pipeline finished. The payload carries a
`transaction`. Inspect its `status` first: when `status` is `"failed"`, read
`failureReason` and show it to the customer; otherwise pass `transaction.id`
to [`getTransaction()`](/platform/sdk-reference/web/get-transaction) to poll
for the final status.
* **`challenge`** — verification is required before the transaction can proceed.
Render the [challenge frame](/platform/frames/challenge) at the `url` from the
payload. Do not construct the URL yourself.
* **`error`** — the flow encountered an error. Log `code` and `message`, then
tear down the frame. The `message` is for logs, not for display to customers.
When the buy button emits a `challenge` event, the customer must complete one or
more verification steps before the transaction can proceed. Set up the challenge
frame with the URL from the event payload. For the full flow, see [Handle
challenges](/platform/guides/handling-challenges).
The challenge frame is self-driving: after initialization, it sequences through
all required verification steps, creates the transaction, and emits `complete`
when the pipeline finishes.
```ts Handle challenges theme={null}
import type { ChallengeEvent } from "@moonpay/platform-sdk-web";
async function openChallengeFrame(challengeUrl: string, buyButton) {
// The challenge URL does not include a channelId — append one before rendering.
const url = new URL(challengeUrl);
url.searchParams.set("channelId", crypto.randomUUID());
const challengeResult = await client.setupChallenge({
challengeUrl: url.toString(),
container: document.querySelector("#challengeModal"),
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "ready":
// Challenge UI is rendered and visible
break;
case "complete":
// All verification resolved, transaction complete.
buyButton.dispose();
pollTransaction(event.payload.transaction.id);
break;
case "cancelled":
// Customer dismissed the challenge — allow retry.
buyButton.dispose();
showRetryOption();
break;
case "error":
buyButton.dispose();
console.error(event.payload.message);
break;
}
},
});
}
```
When you receive `complete`, `cancelled`, or `error` from the challenge frame,
call `buyButton.dispose()` to also tear down the buy button.
The challenge frame handles all verification types automatically — KYC, Strong
Customer Authentication (SCA), CVC re-entry, wallet ownership,
micro-authorization, and 3D Secure (3DS). You never need to distinguish between
them.
When the buy button or challenge frame emits `complete`, the payload includes a
`transaction` with an `id` and `status`. The transaction is created and payment
is processing. Poll for the final status with
[`getTransaction()`](/platform/sdk-reference/web/get-transaction).
```ts Track the transaction theme={null}
async function pollTransaction(transactionId: string) {
const terminal = new Set(["completed", "failed"]);
while (true) {
const res = await client.getTransaction(transactionId);
if (!res.ok) throw new Error(res.error.message);
if (terminal.has(res.value.data.status)) return res.value.data.status;
await new Promise((r) => setTimeout(r, 3000));
}
}
```
You can also track transaction status with the existing
[transaction-updated](/api-reference/widget/webhooks/transaction-updated)
webhook.
## Refresh an expiring quote
Quotes expire. If the current quote expires before the customer taps the button,
fetch a new one and call `setQuote()` with its `signature` instead of
re-rendering the frame.
```ts Refresh the quote theme={null}
const newQuote = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
});
if (newQuote.ok) {
buyButton.setQuote(newQuote.value.signature);
}
```
When you no longer need the button, call `buyButton.dispose()` to unmount the
frame. After you dispose it, no further events reach your `onEvent` callback.
## Transaction statuses
Transactions have the following statuses:
* **Pending:** The transaction has been initiated and the payment accepted. The
assets are being transferred.
* **Complete:** The transaction is finalized. The payment is complete and the
assets have been delivered to their destination.
* **Failed:** The transaction has failed. The payment was not executed and funds
were not transferred.
## Next steps
Full method signature, parameters, events, and errors.
Frame URL, parameters, and protocol messages.
Connect a customer's MoonPay account before you start a buy.
Render the challenge frame when verification is required.
# Pay with card
Source: https://dev.moonpay.com/platform/guides/pay-with-card
Allow customers to buy crypto using stored credit and debit cards.
Use this guide to execute a card transaction after you have a [connected
customer](/platform/guides/connect-a-customer). You will list stored cards, add
new ones through a MoonPay-hosted frame, get a quote, and execute the
transaction — with MoonPay handling PCI-compliant card collection, payment
orchestration, and all verification challenges inside hosted frames.
See the [Going Live](/platform/overview/going-live) section for requirements you
must meet before taking this integration to production.
## Prerequisites
* A MoonPay account with card payments enabled. Contact your MoonPay account
team to enable it.
* A connected customer (via `client.getConnection()` or `client.connect()`).
* A UI surface where you can render MoonPay frames (iframe on web, or
[WebView](/platform/overview/requirements#webviews) on mobile).
* A destination wallet address for the purchased crypto.
## Flow overview
```mermaid theme={null}
sequenceDiagram
autonumber
actor C as Customer
participant FE as Your frontend
participant API as MoonPay API
participant ACF as Add Card frame
participant BF as Buy frame
participant CF as Challenge frame
Note over C,CF: Prerequisite: customer is connected
FE->>API: GET /platform/v1/payment-methods
API-->>FE: { paymentMethodConfigs, paymentMethods }
alt No stored cards
FE->>ACF: Render Add Card frame
ACF-->>FE: complete({ card: { id, brand, last4, ... } })
end
C->>FE: Selects card, enters amount
FE->>API: POST /platform/v1/quotes/buy
API-->>FE: { quote with signature }
C->>FE: Confirms purchase
FE->>BF: Render buy frame (signature, clientToken)
alt Happy path
BF-->>FE: complete({ transaction: { id, status } })
else Verification required
BF-->>FE: challenge({ url })
FE->>CF: Render challenge frame at URL
CF-->>FE: complete({ transaction: { id, status } })
end
FE->>API: GET /platform/v1/transactions/{id}
Note over FE: Poll for final status
```
Fetch the customer's available payment method types and stored cards.
```ts List payment methods theme={null}
const paymentMethodsResult = await client.getPaymentMethods();
if (!paymentMethodsResult.ok) {
// Handle error
}
console.log(paymentMethodsResult.value.data);
```
```ts Result theme={null}
{
paymentMethodConfigs: [
{
type: "card",
capabilities: {
supportedCurrencies: ["USD", "EUR", "GBP"],
supportedTransactionTypes: ["buy"],
allowsDeletion: true,
requiresWidget: false,
},
availability: { active: true },
},
],
paymentMethods: [
{
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
type: "card",
brand: "visa",
last4: "4242",
expirationMonth: "12",
expirationYear: "2027",
cardType: "credit",
availability: { active: true },
},
],
}
```
The result contains two collections:
* **`paymentMethodConfigs`** — available payment method types. Check for
`type: "card"` to confirm card payments are available for this customer.
* **`paymentMethods`** — the customer's stored cards. Each entry includes
`brand`, `last4`, `expirationMonth`, `expirationYear`, `cardType`, and
`availability`.
Display active cards in your payment method picker. For inactive cards
(`availability.active: false`), show the `reasons` value and prompt the user to
add a new card.
| `reasons` value | Suggested UX |
| --------------- | ------------------------------------------------ |
| `card_expired` | "This card has expired. Add a new card." |
| `card_blocked` | "This card is no longer available." |
| `card_declined` | "This card can't be used. Try a different card." |
If `paymentMethods` is empty and `paymentMethodConfigs` includes `type:
"card"`, use the [Add a card](#add-a-card) flow.
When the customer needs to add a new card, set up the Add Card frame. The frame
collects card details and billing address inside a PCI-compliant MoonPay-hosted
UI — card data never touches your domain. For the frame URL, size, and events,
see the [Add Card frame](/platform/frames/add-card) reference.
```ts Add a card theme={null}
import type { AddCardEvent } from "@moonpay/platform-sdk-web";
const addCardResult = await client.setupAddCard({
container: document.querySelector("#addCardContainer"),
onEvent: (event: AddCardEvent) => {
switch (event.kind) {
case "ready":
// Frame rendered — reveal the modal if it was hidden
break;
case "complete":
// Card added. Use event.payload.card.id to get a quote.
console.log(event.payload.card);
// { id, brand, last4, cardType, expirationMonth, expirationYear }
break;
case "error":
console.error(event.payload.message);
break;
}
},
});
if (!addCardResult.ok) {
// Handle error setting up the Add Card frame
}
```
The `complete` event returns the new card's full details including its `id`. Use
this `id` to [get a quote](#get-a-quote). You do not need to fetch payment
methods again.
With a stored card selected, request a quote. Pass the card's `id` in
`paymentMethod` so MoonPay can evaluate card-specific requirements.
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
paymentMethod: { type: "card", id: cardId },
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value);
```
```ts Result theme={null}
{
source: {
amount: "100.00",
asset: { code: "USD" }
},
destination: {
amount: "0.025",
asset: { code: "ETH" }
},
fees: {
network: { amount: "2.50", currencyCode: "USD" },
moonpay: { amount: "3.99", currencyCode: "USD" }
},
wallet: { address: "0x1234..." },
paymentMethod: { type: "card", id: "a1b2c3d4-..." },
expiresAt: "2026-04-29T15:45:00Z",
executable: true,
signature: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
On the same confirmation view, show the contextual quote summary and fee
breakdown before the customer submits the card payment. You remain responsible for this
presentation. See [Going Live](/platform/overview/going-live#quote-presentation)
for the canonical requirements and [fee
behavior](/platform/overview/core-concepts#fee-behavior) for how fees relate to
the source amount. Monitor `expiresAt` and refresh the quote before it expires.
If the buy frame is already loaded, call
`buyResult.value.setQuote(newSignature)` instead of re-creating the frame.
### Headless buy frame
Use `client.setupBuy()` when you want full control over your purchase UI. The
frame is headless — no visible UI — and emits events for you to handle. For the
frame URL, parameters, and events, see the [Buy frame](/platform/frames/buy)
reference.
```ts setupBuy theme={null}
import type { BuyEvent } from "@moonpay/platform-sdk-web";
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
onEvent: (event: BuyEvent) => {
switch (event.kind) {
case "ready":
// Pipeline starting — show a loading indicator
break;
case "complete":
// Transaction complete. Track status via polling.
console.log(event.payload.transaction);
// { id: "txn_01", status: "pending" }
break;
case "challenge":
// Verification required — render the challenge frame
openChallengeFrame(event.payload.url, buyResult);
break;
case "quoteExpired":
// Fetch a new quote, then update the frame:
// const newQuote = await client.getQuote({...});
// event.payload.setQuote(newQuote.value.signature);
break;
case "error":
console.error(event.payload.message);
break;
}
},
});
if (!buyResult.ok) {
// Handle error
}
```
When the buy frame emits a `challenge` event, the customer must complete one or
more verification steps before the transaction can proceed. Set up the challenge
frame with the URL from the event payload — do not construct the URL yourself.
For the frame URL, parameters, and events, see the [Challenge
frame](/platform/frames/challenge) reference.
The frame is self-driving: after initialization, it sequences through all
required verification steps, creates the transaction, and emits `complete` when
the pipeline finishes.
```ts Handle challenges theme={null}
import type { ChallengeEvent } from "@moonpay/platform-sdk-web";
async function openChallengeFrame(
challengeUrl: string,
buyResult: SetupBuyResult,
) {
// The challenge URL does not include a channelId — append one before rendering.
const url = new URL(challengeUrl);
url.searchParams.set("channelId", crypto.randomUUID());
const challengeResult = await client.setupChallenge({
challengeUrl: url.toString(),
container: document.querySelector("#challengeModal"),
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "ready":
// Challenge UI is rendered and visible
break;
case "complete":
// All verification resolved, transaction complete.
buyResult.value.dispose();
navigateToConfirmation(event.payload.transaction);
break;
case "cancelled":
// Customer dismissed the challenge — allow retry.
buyResult.value.dispose();
showRetryOption();
break;
case "error":
buyResult.value.dispose();
console.error(event.payload.message);
break;
}
},
});
}
```
When you receive `complete`, `cancelled`, or `error` from the challenge frame,
call `buyResult.value.dispose()` to also tear down the buy frame.
The frame handles all verification types automatically — KYC, Strong Customer
Authentication (SCA), CVC re-entry, wallet ownership, micro-authorization, and
3D Secure (3DS). You never need to distinguish between them.
When the buy frame or challenge frame emits `complete`, the payload includes
`{ transaction: { id, status } }`. The transaction is created and payment is
processing.
```ts Track the transaction theme={null}
async function pollTransaction(transactionId: string) {
const terminal = new Set(["completed", "failed"]);
while (true) {
const res = await client.getTransaction(transactionId);
if (!res.ok) throw new Error(res.error.message);
if (terminal.has(res.value.data.status)) return res.value.data.status;
await new Promise((r) => setTimeout(r, 3000));
}
}
```
### Transaction statuses
Transactions have the following statuses:
* **Pending:** The transaction has been initiated and the payment accepted. The
assets are being transferred.
* **Complete:** The transaction is finalized. The payment is complete and the
assets have been delivered to their destination.
* **Failed:** The transaction has failed. The payment was not executed and funds
were not transferred.
You can also track transaction status with the existing
[transaction-updated](/api-reference/widget/webhooks/transaction-updated)
webhook.
Let customers remove stored cards at any time.
```ts Delete a stored card theme={null}
const deleteResult = await client.deletePaymentMethod(paymentMethodId);
if (!deleteResult.ok) {
// Handle error
}
```
* Deleting an already-deleted card returns success (idempotent).
* Deleting a card with a pending transaction is rejected.
* Only payment methods with `allowsDeletion: true` can be deleted.
# Pay with Google Pay
Source: https://dev.moonpay.com/platform/guides/pay-with-google-pay
Allow customers to buy crypto headlessly with Google Pay.
Use this guide to execute a transaction with Google Pay after you have a [connected customer](/platform/guides/connect-a-customer). To let new customers buy with Google Pay before they have a MoonPay account, see [Guest checkout](/platform/guides/guest-checkout).
See the [Going Live](/platform/overview/going-live) section for details on the requirements you must meet before you can take this integration to production.
## Prerequisites
* A connected customer (via `client.getConnection()` or `client.connect()`).
* A UI surface where you can render the [Google Pay frame](/platform/frames/google-pay).
You can test the full Google Pay flow without a real Google Pay account by
using [test mode](/platform/overview/test-mode#google-pay). The frame renders
a mock Google Pay button that simulates the payment sheet.
## Flow overview
```mermaid theme={null}
sequenceDiagram
autonumber
actor C as Customer
participant FE as Your frontend
participant API as MoonPay API
participant GPF as Google Pay frame
participant CF as Challenge frame
Note over C,CF: Prerequisite: customer is connected
FE->>API: GET /platform/v1/payment-methods
API-->>FE: [{ type: "google_pay", availability }]
C->>FE: Enters amount
FE->>API: POST /platform/v1/quotes/buy
API-->>FE: { quote with signature }
FE->>GPF: Render Google Pay frame (signature)
GPF-->>FE: ready
FE->>C: Reveals Google Pay button
C->>GPF: Taps button, approves in native sheet
alt Happy path
GPF-->>FE: complete({ transaction: { id, status } })
else Verification required
GPF-->>FE: challenge({ url })
FE->>CF: Render Challenge frame at URL
CF-->>FE: complete({ transaction: { id, status } })
end
FE->>API: GET /platform/v1/transactions/{id}
Note over FE: Poll for final status
```
## Device and browser support
Google Pay relies on the [Payment Request API](https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API), so it only works in browsers that support that API, such as Chrome. The customer also needs a card set up with Google Pay.
In a native Android app, the Payment Request API is disabled by default in Android WebView, so offering Google Pay requires extra setup: Google Play services 25.18.30+, Android WebView for Chrome 137+, and the API enabled on your WebView. See the [frame requirements](/platform/frames/google-pay#android-webview) and the [Android manual integration guide](/platform/guides/manual-integration/android#google-pay-frame).
You don't need to detect the browser yourself. When Google Pay isn't available in the customer's environment, the frame emits an `unsupported` event — hide the button and offer another payment method. The code sample in [Execute the transaction](#execute-the-transaction) handles this event.
## Display payment methods
Use the SDK or API to fetch and display the payment methods that are available for the customer right now.
```ts List payment methods theme={null}
// After connecting, list available payment methods
const paymentMethodsResult = await client.getPaymentMethods();
if (!paymentMethodsResult.ok) {
// Handle error
}
console.log(paymentMethodsResult.value);
```
```ts Result theme={null}
[
{
type: "google_pay",
capabilities: {
supportedCurrencies: ["USD", "EUR", "GBP"],
supportedTransactionTypes: ["buy"],
},
availability: {
active: true,
},
},
];
```
## Get quotes
Quotes provide real-time prices and fees for transactions. Show the contextual
quote summary above the Google Pay button. The Google Pay sheet
presents fees, so you do not need to render a fee breakdown. See
[Going Live](/platform/overview/going-live#quote-presentation) for the required
presentation and [fee behavior](/platform/overview/core-concepts#fee-behavior)
for how fees relate to the source amount.
Only quotes with `executable: true` can be used to execute a transaction. See the [quotes API reference](/api-reference/platform/endpoints/quotes/get) for the fields required to receive `executable: true`.
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" }, // The fiat currency and amount to pay
destination: { asset: { code: "ETH" } }, // The crypto the customer will receive
wallet: { address: "0x1234..." }, // The destination wallet address
paymentMethod: { type: "google_pay" }, // The payment method type
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value);
```
```ts Result theme={null}
{
source: {
amount: "100.00",
asset: {
code: "USD",
name: "US Dollar",
precision: 2
}
},
destination: {
amount: "0.025",
asset: {
code: "ETH",
name: "Ethereum",
precision: 18
}
},
fees: {
network: {
amount: "2.50",
currencyCode: "USD"
},
moonpay: {
amount: "3.99",
currencyCode: "USD"
}
},
wallet: {
address: "0x1234..."
},
paymentMethod: {
type: "google_pay"
},
expiresAt: "2026-01-12T14:45:00Z",
executable: true,
signature: "eyJhbGciOiJFUzI1NiIs..."
}
```
## Execute the transaction
To execute a transaction, set up the payment flow based on the quote. Different payment methods have different requirements—you can configure each as needed to control your experience. For the frame URL, size, permissions, and events, see the [Google Pay frame](/platform/frames/google-pay) reference.
Some transactions require a challenge — the Google Pay frame emits a
`challenge` event when extra verification is needed. See [Handle
challenges](/platform/guides/handling-challenges) for the full flow.
```ts Google Pay theme={null}
import type { GooglePayEvent } from "@moonpay/platform-sdk-web";
// Render the Google Pay frame into your UI and handle callbacks
const googlePayResult = await client.setupGooglePay({
quote: quoteResult.value.signature, // The quote signature from getQuote
container: document.querySelector("#googlePayContainer"), // DOM element to render the button
onEvent: (event: GooglePayEvent) => {
switch (event.kind) {
case "ready":
// The frame is ready. Use this event to reveal the button if needed.
break;
case "complete": {
const txn = event.payload.transaction;
if (txn.status === "failed") {
// The transaction failed. Branch on failureCode for programmatic handling.
switch (txn.failureCode) {
case "authorizationDeclined":
// Prompt the customer to try a different card.
break;
case "serviceUnavailable":
// Retry after a short delay.
break;
default:
// Show txn.failureReason to the customer.
break;
}
break;
}
// The transaction is executing. Track the final status via polling and/or webhooks.
console.log(txn);
// { id: "txn_01", status: "pending" }
break;
}
case "challenge":
// Verification required. Render the challenge frame at the provided URL.
// See: /platform/guides/handling-challenges
console.log(event.payload.url);
break;
case "quoteExpired":
// Fetch a new quote, then pass its signature into the frame
// const newQuote = await client.getQuote({...});
// event.payload.setQuote(newQuote.value.signature);
break;
case "error":
// Depending on the error, you can have the customer pick a different payment method or retry.
console.error(event.payload.message);
break;
case "unsupported":
// Google Pay isn't supported in the current environment.
break;
}
},
});
if (!googlePayResult.ok) {
// Handle error setting up Google Pay
}
// You can update the quote or dispose the frame later
// googlePayResult.value.setQuote(newQuoteSignature);
// googlePayResult.value.dispose();
```
## Transaction statuses
Transactions have the following statuses:
* **Pending:** The transaction has been initiated and the payment accepted. The assets are being transferred.
* **Complete:** The transaction is finalized. The payment is complete and the assets have been delivered to their destination.
* **Failed:** The transaction has failed. The payment was not executed and funds were not transferred.
# Pay with widget
Source: https://dev.moonpay.com/platform/guides/pay-with-widget
Render the MoonPay buy widget to support all payment methods and regions.
Use this guide to render the MoonPay buy widget after you have a
[connected customer](/platform/guides/connect-a-customer). The widget renders
the full MoonPay buy experience — including payment collection
and transaction confirmation — inside an iframe in your application.
It supports all payment methods and regions available in the standard
MoonPay integration.
On the same confirmation view, before the widget, you display the contextual
quote summary and fee breakdown. You remain responsible for this presentation
during the normal hosted flow. See
[Going Live](/platform/overview/going-live#quote-presentation) for the
canonical requirements and [fee
behavior](/platform/overview/core-concepts#fee-behavior) for how fees relate to
the source amount.
## Prerequisites
* A connected customer (via `client.getConnection()` or `client.connect()`).
* A UI surface where you can render the widget frame (for example, a
modal or full-screen container).
## Get a quote
Request a quote for the transaction. The widget requires an executable quote,
so pass both a `wallet` and a `paymentMethod`. A quote that is not
`executable` will not render in the widget.
```ts Get quote theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234..." },
paymentMethod: { type: "venmo" },
});
if (!quoteResult.ok) {
// Handle error
}
```
For the current list of supported `paymentMethod.type` values, see the [Get a
quote API reference](/api-reference/platform/endpoints/quotes/get), which
renders the enum from the live OpenAPI spec. The widget collects payment for
the method in the quote: `apple_pay` renders Apple Pay, including in browsers
the [headless Apple Pay flow](/platform/guides/pay-with-apple-pay) doesn't
support. In those browsers, the customer approves the payment by scanning a QR
code with their iPhone.
## Render the widget
Pass the quote signature to `setupWidget`. The widget handles the
entire purchase flow inside the iframe.
```ts Widget theme={null}
import type { WidgetEvent } from "@moonpay/platform-sdk-web";
const widgetResult = await client.setupWidget({
quote: quoteResult.value.signature,
container: document.querySelector("#widgetContainer"),
onEvent: (event: WidgetEvent) => {
switch (event.kind) {
case "ready":
// The widget is loaded and visible.
break;
case "transactionCreated":
// A transaction has been initiated. The customer may still
// need to complete additional steps (for example, 3-D Secure).
console.log(event.payload.transaction);
// { id: "txn_01", status: "waitingAuthorization" }
break;
case "complete":
// The transaction has reached a terminal state.
console.log(event.payload.transaction);
// { id: "txn_01", status: "pending" }
break;
case "error":
console.error(event.payload.message);
break;
}
},
});
if (!widgetResult.ok) {
// Handle error setting up the widget
}
// Clean up when done
// widgetResult.value.dispose();
```
## Transaction statuses
* **waitingAuthorization:** The transaction has been created but the
customer needs to complete an authorization step (for example,
3-D Secure).
* **Pending:** The payment has been accepted and the assets are being
transferred.
* **Complete:** The transaction is finalized and the assets have been
delivered.
* **Failed:** The transaction has failed. No payment was applied.
## Compare payment methods
The widget is one of five ways to run a buy. See
[Choose a payment method](/platform/guides/payment-methods) to compare it with
Apple Pay, Google Pay, card, and the buy button. Use `getPaymentMethods()` to
determine which methods are available and choose the flow that fits your
experience.
# Choose a payment method
Source: https://dev.moonpay.com/platform/guides/payment-methods
Pick the payment experience that fits your UI and coverage needs.
Every payment flow starts from the same two things: an authenticated customer
(see [Choose an onboarding path](/platform/guides/onboarding-paths)) and an
executable quote. From there, you choose how much of the payment UI you want
to own. Call `getPaymentMethods()` to see which methods are available for the
customer right now, then pick the guide that matches your experience.
[Guest checkout](/platform/guides/guest-checkout) removes the first
requirement: new customers buy with Apple Pay or Google Pay before they have a
MoonPay account, and verification steps up only when a purchase requires it.
## Compare payment methods
| Payment method | UI control | Coverage | Best for |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- |
| [Apple Pay](/platform/guides/pay-with-apple-pay) | Headless frame renders the Apple Pay button; you own the rest of the UI | Devices and regions where Apple Pay is available | Single-tap checkout on Apple devices |
| [Google Pay](/platform/guides/pay-with-google-pay) | Headless frame renders the Google Pay button; you own the rest of the UI | Devices and regions where Google Pay is available | Single-tap checkout on devices that support Google Pay |
| [Card](/platform/guides/pay-with-card) | You build the purchase UI; the headless buy frame runs the pipeline and hosted frames handle card entry | Stored credit and debit cards; requires card payments enabled on your account | Full control over the purchase UI |
| [Bank transfer](/platform/guides/pay-with-bank-transfer) | You build the purchase UI and render the deposit details natively; the headless buy frame runs the pipeline | SEPA (EUR); centralized assets only, not DeFi | Lower-fee payments where the customer pays from their bank |
| [Buy button](/platform/guides/pay-with-buy-button) | You supply a container; MoonPay renders the button, the payment-method picker, and the confirmation sheet | Apple Pay, Google Pay, or card, depending on what's available to the customer | Fast, low-effort express checkout from a single button |
| [Widget](/platform/guides/pay-with-widget) | MoonPay renders the entire buy flow inside an iframe | All payment methods and regions in the standard MoonPay integration | Broad payment coverage without building purchase UI |
Every method can require a challenge when a transaction needs extra
verification. You handle it the same way in each flow: render the
[Challenge frame](/platform/frames/challenge) at the URL you receive. See
[Handle challenges](/platform/guides/handling-challenges).
## Payment guides
Let new customers buy with Apple Pay or Google Pay before they have a
MoonPay account.
Let customers buy crypto headlessly with Apple Pay.
Let customers buy crypto headlessly with Google Pay.
Let customers buy crypto using stored credit and debit cards.
Let customers pay with SEPA (EUR) and render the deposit details natively.
Add a single express checkout button that handles payment-method selection
and confirmation.
Render the MoonPay-hosted buy widget for all payment methods and regions.
Render the Challenge frame when a transaction needs extra verification.
# Configure frame appearance
Source: https://dev.moonpay.com/platform/guides/presentation-and-appearance
Control how co-branded frames look and behave in your app.
Use this guide to keep co-branded frames consistent with the rest of your UI.
Frames render MoonPay-hosted UI inside your app, so presentation choices affect
how the flow feels to customers.
## Prerequisites
* A [connected customer](/platform/guides/connect-a-customer).
* A container element in your UI where you render frames.
## Choose the right UI surface
* **Web**: Render co-branded frames in a modal or sheet and keep the rest of your UI visible behind it.
* **Mobile**: Present co-branded frames in a full-screen route or full sheet. This reduces layout issues when the frame navigates between steps.
## Control light and dark appearance
The [connect frame](/platform/frames/connect) supports a `theme` parameter. Use it to force `dark` or `light` instead of relying on the user's system appearance.
If you use the SDK, pass `theme` when you initialize the connect flow:
```ts Set appearance example highlight={8-10} theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "..." });
const connectResult = await client.connect({
container: connectContainer,
theme: {
appearance: "dark", // Force dark mode
},
onEvent: (event) => {
// Handle events
},
});
```
If you build the connect frame URL manually, include `theme` in the query string:
```html Example URL theme={null}
https://platform.moonpay.com/platform/v1/connect?sessionToken=c3N0XzAwMQ==&publicKey=...&channelId=ch_1&theme=dark
```
If you omit `theme`, the frame uses the user's system preference.
## Practical customization tips
* **Avoid resizing containers mid-flow**: Use a stable container size while the frame is mounted to prevent content jumps.
* **Show clear loading states**: If you wait for a frame’s `ready` event, keep your UI responsive and show a spinner/skeleton.
* **Plan for errors**: If a frame dispatches an `error` event, show a developer-friendly fallback (retry, exit, or contact support).
## Next steps
Learn the shared messaging protocol for integrating without the SDK.
See all initialization parameters including `theme`.
# Terms acceptance
Source: https://dev.moonpay.com/platform/guides/terms-acceptance
Present MoonPay's Terms of Use and Privacy Policy in your own UI, record acceptance with termsAcceptedAt, and capture the related consents on the API-driven path.
Present MoonPay's terms in your own UI. In the API-driven onboarding path there is no MoonPay-hosted terms screen: you display MoonPay's [Terms of Use](https://www.moonpay.com/legal/terms) and [Privacy Policy](https://www.moonpay.com/legal/privacy_policy), record the customer's explicit acceptance, and convey it to MoonPay when you create a session. This page covers the two ways to present the terms, how to record acceptance with `termsAcceptedAt`, and two more steps you own when you capture verification data in your own UI.
## When this applies
You present MoonPay's terms and record the customer's acceptance on two onboarding paths:
* **[Onboarding via API](/platform/guides/customer-api)**: you build the onboarding screens, so everything on this page applies.
* **[Guest checkout](/platform/guides/guest-checkout)**: the presentation rules are identical, and you convey acceptance with the same `termsAcceptedAt` field.
Terms acceptance is recorded against the customer, not per transaction. It is
separate from the per-transaction payment disclosures you render at checkout.
See [Going Live](/platform/overview/going-live) for those.
## Choose a presentation method
Present MoonPay's terms in one of two ways. Pick one at integration time; don't mix them.
* **Reference**: your terms of service include a required language block that links to MoonPay's Terms of Use and states that MoonPay provides the regulated services. One accept control covers both. Your MoonPay account team provides the exact text.
* **Side by side**: you show MoonPay's current Terms of Use in full, unedited, next to your own terms, with a clear boundary between the two. One acceptance covers both.
### Rendering rules
Whichever method you choose, the same rules apply:
* The terms must be visible without any interaction. Do not hide them behind tooltips, expandable menus, or secondary screens.
* Link to the canonical URLs as tappable hyperlinks. Never inline their contents into your own copy: MoonPay updates these documents over time, and a copied snapshot goes stale.
* Capture the timestamp at the moment the customer explicitly accepts, not when the screen renders or when you create the session.
| Document | Canonical URL |
| -------------- | --------------------------------------------------------------------------------------------- |
| Terms of Use | [https://www.moonpay.com/legal/terms](https://www.moonpay.com/legal/terms) |
| Privacy Policy | [https://www.moonpay.com/legal/privacy\_policy](https://www.moonpay.com/legal/privacy_policy) |
## Record the acceptance
Capture the timestamp when the customer accepts, and pass it as `termsAcceptedAt` (ISO 8601) when you create the session with `POST /platform/v1/sessions`.
```ts Create session with terms acceptance theme={null}
const url = "https://api.moonpay.com/platform/v1/sessions";
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
"X-Api-Key": "sk_test_123",
},
method: "POST",
body: JSON.stringify({
externalCustomerId: "your_user_id",
deviceIp: "...ip address from client",
termsAcceptedAt: "2026-07-17T09:41:12Z", // ISO 8601, within 60s ahead of server time
}),
});
console.log(await res.json());
```
```json Result theme={null}
{
"sessionToken": "c3N0XzAwMQ=="
}
```
`termsAcceptedAt` can be at most 60 seconds ahead of server time; there is no past limit. MoonPay records the terms version that was live at that timestamp and binds the acceptance to the customer when the session is authorized. A timestamp outside the allowed range returns a 400 with the code `terms_accepted_at_out_of_range`.
Re-sending `termsAcceptedAt` for a customer who has already accepted the current version is safe: the attestation is idempotent. When in doubt, send it.
The attestation records what you assert: that this customer explicitly accepted MoonPay's terms at that moment. You remain responsible for the authenticity of the acceptance, and you keep your own acceptance records available to MoonPay on request.
Attestations are scoped to the API key's environment. If the customer
transacts in both sandbox and live, record their acceptance in both.
See the [sessions API
reference](/api-reference/platform/endpoints/sessions/create) for all fields
and error responses.
## Handle `termsAcceptanceRequired`
Until a valid attestation exists for the current terms version, the customer can't transact. The `termsAcceptanceRequired` status surfaces with no credentials attached, from both the connection check ([web](/platform/sdk-reference/web/get-connection#connection), [React Native](/platform/sdk-reference/react-native/get-connection#connection)) and the [Auth frame](/platform/frames/auth). The `skipKyc` option never suppresses it: legal requirements can't be skipped.
To recover:
1. Re-present the terms with the presentation method you chose.
2. Capture a fresh acceptance timestamp.
3. Create a new session with `termsAcceptedAt`.
4. Relaunch the flow.
Passing `termsAcceptedAt` requires the Identity or Guest Checkout account capability.
## Re-present the terms when they change
Two events require a fresh acceptance:
* **MoonPay materially updates its terms.** `termsAcceptanceRequired` reappears on the connection check. Run the same recovery: re-present the terms, capture a fresh timestamp, create a new session with `termsAcceptedAt`, and relaunch the flow.
* **Your own terms materially change (reference method).** MoonPay sends no signal, because the reference lives in your terms of service. Re-present the terms and send a fresh `termsAcceptedAt` yourself.
## Before you submit customer data
Capturing verification data in your own UI comes with two more steps: a consent you display and a verification you perform.
### Biometric consent for selfie and document images
If your integration captures selfie or identity-document images, display the following biometric consent text to the customer before you submit any images through the [file-upload flow](/platform/guides/customer-api#upload-a-file).
By clicking \[Continue], you consent to Persona and its service providers collecting and processing your biometric data for identity verification and fraud prevention, per its Privacy Policy. Your biometric information will be stored up to 3 years.
Replace `[Continue]` with the label of the control the customer taps to proceed.
Keep the Privacy Policy text hyperlinked to [https://withpersona.com/legal/privacy-policy](https://withpersona.com/legal/privacy-policy) so the customer can open it.
### Verify phone numbers before you submit them
Verify that the customer owns the phone number with a one-time passcode before you submit it, and re-verify at least once every 30 days. MoonPay does not independently re-verify the number: submitting it is your attestation that verification occurred. See [KYC data requirements](/platform/guides/kyc-data-requirements) for the format rules.
## Next steps
Check KYC status, submit outstanding requirements, and handle verification
in your own UI.
Let new customers buy with Apple Pay or Google Pay; the same
`termsAcceptedAt` field records their acceptance.
The acceptance criteria you meet before production, including
per-transaction payment disclosures.
# Verification tiers
Source: https://dev.moonpay.com/platform/guides/verification-tiers
The KYC verification steps customers complete at each tier, per region, and how higher tiers unlock higher purchase limits.
Verification tiers are cumulative: each tier builds on the one before it and
unlocks a higher purchase limit. MoonPay always decides what verification a
customer needs. The checkout flow detects where a customer stands and prompts
them for exactly the missing steps, so you never assign or track a tier
yourself. This page covers which steps a customer completes at each tier and
when. [KYC data requirements](/platform/guides/kyc-data-requirements) covers
the exact fields, formats, and documents each step requires per country.
A tier is a conceptual model, not an API field.
[Customer API](/platform/guides/customer-api) integrations see the same
escalation concretely: `kyc.status` and `kyc.requirements` on the
[customer object](/api-reference/platform/objects-and-types/customer) list
exactly what is outstanding for a given customer. Hosted and widget
integrations see it as the prompts MoonPay renders.
## How verification tiers work
Each regional table below is a ladder. A customer starts at the lowest tier
available in their region and moves up by completing the steps the next tier
adds. Tiers are cumulative, so a customer at a given tier has completed every
step from the tiers below it. Higher tiers unlock higher purchase limits. Tier
numbering varies by region, and each table lists only the tiers that exist in
that region.
Tiers describe the standard escalation. MoonPay asks customers assessed as
higher risk to complete additional steps, such as the Customer Questionnaire
or Enhanced Due Diligence, regardless of tier.
MoonPay does not publish limit amounts. Your MoonPay account team shares limit
specifics for your integration directly.
## Verification steps at a glance
Four steps recur across the regional tables. The details column links to the
exact fields, formats, and documents each step requires.
| Step | What the customer provides | Details |
| ---------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| ID verification | A government-issued photo ID and a liveness check selfie | [Identity documents](/platform/guides/kyc-data-requirements#identity-documents), [Selfie and liveness check](/platform/guides/kyc-data-requirements#selfie-and-liveness-check) |
| Customer Questionnaire (CDD) | Occupation, source of funds, and declared annual income | [Due diligence questionnaires](/platform/guides/kyc-data-requirements#due-diligence-questionnaires) |
| Enhanced Due Diligence (EDD) | A deeper review that requires proof of income, such as a payslip or bank statement | [Due diligence questionnaires](/platform/guides/kyc-data-requirements#due-diligence-questionnaires) |
| Proof of Address (PoA) | A document confirming the customer's residential address, such as a utility bill or bank statement | [Proof of address](/platform/guides/kyc-data-requirements#proof-of-address) |
The regional tables use one more shorthand: **basic information** is the
customer's email, full name, date of birth, residential address, and
nationality.
## United States
Express Checkout is enabled per partner, not by default. To request it,
contact your MoonPay account team at
[team@moonpay.com](mailto:team@moonpay.com). Despite the similar name, this
verification tier is separate from the [express checkout
button](/platform/guides/pay-with-buy-button) that the buy button frame
renders for payments.
| Tier | What the customer completes |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Express Checkout | Basic information only. No phone number or ID upload. |
| Tier 1 | Basic information, phone number, and [Social Security number (SSN)](/platform/guides/kyc-data-requirements#tax-identifiers). |
| Tier 2 | Everything in Tier 1, plus ID verification and the Customer Questionnaire. |
| Tier 3 | Everything in Tier 2, plus Enhanced Due Diligence. |
This table lists what the customer completes for verification. It does not
describe what you pass when you create a session. [Guest
checkout](/platform/guides/guest-checkout) requires the customer's email
address and phone number on every session, regardless of tier.
New York requires full KYC (ID verification and the Customer Questionnaire)
from the very first transaction. Express Checkout is not available to New York
residents.
## Canada
| Tier | What the customer completes |
| ------ | --------------------------------------------------------------------------------------------------- |
| Tier 2 | Basic information, the Canada terms and conditions, and ID verification. |
| Tier 3 | Everything in Tier 2, plus the Customer Questionnaire. |
| Tier 4 | Everything in Tier 3, plus Enhanced Due Diligence (including proof of income) and Proof of Address. |
## United Kingdom
| Tier | What the customer completes |
| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tier 2 | Basic information, phone number, ID verification, a [tax identification number (TIN)](/platform/guides/kyc-data-requirements#tax-identifiers), UK Self-Categorisation, the Appropriateness Test, the Investment Risk Warning, the Customer Questionnaire, and Proof of Address on a risk-based basis. |
| Tier 3 | Everything in Tier 2, plus Enhanced Due Diligence. |
A lifetime purchase cap applies until the customer completes the Customer
Questionnaire, in line with UK financial promotion rules.
## European Economic Area
| Tier | What the customer completes |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Tier 2 | Basic information, phone number, ID verification, a [tax identification number (TIN)](/platform/guides/kyc-data-requirements#tax-identifiers), the Customer Questionnaire, and Proof of Address on a risk-based basis. |
| Tier 3 | Everything in Tier 2, plus Enhanced Due Diligence. |
Austria, Belgium, Bulgaria, Croatia, Cyprus, Czech Republic, Denmark, Estonia,
Finland, France, Germany, Greece, Hungary, Iceland, Ireland, Italy, Latvia,
Liechtenstein, Lithuania, Luxembourg, Malta, Netherlands, Norway, Poland,
Portugal, Romania, Slovakia, Slovenia, Spain, and Sweden.
## Australia and New Zealand
| Tier | What the customer completes |
| ------ | ------------------------------------------------------ |
| Tier 2 | Basic information, phone number, and ID verification. |
| Tier 3 | Everything in Tier 2, plus the Customer Questionnaire. |
| Tier 4 | Everything in Tier 3, plus Enhanced Due Diligence. |
## Rest of world
Rest of world covers Latin America, Africa, most of Asia, and the Middle East.
| Tier | What the customer completes |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Tier 2 | Basic information, phone number, ID verification, a [tax identification number](/platform/guides/kyc-data-requirements#tax-identifiers) where the country requires it, the Customer Questionnaire, and Proof of Address. |
| Tier 3 | Everything in Tier 2, plus Enhanced Due Diligence. |
Express Checkout is available in the United States only.
## Changes to tier requirements
Verification requirements evolve with MoonPay's compliance obligations. When
tier requirements change, the [changelog](/platform/changelog) announces the
change. The changelog supports RSS, so you can subscribe to updates. The live
checkout flow and `kyc.requirements` on the customer object always reflect the
current requirements.
# Core concepts
Source: https://dev.moonpay.com/platform/overview/core-concepts
Key terms and concepts for integrating with the MoonPay Platform
## Connections
An active **connection** represents a customer's permission to associate the MoonPay account to your application so you can:
* List and manage payment methods in your UI
* Get quotes that include detailed fees and customer limits
* Execute payments (for example, Apple Pay or debit cards) without redirecting the customer
* View and track transaction history
Read the [hosted onboarding guide](/platform/guides/connect-a-customer) for
implementation details.
## Frames
The integration uses a combination of API calls and frames. For steps that have compliance or regulatory requirements, you render an embedded frame (a WebView in mobile apps and an iframe on the web). This keeps sensitive data out of your application while MoonPay manages compliance.
There are two types of frames: **co-branded** and **headless**. Both communicate with your app using `postMessage` (on web and mobile). Each frame has its own lifecycle events and message patterns, documented in the [frames overview](/platform/frames/overview). You can manage frames yourself, or use the SDK for drop-in frame setup and event handling.
### Co-branded frames
Co-branded frames render MoonPay-hosted UI that you can theme to match your application. They are designed for contextual rendering in modals or sheets. A typical example is the MoonPay login flow used when a customer connects their account.
### Headless frames
Headless frames are either invisible or display minimal, non-customizable elements such as an Apple Pay button. You can inline these frames wherever needed in your UI.
## Challenges
Challenges complete specific tasks that require upgraded authentication, identity verification, or connection updates. Common cases include:
* Authentication upgrades (required for sensitive or destructive actions)
* Identity verification (Know Your Customer, KYC)
* Strong Customer Authentication (SCA), such as [3D Secure](https://www.checkout.com/products/authentication-3ds), where the customer’s bank requires additional verification
Frames and the Customer API surface challenges. The [Apple Pay](/platform/frames/apple-pay), [Google Pay](/platform/frames/google-pay), and [buy](/platform/frames/buy) frames emit a `challenge` event with a URL to load into the dedicated [Challenge frame](/platform/frames/challenge). The [Customer API](/platform/guides/customer-api) returns a `kyc.challenge` URL when verification needs a hosted step. See [Handle challenges](/platform/guides/handling-challenges) for the full flow.
## Customer
A customer is a person using your app who can have a connected MoonPay account.
### Verification (KYC)
MoonPay performs Know Your Customer (KYC) checks to meet financial compliance requirements and help prevent fraud and money laundering. MoonPay always decides what verification a customer needs. You choose who renders the UI that captures it. [Verification tiers](/platform/guides/verification-tiers) lists the steps customers complete at each verification level, per region.
A customer's KYC standing moves through a lifecycle: `not_created`, then `collecting` while requirements are outstanding, then `verifying` while MoonPay processes the submitted data, and finally `active` (or `unavailable` when verification cannot proceed). The API exposes this as `kyc.status`, with the outstanding items listed in `kyc.requirements`. See the [Customer API guide](/platform/guides/customer-api) for the full status table.
You have three ways to run verification. By default, the co-branded [connect flow](/platform/guides/connect-a-customer) captures and verifies everything in MoonPay-hosted UI. Alternatively, you capture KYC data in your own UI and submit it with the [Customer API](/platform/guides/customer-api). [Guest checkout](/platform/guides/guest-checkout) defers verification until a purchase requires it. When MoonPay can't verify a customer from submitted data alone, it falls back to a hosted challenge (see [Handle challenges](/platform/guides/handling-challenges)). To pick the path that fits your app, see [Choose an onboarding path](/platform/guides/onboarding-paths).
## Quotes
Quotes provide real-time prices and fees for fiat-to-crypto purchases.
Every quote response carries an `executable` boolean:
* `executable: true` — the quote can be used to execute a transaction.
* `executable: false` — use the quote for cost estimation only.
### Fee behavior
When you quote by `source.amount`, the optional `feeBehavior` field controls how
fees relate to that amount. The request and response values have different
meanings:
* `inclusive` (default): the request `source.amount` is the total paid. Fees
are deducted from it.
* `exclusive`: the request `source.amount` is the source principal. The API
adds fees, and the response `source.amount` is the total paid.
The following illustrative comparison assumes USD as the quote currency, a
request `source.amount` of `$100.00`, and total non-deprecated fees of `$5.00`.
Actual fee and destination amounts vary by quote.
| | Inclusive | Exclusive |
| -------------------------------------- | ------------------------------ | ------------------------- |
| Request `source.amount` | `$100.00` | `$100.00` |
| What the request means | Total paid | Purchase principal |
| How fees apply | Deducted from requested amount | Added to requested amount |
| Response `source.amount` (**You Pay**) | `$100.00` | `$105.00` |
| Amount used to buy crypto | `$95.00` | `$100.00` |
Destination-amount quotes are effectively inclusive. The response reports
`feeBehavior: "inclusive"` even if you request `exclusive`. They are not in this
comparison because their source output depends on live rates and fees.
See the [quotes API reference](/api-reference/platform/endpoints/quotes/get)
for the request fields required to receive `executable: true`.
# Going Live
Source: https://dev.moonpay.com/platform/overview/going-live
Acceptance criteria for going live with your integration.
All requirements on this page are verified by MoonPay before going live.
## Overview
This page defines the acceptance criteria that apply to MoonPay Platform
integrations. Meeting these requirements is a condition of going live. They
address usability, legal accuracy, and regulatory compliance.
MoonPay may update these criteria at any time. When changes are required,
MoonPay will provide reasonable written notice to allow time for
implementation.
### How to read this page
Each requirement is annotated with a **geo tag** identifying where the
customer must be located for the rule to apply:
### Applicability by payment experience
Use this matrix with the requirements below. **You render** means you are
responsible even when you also render a MoonPay frame.
| Payment experience | Contextual quote summary | Fee breakdown | Payment disclosures |
| --------------------- | ------------------------ | ------------------------------------ | ------------------- |
| Standalone Apple Pay | You render | Apple Pay sheet | You render |
| Standalone Google Pay | You render | Google Pay sheet | You render |
| Cards | You render | You render | You render |
| Bank transfers | You render | You render | You render |
| Buy button | You render | You render (Apple/Google Pay: sheet) | You render |
| Widget | You render | You render | MoonPay renders |
***
## Universal requirements
### Accuracy
All information you present to the customer must be true, accurate, and not
misleading.
### Terms of Use acceptance
Applies when you onboard customers through the
[Customer API](/platform/guides/customer-api) or
[guest checkout](/platform/guides/guest-checkout): you present MoonPay's Terms
of Use and Privacy Policy in your own UI and record the customer's acceptance,
as described in [Terms acceptance](/platform/guides/terms-acceptance).
This is verified before go-live, and is distinct from the per-transaction
payment disclosures covered elsewhere on this page.
***
## Quote presentation
### Contextual quote summary
For standalone Apple Pay, standalone Google Pay, cards, the buy button, and the
widget, present the current quote on the same confirmation view before the
customer submits payment. You are responsible for this presentation, including for
normal buy-button and widget flows.
The contextual summary must include:
| Value | Source |
| --------------------------- | --------------------------- |
| **You Pay** | Response `source.amount` |
| Amount used to buy \[token] | Calculated source principal |
| Exchange rate | `exchangeRate` |
| Total crypto received | `destination.amount` |
Only **You Pay** is an exact label. You may use semantically equivalent labels
for the other three values.
Use the effective [`feeBehavior`](/platform/overview/core-concepts#fee-behavior)
to calculate the source principal with decimal arithmetic in the quote currency:
* For `inclusive`, subtract every present, non-deprecated `fees.*.amount` from
the response `source.amount`, including `fees.defi.amount` when present.
Always exclude deprecated `fees.partner.amount` to avoid double counting.
* For `exclusive`, use the request `source.amount` as the principal.
Do not add fees to the response `source.amount`.
### Required line items
For cards, the buy button, and the widget, show these three rows when their
fields are present in a table or row-based fee breakdown on the same
confirmation view before the customer submits payment. Use these exact labels:
| Label | Quote field |
| ----------------- | ----------------------- |
| **Network Fee** | `fees.network.amount` |
| **Ecosystem Fee** | `fees.ecosystem.amount` |
| **MoonPay Fee** | `fees.moonpay.amount` |
Do not add a DeFi Fee row. Do not place the contextual quote-summary values in
this fee breakdown.
### Waived fees
If a fee field is absent, omit its row. If a present fee is zero or waived,
show its row with `$0.00`, or the equivalent zero amount in the quote currency.
### Amount consistency
The **You Pay** amount must exactly match the amount charged. Keep every quote
value you present consistent with the current quote. Any discrepancy
between the quoted total and the final payment will block go-live approval.
### Payment experience exceptions
Standalone Apple Pay and standalone Google Pay do not require you to render a
fee breakdown because their payment sheets present fees. They
still require the contextual quote summary above the Apple Pay or Google Pay
button.
***
## Payment disclosures
### Determine applicable disclosures
Use `paymentDisclosures` on the
[quote response](/api-reference/platform/endpoints/quotes/get#response-data-payment-disclosures)
as the transaction-specific source of truth for the disclosure copy to render.
`customer.country`, `customer.administrativeArea`, and `customer.area` from the
[check](/platform/frames/check) or [connect](/platform/frames/connect) complete
event identify the customer's jurisdiction, but do not select disclosure copy
on their own.
`capabilities.ramps.requirements.paymentDisclosures` on the connection event is
deprecated. Use the quote response instead.
You render quote-driven disclosures for standalone Apple Pay, standalone
Google Pay, and cards. MoonPay renders them in the normal hosted buy-button and
widget UI.
### Disclosures - US (New York and Washington)
For standalone Apple Pay, when `paymentDisclosures` contains
`us-transaction-finality`, display the following exact text directly above the
Apple Pay frame for customers located in **NY or WA**:
I agree to MoonPay's Terms of Use and understand that, once executed, this transaction cannot be cancelled, recalled, refunded, or otherwise undone. Fraudulent transactions may result in the loss of funds with no recourse.
```html wrap theme={null}
I agree to MoonPay's
Terms of Use and understand
that, once executed, this transaction cannot be cancelled, recalled, refunded,
or otherwise undone. Fraudulent transactions may result in the loss of funds
with no recourse.
```
### Disclosures - EEA
The `paymentDisclosures` array on the quote response identifies which disclosure to render:
* `eea-crypto-asset-risk` — render the **Standard crypto-assets** disclosure below
* `eea-unregulated-stablecoin-risk` — render the **Non-MiCA-compliant stablecoins** disclosure below
* Both present — render both
For customers located in the **EEA**, display the applicable exact text on the
same confirmation view, directly above or below the Apple Pay button, Google
Pay button, or card purchase confirmation, for standalone Apple Pay, standalone
Google Pay, and cards. The copy depends on the crypto asset in the quote.
#### Standard crypto-assets
By continuing, you agree to transact with MoonPay Europe, subject to its Terms of Use and Privacy Policy. Crypto-assets can be risky and values may decrease quickly. Transfers are irreversible once broadcast to the blockchain. The quoted exchange rate may include a spread. Learn more and review the whitepaper (if available).
```html wrap theme={null}
By continuing, you agree to transact with MoonPay Europe, subject to its
Terms of Use and
Privacy Policy.
Crypto-assets can be risky and values may decrease quickly. Transfers are
irreversible once broadcast to the blockchain. The quoted exchange rate may
include a spread. Learn more and
review the whitepaper
(if available).
```
#### Non-MiCA-compliant stablecoins
Applies when the customer is transacting in a stablecoin that is not
MiCA-compliant (for example USDT, cUSD, DAI, PYUSD).
Important: You are about to transact in a stablecoin that is not MiCA-compliant, carries fewer safeguards, and may be difficult to sell. By continuing, you agree to transact with MoonPay Europe subject to its Terms of Use and Privacy Policy. Transfers are irreversible once broadcast to the blockchain. The quoted exchange rate may include a spread. Learn more and review the whitepaper (if available).
```html wrap theme={null}
Important: You are about to transact in a stablecoin that is not MiCA-compliant,
carries fewer safeguards, and may be difficult to sell. By continuing, you agree
to transact with MoonPay Europe subject to its
Terms of Use and
Privacy Policy.
Transfers are irreversible once broadcast to the blockchain. The quoted exchange
rate may include a spread.
Learn more and
review the whitepaper
(if available).
```
### Disclosure rendering rules
For disclosures you render, the disclosure must be visible without any
interaction. It must not be hidden behind expandable menus, tooltips, or
secondary screens. The Terms of Use must be a tappable link. Render the full
text without truncation.
***
## Attribution
For cards, display **"Powered by MoonPay"** on the Buy screen.
***
## Gateway (DeFi tokens)
Applies when a customer buys a DeFi token via Gateway. Gateway is available to
customers in the **US, excluding New York and Washington**, and quotes are only
returned in that geography, so these requirements apply wherever a DeFi quote is
available.
A Gateway purchase happens in two steps: the customer buys a stablecoin from
MoonPay, then that stablecoin is swapped for the destination token on a
decentralised exchange (DEX). DeFi tokens are identified by a
[`caip19`](/api-reference/platform/endpoints/quotes/get#identifying-the-destination-token)
destination and are returned by the
[list assets](/api-reference/platform/endpoints/assets/list) endpoint with
`source` set to `defi`. When a quote is for a DeFi token, its
`paymentDisclosures` array contains the `gateway-token` disclosure.
Apart from the overrides below, the card requirements under
[Quote presentation](#quote-presentation) and
[Payment disclosures](#payment-disclosures) apply.
### Fees and slippage
In addition to the standard [required line items](#required-line-items), display
these before the customer transacts:
| Line item | Source | Description |
| ------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Swap fee** | `fees.defi` | The fee for swapping the stablecoin into the destination token on the DEX. |
| **Slippage** | `slippageBps` | The maximum price movement tolerated for the swap, in basis points. Show it with the explanation: "Swap will only execute if final price falls within this range." |
The [Apple Pay fee exception](#payment-experience-exceptions) still applies:
with Apple Pay you don't display fees up front, but the total charged must
match the Apple Pay sheet.
### Disclosures - DeFi tokens (Gateway)
Display the following exact text directly above or below the payment frame when
the quote's `paymentDisclosures` array contains the `gateway-token` disclosure
(currently version `1`).
By proceeding, you agree to two steps under these terms: (1) Buying stablecoin from MoonPay and (2) Swapping stablecoin for your chosen destination asset via a decentralised exchange.
```html wrap theme={null}
By proceeding, you agree to two steps under
these terms: (1) Buying
stablecoin from MoonPay and (2) Swapping stablecoin for your chosen destination
asset via a decentralised exchange.
```
The standard [disclosure rendering rules](#disclosure-rendering-rules) apply: the
disclosure must be visible without any interaction, must not be hidden behind
expandable menus, tooltips, or secondary screens, the Terms of Use must be a
tappable link, and the full text must be rendered without truncation.
# Introduction
Source: https://dev.moonpay.com/platform/overview/introduction
Build fiat-to-crypto experiences with headless payments. You control the user experience while MoonPay handles compliance, risk, and fraud.
## Welcome to the MoonPay Platform
Use the MoonPay Platform APIs, SDKs, and frames to build crypto ramps directly in your app. Before you start, review the [requirements](/platform/overview/requirements) and [core concepts](/platform/overview/core-concepts).
Some Platform API capabilities are [enabled per partner](/api-reference/platform/documentation/using-the-api#capability-enablement) by MoonPay. To request access to a capability, contact your MoonPay account team or [team@moonpay.com](mailto:team@moonpay.com).
## Getting started
Compare hosted onboarding, onboarding via API, and guest checkout, and pick
the path that fits your app.
The hosted path: MoonPay's co-branded connect frame handles login and the
full KYC flow.
The API-driven path: capture KYC data in your own screens and submit it with
the Customer API.
The deferred path: new customers buy instantly, with verification stepped up
at purchase time.
Embed headless and co-branded UI in your app.
Explore the Platform API endpoints and parameters.
## Quickstart
The quickstart below shows the fastest path, [hosted onboarding](/platform/guides/connect-a-customer) with Apple Pay. For the alternatives, see [Choose an onboarding path](/platform/guides/onboarding-paths).
Get a session token>}>
Create a [session token](/api-reference/platform/endpoints/sessions/create) on your server and send the token to your frontend.
```ts Create session token theme={null}
// Server-side code example
const url = "https://api.moonpay.com/platform/v1/sessions";
const res = await fetch(url, {
headers: {
"Content-Type": "application/json",
"X-Api-Key": "sk_test_123",
},
method: "POST",
body: JSON.stringify({
externalCustomerId: "your_user_id",
deviceIp: "...ip address from client",
}),
});
console.log(await res.json());
```
```json Result theme={null}
{
"sessionToken": "c3N0XzAwMQ=="
}
```
Connect a customer>}>
On your frontend, check whether the customer has an active connection. If they do, you receive credentials for the next steps.
```ts Check the connection theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
// Create the client with your session token
const client = createClient({
sessionToken: "c3N0XzAwMQ==", // The session token from your server
});
// Check if the customer has an active connection
const connectionResult = await client.getConnection();
if (!connectionResult.ok) {
// Handle error
}
console.log(connectionResult.value);
```
```ts Result (active) theme={null}
{
status: "active",
customer: {
id: "Y3VzX2FiYzEyMw=="
},
// Encrypted credentials — the SDK decrypts and stores them internally
credentials: "ZW5jXzAwMQ==",
capabilities: {}
}
```
```ts Result (requires connection) theme={null}
{
status: "connectionRequired",
// Encrypted credentials — the SDK decrypts and stores them internally
credentials: "ZW5jXzAwMQ=="
}
```
List payment methods>}>
List the payment methods available to the customer at the current time.
```ts List payment methods theme={null}
// After connecting, list available payment methods
const paymentMethodsResult = await client.getPaymentMethods();
if (!paymentMethodsResult.ok) {
// Handle error
}
console.log(paymentMethodsResult.value);
// [{ type: "apple_pay", capabilities: {...}, availability: {...} }, ...]
```
Get quotes>}>
Get [quotes](/platform/overview/core-concepts#executable-quotes) with detailed fees and limits for transactions.
```ts Get quotes theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" }, // The fiat currency and amount to pay
destination: { asset: { code: "ETH" } }, // The crypto the customer will receive
wallet: { address: "0x..." }, // The destination wallet address
paymentMethod: { type: "apple_pay" },
});
if (!quoteResult.ok) {
// Handle error
}
console.log(quoteResult.value);
// { signature: "...", expiresAt: "2026-01-12T14:45:00Z", ... }
```
Execute headless payments>}>
Once you have a quote, [execute the transaction](/platform/guides/pay-with-apple-pay).
```ts Pay with Apple Pay theme={null}
import type { ApplePayEvent } from "@moonpay/platform-sdk-web";
const paymentButtonContainer = document.querySelector("#payment");
const setupApplePayResult = await client.setupApplePay({
quote: quoteResult.value.signature, // The quote signature
container: paymentButtonContainer,
onEvent: (event: ApplePayEvent) => {
switch (event.kind) {
case "ready":
// Reveal the button
paymentButtonContainer.style.opacity = "1";
break;
case "complete":
// The transaction is executing. Use polling and/or webhooks to track final status.
console.log(event.payload.transaction);
// { id: "txn_01", status: "pending" }
break;
case "quoteExpired":
// Fetch a new quote and update the frame
// event.payload.setQuote(newQuote.signature);
break;
}
},
});
if (!setupApplePayResult.ok) {
// Handle error
}
```
# Requirements
Source: https://dev.moonpay.com/platform/overview/requirements
Requirements for the headless ramp integration.
To integrate the headless ramp, you will need:
* A partner account and API credentials
* A frontend (web or mobile) app
* If you're using the SDK, ensure it's installed
* A server for sending requests to MoonPay and receiving webhooks
MoonPay will work with you directly to set up your account and credentials.
## Integrating on the web
### Content Security Policy
If you embed MoonPay frames on the web, your Content Security Policy (CSP) must allow MoonPay’s iframes and network calls.
Your [CSP](https://developer.mozilla.org/en-US/docs/Glossary/CSP) should include at least the following rules:
* [frame-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-src)
* [connect-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src)
```sh theme={null}
Content-Security-Policy: frame-src https://*.moonpay.com/; connect-src https://*.moonpay.com/;
```
## Configuration
### Domain settings
When integrating MoonPay on the web, provide your app’s [origin](https://developer.mozilla.org/en-US/docs/Web/API/URL/origin) per environment. This allows MoonPay to embed frames securely.
### Apple Pay
To use Apple Pay on the web, you must complete Apple’s domain verification to prove ownership of your site. This is currently a manual process.
## Integrating in mobile apps
### WebViews
This integration supports using [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview) on iOS and [WebView](https://developer.android.com/reference/android/webkit/WebView) on Android.
When using `WKWebview` on iOS you will need to set [`allowsInlineMediaPlayback`](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/allowsinlinemediaplayback) to `true` for the [Connect frame](/platform/frames/connect).
# Supported currencies
Source: https://dev.moonpay.com/platform/overview/supported-currencies
See which cryptocurrencies MoonPay supports for buying, selling, and MoonPay Gateway.
* Customers in Canada cannot purchase Stablecoins.
* Availability of Sell subject to further geographic restrictions. Check your account for availability.
* Learn about the [MiCA sustainability indicators](/widget/sustainability-transparency) of supported assets.
* **EEA includes:** Austria, Belgium, Bulgaria, Croatia, Cyprus, Czechia, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Iceland, Ireland, Italy, Latvia, Liechtenstein, Lithuania, Luxembourg, Malta, Netherlands, Norway, Poland, Portugal, Romania, Slovakia, Slovenia, Spain, Sweden
## On-ramp and off-ramp
The following table lists the cryptocurrencies available through MoonPay's
on-ramp and off-ramp products. Availability varies by region.
## MoonPay Gateway
Offer DeFi tokens through a single MoonPay checkout.
MoonPay Gateway lets customers purchase supported DeFi tokens directly with
fiat. Behind the scenes, MoonPay completes the on-ramp and swaps into the
selected token, delivering it to the customer's destination wallet. Partners
offer this as one integrated purchase flow without building wallet
provisioning, swap routing, or transaction-signing infrastructure.
Gateway supports tokens on **Solana, Ethereum, Base, and HyperCore**.
### Why use Gateway
* **Offer more assets:** Add supported DeFi tokens to your existing catalog
alongside the cryptocurrencies available through MoonPay's standard on-ramp.
* **Keep the purchase simple:** Customers pay with fiat and receive the selected
token without separately buying a bridge asset or completing a swap.
* **Reuse your integration:** Discover and quote DeFi tokens through the same
Platform API and complete purchases with the existing MoonPay payment
buttons.
* **Leave the infrastructure to MoonPay:** MoonPay provisions the intermediate
wallet, executes the swap, and handles authorization securely inside the
payment frame.
### How Gateway works
1. Use the [List assets](/api-reference/platform/endpoints/assets/list) endpoint
to discover supported DeFi tokens alongside standard assets.
2. [Request a quote](/api-reference/platform/endpoints/quotes/get) using the
token's CAIP-19 identifier. The quote includes the estimated token amount,
fees, slippage, and required disclosures.
3. Present an existing MoonPay payment button. MoonPay completes the fiat
purchase, securely authorizes the swap, and delivers the token to the
customer's destination wallet.
4. Implement the required customer disclosures for Gateway purchases. See the
[Gateway (DeFi tokens)](/platform/overview/going-live#gateway-defi-tokens)
Going Live requirements.
The Gateway catalog changes as tokens and networks evolve. Use the List assets
endpoint as the source of truth instead of hardcoding availability.
See, for example, [Pay with the buy button](/platform/guides/pay-with-buy-button)
to integrate the checkout experience. Gateway purchases also work with
[Apple Pay](/platform/guides/pay-with-apple-pay) and
[Google Pay](/platform/guides/pay-with-google-pay).
# Test mode
Source: https://dev.moonpay.com/platform/overview/test-mode
Use test mode to develop and test your integration without transferring real assets.
MoonPay provides a test mode for integration development and testing. Test mode uses testnet blockchains and simulated payments. No real assets are transferred.
## Enabling test mode
Test mode is determined by the API key you use when [creating a session](/api-reference/platform/endpoints/sessions/create). Use your **test API key** (`sk_test_...`) to enable test mode. Use your **live API key** (`sk_live_...`) for production.
You can find your API keys on the [Developers page](https://dashboard.moonpay.com/developers) in your MoonPay dashboard.
All frames and SDK methods automatically operate in test mode when initialized
with a session token created using test API keys.
## Test accounts
When creating test accounts:
* **KYC verification is simulated** - documents are not verified
* **We recommend using a US or UK address** for test accounts, as these work best with test payment cards
* You can skip document submission by clicking "Skip document submission" if prompted
## Test data
### Email addresses
You must use a real email address that you can access. MoonPay sends OTP codes for login verification, and there are no test bypass values.
You cannot reuse the same email address for different customers.
**Tip:** Use the `+` suffix pattern to create multiple unique addresses from a single inbox: `you+test1@example.com`, `you+test2@example.com`, etc.
### Phone numbers
You must use a real phone number that you can access. MoonPay sends OTP codes for verification, and there are no test bypass values.
A phone number can only be associated with one customer at a time. If you
verify with the same phone number on a different customer, it will be removed
from the previous customer.
### SSN (US residents)
SSN values are not verified in test mode. You can enter any 9-digit value.
Do not use your real Social Security number. Use a fake value like
`123456789`.
## Test payment cards
Use the following test cards to simulate payments. **Do not enter real payment card information.** Use any valid 3-digit CVV and any future expiration date.
### US customers
| Card type | Card number | Expiration | CVV |
| :---------- | :-------------------- | :--------- | :---- |
| Visa Credit | `4000 0200 0000 0000` | `12/2030` | `100` |
### UK customers
| Card type | Card number | Expiration | CVV |
| :---------- | :-------------------- | :--------- | :---- |
| Visa Credit | `4242 4242 4242 4242` | `12/2030` | `100` |
| Visa Debit | `4659 1055 6905 1157` | `12/2030` | `100` |
### EU customers
| Card type | Card number | Expiration | CVV |
| :-------------------- | :-------------------- | :--------- | :---- |
| Mastercard Debit (DE) | `5305 4847 4880 0098` | `12/2030` | `100` |
### 3D Secure test cards
3D Secure (3DS) is an additional verification step the customer's bank may require during a card payment. The cards below trigger deterministic 3DS outcomes so you can test your [challenge handling](/guides/handling-challenges) end to end.
* **Frictionless** outcomes complete without any customer interaction.
* **Challenge** outcomes render a 3DS challenge frame the customer must complete or cancel.
When a card triggers a 3DS challenge, the simulator prompts for a password.
Enter `Checkout1!` to complete the challenge.
#### Frictionless
| Outcome | Region | Card type | Card number | Expiration | CVV |
| :-------- | :------ | :---------------- | :-------------------- | :--------- | :---- |
| Succeeded | US | Visa Credit | `4485 0403 7153 6584` | `12/2030` | `100` |
| Succeeded | EU (FR) | Visa Credit | `4010 0562 0000 0018` | `12/2030` | `100` |
| Succeeded | EU (FR) | Mastercard Credit | `5137 2100 0000 0018` | `12/2030` | `100` |
| Failed | EU (FR) | Visa Credit | `4022 0501 0000 0000` | `12/2030` | `100` |
| Failed | EU (FR) | Mastercard Credit | `5132 5626 0000 0029` | `12/2030` | `100` |
#### Challenge
| Outcome | Region | Card type | Card number | Expiration | CVV |
| :-------- | :------ | :---------------- | :-------------------- | :--------- | :---- |
| Succeeded | US | Mastercard Credit | `5385 3083 6013 5181` | `12/2030` | `100` |
| Succeeded | EU (FR) | Visa Debit | `4010 0617 0000 0021` | `12/2030` | `100` |
| Succeeded | EU (FR) | Mastercard Credit | `5137 2100 0000 0158` | `12/2030` | `100` |
| Failed | US | Visa Credit | `4243 7542 7170 0719` | `12/2030` | `100` |
| Failed | EU (FR) | Visa Credit | `4150 5610 0000 0027` | `12/2030` | `100` |
| Failed | EU (FR) | Mastercard Credit | `5341 0348 0000 0024` | `12/2030` | `100` |
### Declined transactions
Use these cards to test error handling for different failure scenarios.
| Card number | Expiration | CVV | Decline reason |
| :-------------------- | :--------- | :---- | :----------------------- |
| `4544 2491 6767 3670` | `12/2030` | `100` | Insufficient funds |
| `4897 4535 6848 5113` | `12/2030` | `100` | Suspected fraud |
| `4818 9242 5013 1070` | `12/2030` | `100` | Restricted card |
| `4556 2537 5271 2245` | `12/2030` | `100` | Security violation |
| `4095 2548 0264 2505` | `12/2030` | `100` | Timeout / Internal error |
| `5437 8211 3539 9682` | `12/2030` | `100` | Insufficient funds |
| `5279 9884 0539 8834` | `12/2030` | `100` | Restricted card |
| `5265 1622 7058 7964` | `12/2030` | `100` | Timeout / Internal error |
| `5363 4501 8040 2239` | `12/2030` | `100` | Lost card |
## Apple Pay
In test mode, the Apple Pay frame renders a mock Apple Pay button instead of the native Apple Pay UI. When a customer taps the mock button, a browser confirmation dialog (`window.confirm`) appears in place of the Apple Pay payment sheet:
* **Ok** simulates a successful transaction.
* **Cancel** simulates a failed transaction.
This lets you test the full Apple Pay flow without a real Apple Pay account or Safari-specific setup.
If you embed the Apple Pay frame in an iframe with a `sandbox` attribute,
include `allow-modals` in the sandbox value. This allows `window.confirm` to
work cross-origin inside the frame. See the [Apple Pay frame
reference](/platform/frames/apple-pay#permissions) for details.
## Google Pay
In test mode, the Google Pay frame renders a mock Google Pay button instead of the native Google Pay sheet. When a customer taps the mock button, a browser prompt (`window.prompt`) appears in place of the Google Pay payment sheet:
* **OK** authorizes the payment using the billing address in the prompt.
* **Cancel** declines the payment.
The prompt is pre-filled with a US billing address you can edit as JSON before confirming:
```json theme={null}
{
"name": "John Doe",
"address1": "123 Main St",
"address2": "",
"address3": "",
"locality": "San Francisco",
"administrativeArea": "CA",
"countryCode": "US",
"postalCode": "94105",
"phoneNumber": "+14155551234"
}
```
Keep `phoneNumber` in E.164 format (for example `+14155551234`). Invalid JSON in the prompt falls back to this default address.
This lets you test the full Google Pay frame flow without a Google Pay account or the native payment sheet.
The mock applies only when the frame uses this prompt. MoonPay treats the resulting token as a test-mode mock (`authorized` or `declined`). A real Google Pay payment token in test mode still goes to the sandbox payment processor.
If you embed the Google Pay frame in an iframe with a `sandbox` attribute,
include `allow-modals` in the sandbox value. This allows `window.prompt` to
work cross-origin inside the frame. See the [Google Pay frame
reference](/platform/frames/google-pay#permissions) for details.
## Triggering challenges
In test mode, you can force a [challenge](/platform/guides/handling-challenges) by setting the buy amount to a specific value. This lets you test how your integration renders and resolves a challenge without reproducing the real-world conditions that normally trigger one.
| Buy amount | Challenge | Payment method |
| :--------- | :--------------- | :----------------- |
| `48` | Wallet ownership | Apple Pay and card |
| `49` | CVV re-entry | Card |
The amount must match exactly. For example, a quote for `49` triggers the CVV challenge, but `49.01` does not. These triggers only apply in test mode and have no effect with a live API key.
Challenges are surfaced by a `challenge` event that points to the [challenge
frame](/platform/frames/challenge). See [Handle
challenges](/platform/guides/handling-challenges) for how to render and
resolve one.
## Bank transfers
Bank-transfer buy transactions (`sepa` for EUR) settle asynchronously. In production, the transaction settles when the customer's deposit arrives and is confirmed. That confirmation never happens in test mode, so a `pending` bank-transfer transaction cannot complete on its own without sending real money.
Use the [simulate bank-transfer settlement](/api-reference/platform/endpoints/transactions/simulate-bank-transfer) endpoint to drive a bank-transfer transaction to a terminal state without real funds.
### Prerequisites
* The transaction is a bank transfer with `paymentMethod.type` set to `sepa` and is in `pending`.
* The request uses the access token of the transaction's own customer. Another customer's transaction returns `404`.
* Test mode only. A live-mode transaction returns `403`.
Open-banking bank transfers settle through a different flow and are not supported by the simulator.
### Outcomes
Set the required `outcome` field to one of the following:
| Outcome | What it simulates | Result |
| :-------- | :----------------------------------------- | :------------------------------------------------------------------------------------ |
| `settled` | The customer's deposit arrives and matches | The transaction proceeds to completion. |
| `timeout` | The customer never pays | The transaction fails with a bank-transfer timeout, the same as a real 7-day timeout. |
### Poll for the terminal state
Settlement is asynchronous, so the response reflects the transaction's current state rather than its terminal state. Poll [`getTransaction`](/platform/sdk-reference/web/get-transaction) or `GET /platform/v1/transactions/{id}` for the outcome. The `timeout` outcome is immediate; `settled` completes after the simulated deposit is processed.
### Model a specific sender (optional)
The simulator synthesizes sender bank details per the transaction's currency. To model a specific incoming deposit, pass a `senderBankAccount` object with an `accountHolderName` and the account identifiers for the currency:
* `sepa` (EUR): provide an `iban`.
```json Simulate a settled SEPA deposit theme={null}
{
"outcome": "settled",
"senderBankAccount": {
"accountHolderName": "Ada Lovelace",
"iban": "DE89370400440532013000"
}
}
```
See the [simulate bank-transfer settlement](/api-reference/platform/endpoints/transactions/simulate-bank-transfer) reference for the full request and response shape.
## Test assets
Test mode supports the following assets and testnets:
| Asset | Code | Testnet | Notes |
| :------------ | :------------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bitcoin | `BTC` | Testnet3 | |
| Bitcoin Cash | `BCH` | Testnet | |
| Ethereum | `ETH` | Sepolia | |
| ERC-20 tokens | `USDC`, `LINK`, `PYUSD`, `RLUSD` | Sepolia | Transfers deliver MoonPay's test token, [MoonPayToken](https://sepolia.etherscan.io/address/0x699cfe8997d647d03325ef4bfd039d5bb0984a17), not the real token. |
| Litecoin | `LTC` | Testnet | |
| Solana | `SOL` | Devnet | Native `SOL` only; SPL tokens are not available in test mode. |
| Stellar | `XLM` | Testnet | |
| TON | `TON` | Testnet | |
| XRP Ledger | `XRP`, `RLUSD` | Testnet | |
This table helps you plan your integration; it is not a contract. The set of
assets available in test mode shifts over time.
Test-mode purchases deliver 1/100th of the quoted amount because MoonPay holds limited testnet funds. A purchase quoted at 0.1 ETH delivers 0.001 ETH to your test wallet.
[Quote requests](/api-reference/platform/endpoints/quotes/get) for assets that aren't available in test mode fail with a `400 invalid_request` error. Use an asset such as `SOL` or `ETH` when testing.
## Troubleshooting
### Common errors
| Error | Cause | Solution |
| :---------------------------------------------------------------------- | :-------------------------------------------------- | :-------------------------------------------------------------------------------------------------- |
| `Framing 'https://platform.moonpay.com' violates "frame-ancestors" CSP` | Your app or website domain has not been allowlisted | Add domains at [https://dashboard.moonpay.com/developers](https://dashboard.moonpay.com/developers) |
# Using agents
Source: https://dev.moonpay.com/platform/overview/using-agents
Connect AI coding agents to the MoonPay Platform documentation using MCP, llms.txt, and contextual code actions.
AI coding agents can search and reference the MoonPay Platform documentation
directly from your development environment. The docs provide four integration
points: an MCP server for real-time search, agent skills for structured
capabilities, `llms.txt` files for bulk context, and contextual code actions on
every code block.
## MCP server
The documentation includes a
[Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that
gives AI agents direct access to search and retrieve content. Instead of relying
on web search, your agent queries the documentation index for accurate,
up-to-date results.
The server URL is:
```bash MCP server URL theme={null}
https://dev.moonpay.com/mcp
```
### Connect your client
Run the following command to add the MCP server to Claude Code:
```bash Add MCP server theme={null}
claude mcp add --transport http moonpay-docs https://dev.moonpay.com/mcp
```
By default this adds the server to your local scope. Use `--scope project`
to share the configuration with your team via `.mcp.json`, or
`--scope user` to make it available across all your projects.
Add the MCP server to your project's `.cursor/mcp.json` file, or open
**Settings → Cursor Settings → MCP** and add a new server:
```json .cursor/mcp.json theme={null}
{
"mcpServers": {
"moonpay-docs": {
"url": "https://dev.moonpay.com/mcp"
}
}
}
```
Restart Cursor after adding the server.
Add the following to your Claude Desktop configuration file
(`claude_desktop_config.json`). On macOS, this file is at
`~/Library/Application Support/Claude/claude_desktop_config.json`. On
Windows, it is at `%APPDATA%\Claude\claude_desktop_config.json`.
```json claude_desktop_config.json theme={null}
{
"mcpServers": {
"moonpay-docs": {
"url": "https://dev.moonpay.com/mcp"
}
}
}
```
Restart Claude Desktop after saving the file.
Add the following to your Codex configuration file at
`~/.codex/config.toml`, or to a project-scoped `.codex/config.toml`:
```toml config.toml theme={null}
[mcp_servers.moonpay-docs]
url = "https://dev.moonpay.com/mcp"
```
You can also add it with the CLI:
```bash Add MCP server theme={null}
codex mcp add moonpay-docs https://dev.moonpay.com/mcp
```
Any MCP-compatible client can connect using the server URL above. Refer to
your client's documentation for setup instructions.
## Skills
The documentation publishes a
[`SKILL.md`](https://agentskills.io/) file that describes MoonPay Platform
capabilities in a structured, machine-readable format. Unlike the MCP server,
which responds to individual queries, `SKILL.md` gives an agent a complete
picture of available workflows, required inputs, and constraints up front.
View the generated file at
[`dev.moonpay.com/SKILL.md`](https://dev.moonpay.com/SKILL.md).
### Install skills
Run the following command to add MoonPay Platform skills to your agent's
context:
```bash npx theme={null}
npx skills add https://dev.moonpay.com
```
```bash bunx theme={null}
bunx skills add https://dev.moonpay.com
```
```bash pnpm dlx theme={null}
pnpm dlx skills add https://dev.moonpay.com
```
The skills CLI discovers and installs the `skill.md` file automatically. Once
installed, your agent can reference MoonPay Platform capabilities without
additional configuration.
## Contextual code actions
Code blocks throughout these docs include contextual actions for sending code
directly to an AI tool. Hover over any code block to see options for Cursor,
Claude, and ChatGPT. Each option copies the code along with surrounding
documentation context so the AI tool understands how to use it.
## llms.txt
The documentation provides
[`llms.txt`](https://llmstxt.org/) files for direct content
ingestion by large language models:
| File | Description |
| :------------------------------------------------------- | :------------------------------------------------------------- |
| [`llms.txt`](https://dev.moonpay.com/llms.txt) | A concise summary of the documentation structure and key pages |
| [`llms-full.txt`](https://dev.moonpay.com/llms-full.txt) | The complete documentation content for full-context indexing |
Use these files to give an AI tool broad context about the MoonPay Platform
without connecting to the MCP server. You can paste the contents into a chat
session or point a tool at the URL directly.
# SDK reference
Source: https://dev.moonpay.com/platform/sdk-reference/overview
Reference documentation for the MoonPay Platform SDKs.
Pick the SDK for your platform. Each reference documents the client methods, options, events, return types, and errors.
`@moonpay/platform-sdk-web` — for first-party browser apps.
`@moonpay/platform-sdk-react-native` — for iOS and Android apps built with
React Native.
Skip the SDK and drive [frames](/platform/frames) directly. Use this on iOS,
Android, or Flutter, or when you prefer not to bundle a third-party SDK.
**Need an SDK for your platform?** Contact us if you need an SDK for iOS,
Android, Flutter, or another platform.
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-add-card
Inline card entry frame. Embed card capture in your own layout.
`` renders the Add Card frame inline in your layout. It is
the declarative alternative to
[`client.setupAddCard()`](/platform/sdk-reference/react-native/setup-add-card).
Use this component when you want the card capture form embedded in a custom
screen rather than presented as a full-screen modal.
```tsx AddCardScreen.tsx theme={null}
import {
MoonPayAddCard,
type AddCardEvent,
} from "@moonpay/platform-sdk-react-native";
import { StyleSheet, View } from "react-native";
export function AddCardScreen() {
const handleEvent = (event: AddCardEvent) => {
switch (event.kind) {
case "ready":
break;
case "complete":
// event.payload.card contains the saved card details.
console.log(event.payload.card.last4, event.payload.card.brand);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
};
return (
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});
```
***
## Props
| Prop | Type | Required | Default | Description |
| --------- | ------------------------------- | -------- | --------- | ---------------------------------------------------------------------------- |
| `onEvent` | `(event: AddCardEvent) => void` | | — | Callback for add-card lifecycle events. See [`AddCardEvent`](#addcardevent). |
| `style` | `ViewStyle` | | `flex: 1` | Container style. |
### `AddCardEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ------------ | ----------------------------------- | ------------------------------------------------------- |
| `"ready"` | — | The card entry UI is rendered and ready. |
| `"complete"` | `{ card: CardResponse }` | The card was saved. Inspect `CardResponse` for details. |
| `"error"` | `{ code: string; message: string }` | The flow encountered an error. |
**`"error"` codes:** `"configurationError"` | `"generic"`
### `CardResponse`
| Field | Type | Description |
| ----------------- | ------------------ | --------------------------------- |
| `id` | `string` | The saved card identifier. |
| `type` | `string` | Payment method type. |
| `cardType` | `string` | Card scheme type. |
| `brand` | `string` | Card network (for example, Visa). |
| `last4` | `string` | Last four digits of the card. |
| `expirationMonth` | `string` | Two-digit expiry month. |
| `expirationYear` | `string` | Four-digit expiry year. |
| `availability` | `{ active: true }` | Card availability status. |
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-apple-pay-button
Inline Apple Pay button. Render it wherever it should appear in your layout.
`` renders an Apple Pay button inline in your layout.
Apple Pay availability inside a WebView depends on iOS WebKit support. The
component emits `"unsupported"` when Apple Pay isn't available — surface a
fallback payment method.
```tsx ApplePaySection.tsx theme={null}
import React from "react";
import {
MoonPayApplePayButton,
MoonPayChallenge,
type ApplePayEvent,
} from "@moonpay/platform-sdk-react-native";
export function ApplePaySection({
quoteSignature,
}: {
quoteSignature: string;
}) {
const [challengeUrl, setChallengeUrl] = React.useState(null);
const handleEvent = (event: ApplePayEvent) => {
switch (event.kind) {
case "ready":
break;
case "buttonPressed":
// Customer tapped the button — show a loading state or fire analytics.
break;
case "complete":
console.log(event.payload.transaction);
break;
case "quoteExpired":
// Fetch a new quote and update the prop, or call setQuote directly:
// event.payload.setQuote(newQuote.signature);
break;
case "challenge":
setChallengeUrl(event.payload.url);
break;
case "error":
console.error(event.payload.kind, event.payload.message);
break;
case "unsupported":
// Apple Pay isn't available — fall back to another method.
break;
}
};
return (
<>
{challengeUrl && (
{
if (e.kind === "complete" || e.kind === "cancelled") {
setChallengeUrl(null);
}
}}
/>
)}
>
);
}
```
***
## Props
| Prop | Type | Required | Default | Description |
| ----------------------- | -------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | — | Quote `signature` from [`getQuote()`](/platform/sdk-reference/react-native/get-quote). Reactive — updating this prop pushes the new signature into the frame without a remount. |
| `externalTransactionId` | `string` | | — | Partner-assigned identifier for this transaction attempt. Mount-time only. |
| `onEvent` | `(event: ApplePayEvent) => void` | | — | Callback for Apple Pay lifecycle events. See [`ApplePayEvent`](#applepayevent). |
| `style` | `ViewStyle` | | `height: 48` | Container style. |
### `ApplePayEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ----------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The Apple Pay UI is rendered and ready. |
| `"buttonPressed"` | — | The customer tapped the Apple Pay button, before iOS presents the payment sheet. An intent-to-buy signal with no payload — it still fires if the customer then cancels the sheet. Not a purchase outcome; listen for `"complete"` for the result. |
| `"complete"` | `{ transaction: FrameTransaction }` | The payment finished. Inspect `FrameTransaction` for the outcome. |
| `"challenge"` | `{ kind: "frame"; url: string }` | Verification required. Render [``](/platform/sdk-reference/react-native/components/moonpay-challenge) with the provided URL. |
| `"quoteExpired"` | `{ setQuote: (signature: string) => void }` | The quote expired. Fetch a new quote and update the `quote` prop, or call `payload.setQuote(...)` directly. |
| `"error"` | `{ kind: string; message: string }` | The flow encountered an error. See error kinds below. |
| `"unsupported"` | — | Apple Pay isn't available in the user's environment. |
**`"error"` kinds:** `"configurationError"` | `"invalidQuote"` |
`"quoteExpired"` | `"oneTapApplePaySecondFactorRequired"` | `"genericError"`
`"oneTapApplePaySecondFactorRequired"` occurs during the 1TAP Apple Pay flow
when MoonPay needs a second authentication factor. Hand off to
[`client.connect()`](/platform/sdk-reference/react-native/connect) or render
[``](/platform/sdk-reference/react-native/components/moonpay-connect),
then retry.
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-auth
Inline auth frame for headless and Customer API integrations.
`` renders the MoonPay auth frame inline in your layout. It is
the declarative alternative to
[`client.setupAuth()`](/platform/sdk-reference/react-native/setup-auth).
The auth frame is a lighter-weight counterpart to the connect flow. It drives
the customer through email/OTP authentication only, without the full connect
UI. This makes it a good fit for headless and Customer API integrations.
**Precondition:** a `clientToken` must be present in the client context before
mounting this component. The token is populated automatically when
[`client.getConnection()`](/platform/sdk-reference/react-native/get-connection)
or [``](/platform/sdk-reference/react-native/components/moonpay-connection-check)
resolves with `status: "connectionRequired"`. If no token is present, the
component emits an `"error"` event.
When the customer completes the flow with an active connection, credentials are
decrypted and stored on the shared client automatically.
```tsx AuthScreen.tsx theme={null}
import {
MoonPayAuth,
type AuthEvent,
} from "@moonpay/platform-sdk-react-native";
import { StyleSheet, View } from "react-native";
export function AuthScreen() {
const handleEvent = (event: AuthEvent) => {
switch (event.kind) {
case "ready":
break;
case "complete":
// Emitted only for active or termsAcceptanceRequired connections.
console.log(event.payload.status);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
};
return (
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});
```
***
## Props
| Prop | Type | Required | Default | Description |
| --------- | ---------------------------- | -------- | --------- | ------------------------------------------------------------------ |
| `onEvent` | `(event: AuthEvent) => void` | | — | Callback for auth lifecycle events. See [`AuthEvent`](#authevent). |
| `style` | `ViewStyle` | | `flex: 1` | Container style. |
### `AuthEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ------------ | ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The auth UI is rendered and ready. |
| `"complete"` | `Connection` | Emitted only when the connection is `active` or `termsAcceptanceRequired`. Credentials are already applied. |
| `"error"` | `ConnectionError` | The flow encountered an error. |
**`ConnectionError`** is discriminated by `code`:
| `code` | Extra fields | Description |
| ------------------- | -------------------------------------- | --------------------------------------------------------- |
| `"validationError"` | `errors: ConfigValidationFieldError[]` | One or more session/client/public-key fields are invalid. |
| `"generic"` | `message?: string` | A generic connection failure. |
`ConfigValidationFieldError` entries have `code:
"invalidSessionToken" | "invalidClientToken" | "invalidPublicKey"` and a
developer-facing `message`.
`"complete"` is emitted only for `active` and `termsAcceptanceRequired`
connection statuses. If the customer's connection resolves to another status
(such as `pending` or `unavailable`), no `"complete"` event fires.
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-buy-button
Inline card buy button. Render it wherever it should appear in your layout.
`` renders a card payment button inline in your layout.
This button is for card payments. For bank transfers (SEPA), render your own
native button and open the headless Buy frame with
[`client.setupBuy()`](/platform/sdk-reference/react-native/setup-buy#bank-transfer)
instead.
```tsx BuyButtonSection.tsx theme={null}
import React from "react";
import {
MoonPayBuyButton,
MoonPayChallenge,
type BuyButtonEvent,
} from "@moonpay/platform-sdk-react-native";
export function BuyButtonSection({
quoteSignature,
}: {
quoteSignature: string;
}) {
const [challengeUrl, setChallengeUrl] = React.useState(null);
const handleEvent = (event: BuyButtonEvent) => {
switch (event.kind) {
case "buttonPressed":
// Customer tapped the button — show a loading state or fire analytics.
break;
case "complete":
console.log(event.payload.transaction);
break;
case "challenge":
setChallengeUrl(event.payload.url);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
};
return (
<>
{challengeUrl && (
{
if (e.kind === "complete" || e.kind === "cancelled") {
setChallengeUrl(null);
}
}}
/>
)}
>
);
}
```
***
## Props
| Prop | Type | Required | Default | Description |
| ----------------------- | --------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | — | Quote `signature` from [`getQuote()`](/platform/sdk-reference/react-native/get-quote). Reactive — updating this prop pushes the new signature into the frame without a remount. |
| `externalTransactionId` | `string` | | — | Partner-assigned identifier for this transaction attempt. Mount-time only. |
| `onEvent` | `(event: BuyButtonEvent) => void` | | — | Callback for buy button lifecycle events. See [`BuyButtonEvent`](#buybuttonevent). |
| `style` | `ViewStyle` | | `height: 48` | Container style. |
### `BuyButtonEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ----------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"buttonPressed"` | — | The customer tapped the pay button, before the payment sheet appears. An intent-to-buy signal with no payload — it still fires if the customer then cancels. Not a purchase outcome; listen for `"complete"` for the result. |
| `"complete"` | `{ transaction: FrameTransaction }` | The payment finished. Inspect `FrameTransaction` for the outcome. |
| `"challenge"` | `{ kind: "frame"; url: string }` | Verification required. Render [``](/platform/sdk-reference/react-native/components/moonpay-challenge) with the provided URL. |
| `"error"` | `{ code: string; message: string }` | The flow encountered an error. |
When the quote expires, the frame emits an `"error"` event with
`code: "quoteExpired"`. Fetch a new quote and update the `quote` prop to retry.
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-buy-frame
Headless buy frame. The declarative alternative to client.setupBuy().
`` is a headless component (renders nothing visible) that
drives the buy pipeline. It is the declarative alternative to
[`client.setupBuy()`](/platform/sdk-reference/react-native/setup-buy).
Use this component when you want to drive the buy flow without a visible frame
— for example, to present your own UI while the transaction processes in the
background, then show [``](/platform/sdk-reference/react-native/components/moonpay-challenge)
only if a challenge step is required.
The `quote` prop is **reactive**: updating it pushes the new signature directly
into the live frame without a remount.
```tsx BuyFlow.tsx theme={null}
import React from "react";
import {
MoonPayBuyFrame,
MoonPayChallenge,
type BuyEvent,
} from "@moonpay/platform-sdk-react-native";
export function BuyFlow({ quoteSignature }: { quoteSignature: string }) {
const [challengeUrl, setChallengeUrl] = React.useState(null);
const handleEvent = (event: BuyEvent) => {
switch (event.kind) {
case "ready":
break;
case "complete":
console.log(event.payload.transaction);
break;
case "challenge":
setChallengeUrl(event.payload.url);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
};
return (
<>
{challengeUrl && (
{
if (e.kind === "complete" || e.kind === "cancelled") {
setChallengeUrl(null);
}
}}
/>
)}
>
);
}
```
***
## Props
| Prop | Type | Required | Description |
| ----------------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | Quote `signature` from [`getQuote()`](/platform/sdk-reference/react-native/get-quote). Reactive — updating this prop pushes the new signature into the frame without a remount. |
| `externalTransactionId` | `string` | | Partner-assigned identifier for this transaction attempt. Mount-time only. |
| `onEvent` | `(event: BuyEvent) => void` | | Callback for buy frame lifecycle events. See [`BuyEvent`](#buyevent). |
This component renders nothing visible (0×0). It has no `style` prop.
### `BuyEvent`
| kind | Payload | When you receive it |
| ------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The buy frame is ready. |
| `"complete"` | `{ transaction: FrameTransaction }` | The purchase finished. Inspect `FrameTransaction` for the outcome. |
| `"challenge"` | `{ kind: "frame"; url: string }` | Verification required. Render [``](/platform/sdk-reference/react-native/components/moonpay-challenge) with the provided URL. |
| `"error"` | `{ code: string; message: string }` | The flow encountered an error. |
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-challenge
Inline 3DS/challenge frame. Mount it when a payment frame emits a challenge event, or when a buy quote returns a challenge.
`` renders a 3DS or identity challenge frame inline in your
layout. It is the declarative alternative to
[`client.setupChallenge()`](/platform/sdk-reference/react-native/setup-challenge).
Mount this component when a payment frame (Apple Pay, Google Pay, buy button,
or buy frame) emits a `"challenge"` event. Pass the `url` from the event
payload directly. Unmounting the component disposes the frame and stops event
delivery.
```tsx PaymentScreen.tsx theme={null}
import React from "react";
import {
MoonPayApplePayButton,
MoonPayChallenge,
type ApplePayEvent,
type ChallengeEvent,
} from "@moonpay/platform-sdk-react-native";
import { StyleSheet, View } from "react-native";
export function PaymentScreen({ quoteSignature }: { quoteSignature: string }) {
const [challengeUrl, setChallengeUrl] = React.useState(null);
const handlePayEvent = (event: ApplePayEvent) => {
if (event.kind === "challenge") {
setChallengeUrl(event.payload.url);
}
};
const handleChallengeEvent = (event: ChallengeEvent) => {
switch (event.kind) {
case "complete":
console.log(event.payload);
setChallengeUrl(null);
break;
case "cancelled":
console.log("Challenge cancelled", event.payload);
setChallengeUrl(null);
break;
case "error":
console.error(event.payload.code, event.payload.message);
setChallengeUrl(null);
break;
}
};
return (
{challengeUrl ? (
) : (
)}
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});
```
***
## Props
| Prop | Type | Required | Default | Description |
| --------- | --------------------------------- | -------- | --------- | --------------------------------------------------------------------------------- |
| `url` | `string` | ✅ | — | Full challenge URL from a payment frame's `"challenge"` event. Mount-time only. |
| `onEvent` | `(event: ChallengeEvent) => void` | | — | Callback for challenge lifecycle events. See [`ChallengeEvent`](#challengeevent). |
| `style` | `ViewStyle` | | `flex: 1` | Container style. |
### `ChallengeEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ------------- | ----------------------------------- | --------------------------------------------------------------- |
| `"ready"` | — | The challenge UI is rendered and ready. |
| `"complete"` | `CompleteResult` | The challenge finished. See `CompleteResult` shape below. |
| `"cancelled"` | `Cancellation` | The customer dismissed the challenge. See `Cancellation` below. |
| `"error"` | `{ code: string; message: string }` | The challenge encountered an error. |
**`CompleteResult`** is a discriminated union on `flow`:
| `flow` | Extra fields |
| -------------------------------- | ----------------------------------------------- |
| `"buy"` | `transaction: FrameTransaction` |
| `"identity"` | `identityId: string` |
| `"guest_checkout_limit_upgrade"` | `status: "pending" \| "upgraded" \| "rejected"` |
**`Cancellation`** is a discriminated union on `flow`:
| `flow` | Extra fields |
| -------------------------------- | --------------------------------------------------- |
| `"buy"` | `transactionId?: string`, `challengeToken?: string` |
| `"identity"` | — |
| `"guest_checkout_limit_upgrade"` | — |
On `"cancelled"`, carry `transactionId` and `challengeToken` to resume the
transaction if needed.
On a `"guest_checkout_limit_upgrade"` `"complete"`, read `status`. `upgraded`
means the customer's guest limit is raised, so request the quote again to get
one with `executable: true`. `rejected` means verification failed and running
the upgrade again does not change it. See [Upgrade a guest
account](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up).
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-connect
Inline connect flow. Render it wherever it should appear in your layout.
`` renders the MoonPay connect flow inline in your layout. It
is the declarative alternative to
[`client.connect()`](/platform/sdk-reference/react-native/connect).
When the customer completes the flow with an active connection, credentials are
decrypted and stored on the shared client automatically. Subsequent calls to
`client.getQuote()`, `client.setupApplePay()`, etc. use those credentials
without any extra steps.
If you want the full end-to-end flow (create session → check connection →
connect), start with the
[Connect a customer](/platform/guides/connect-a-customer) guide.
```tsx ConnectScreen.tsx theme={null}
import {
MoonPayConnect,
type ConnectEvent,
} from "@moonpay/platform-sdk-react-native";
import { StyleSheet, View } from "react-native";
export function ConnectScreen() {
const handleEvent = (event: ConnectEvent) => {
switch (event.kind) {
case "ready":
// The UI is ready. Reveal the surface if you hid it while loading.
break;
case "complete":
// event.payload is the Connection — same shape as client.getConnection() returns.
if (event.payload.status === "active") {
console.log(event.payload.customer.id);
}
break;
case "error":
console.error(event.payload);
break;
}
};
return (
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});
```
***
## Props
| Prop | Type | Required | Default | Description |
| --------- | ------------------------------------ | -------- | --------- | --------------------------------------------------------------------------- |
| `theme` | `{ appearance?: "light" \| "dark" }` | | — | Appearance override for the connect frame. Mount-time only. |
| `onEvent` | `(event: ConnectEvent) => void` | | — | Callback for connect lifecycle events. See [`ConnectEvent`](#connectevent). |
| `style` | `ViewStyle` | | `flex: 1` | Container style. |
### `ConnectEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `"ready"` | — | The connect UI is rendered and ready. |
| `"complete"` | [`Connection`](/platform/sdk-reference/react-native/get-connection#connection) | The customer completed the flow. Same shape as `client.getConnection()` returns. |
| `"error"` | `ConnectEventError` | The flow encountered an error. |
**`ConnectEventError`** is discriminated by `code`:
| `code` | Extra fields | Description |
| ------------------- | -------------------------------------- | --------------------------------------------------------- |
| `"validationError"` | `errors: ConfigValidationFieldError[]` | One or more session/client/public-key fields are invalid. |
| `"generic"` | `message?: string` | A generic connection failure. |
`ConfigValidationFieldError` entries have `code:
"invalidSessionToken" | "invalidClientToken" | "invalidPublicKey"` and a
developer-facing `message`.
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-connection-check
Headless connection check. The declarative alternative to client.getConnection().
`` is a headless component (renders nothing visible)
that checks the customer's connection status. It is the declarative alternative
to [`client.getConnection()`](/platform/sdk-reference/react-native/get-connection).
When the connection is active, credentials are decrypted and stored on the
shared client automatically. Subsequent calls to `client.getQuote()`,
`client.setupApplePay()`, etc. use those credentials without any extra steps.
Mount this component once early in your flow — for example, when your app loads
or when a payment screen becomes visible. Unmount it after you receive the
`"complete"` event.
```tsx PaymentFlow.tsx theme={null}
import React from "react";
import {
MoonPayConnectionCheck,
MoonPayConnect,
type ConnectionCheckEvent,
} from "@moonpay/platform-sdk-react-native";
export function PaymentFlow() {
const [connectionStatus, setConnectionStatus] = React.useState(
null,
);
const handleCheckEvent = (event: ConnectionCheckEvent) => {
if (event.kind === "complete") {
setConnectionStatus(event.payload.status);
}
if (event.kind === "error") {
console.error(event.payload.message);
}
};
return (
<>
{connectionStatus === null && (
)}
{connectionStatus === "connectionRequired" && }
>
);
}
```
***
## Props
| Prop | Type | Required | Description |
| --------- | --------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `skipKyc` | `boolean` | | Pass `true` for headless/Customer API integrations so the check opts out of KYC-based statuses. Mount-time only. |
| `onEvent` | `(event: ConnectionCheckEvent) => void` | | Callback for connection check events. See [`ConnectionCheckEvent`](#connectioncheckevent). |
This component renders nothing visible (0×0). It has no `style` prop.
### `ConnectionCheckEvent`
| kind | Payload | When you receive it |
| ------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------ |
| `"complete"` | [`Connection`](/platform/sdk-reference/react-native/get-connection#connection) | The check finished. Inspect `payload.status` to route. |
| `"error"` | `{ message: string }` | The check encountered an error. |
**`Connection` statuses:**
| Status | Meaning |
| --------------------------- | ---------------------------------------------------------------------------- |
| `"active"` | The customer is connected. Credentials are applied to the client. |
| `"connectionRequired"` | The customer needs to connect. Render `` or ``. |
| `"unavailable"` | The customer's region is not supported. |
| `"pending"` | KYC is in progress — may resolve on a subsequent check. |
| `"failed"` | KYC failed or was rejected. |
| `"termsAcceptanceRequired"` | The customer must accept updated terms before proceeding. |
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-connection-reset
Headless reset frame. The declarative alternative to client.resetConnection().
`` is a headless component (renders nothing visible)
that clears the customer's MoonPay connection for this partner. It is the
declarative alternative to
[`client.resetConnection()`](/platform/sdk-reference/react-native/reset-connection).
Mount this component after clearing your own local auth state to signal to
MoonPay that the customer has signed out. Unmount it after you receive the
`"complete"` event (or on error). The reset completes or times out within 5
seconds.
```tsx SignOutScreen.tsx theme={null}
import React from "react";
import {
MoonPayConnectionReset,
type ConnectionResetEvent,
} from "@moonpay/platform-sdk-react-native";
export function SignOutScreen({ onDone }: { onDone: () => void }) {
const [resetting, setResetting] = React.useState(true);
const handleEvent = (event: ConnectionResetEvent) => {
if (event.kind === "complete") {
setResetting(false);
onDone();
}
if (event.kind === "error") {
console.error(event.payload.message);
setResetting(false);
onDone();
}
};
return <>{resetting && }>;
}
```
***
## Props
| Prop | Type | Required | Description |
| --------- | --------------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `onEvent` | `(event: ConnectionResetEvent) => void` | | Callback for reset lifecycle events. See [`ConnectionResetEvent`](#connectionresetevent). |
This component renders nothing visible (0×0). It has no `style` prop.
### `ConnectionResetEvent`
| kind | Payload | When you receive it |
| ------------ | --------------------- | ---------------------------------------------------- |
| `"complete"` | — | The connection was successfully reset. |
| `"error"` | `{ message: string }` | The reset failed or timed out. Unmount and continue. |
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-google-pay-button
Inline Google Pay button. Render it wherever it should appear in your layout.
`` renders a Google Pay button inline in your layout.
Google Pay availability depends on the device and environment. The component
emits `"unsupported"` when Google Pay isn't available — surface a fallback
payment method.
```tsx GooglePaySection.tsx theme={null}
import React from "react";
import {
MoonPayGooglePayButton,
MoonPayChallenge,
type GooglePayEvent,
} from "@moonpay/platform-sdk-react-native";
export function GooglePaySection({
quoteSignature,
}: {
quoteSignature: string;
}) {
const [challengeUrl, setChallengeUrl] = React.useState(null);
const handleEvent = (event: GooglePayEvent) => {
switch (event.kind) {
case "ready":
break;
case "buttonPressed":
// Customer tapped the button — show a loading state or fire analytics.
break;
case "complete":
console.log(event.payload.transaction);
break;
case "quoteExpired":
// Fetch a new quote and update the prop, or call setQuote directly:
// event.payload.setQuote(newQuote.signature);
break;
case "challenge":
setChallengeUrl(event.payload.url);
break;
case "error":
console.error(event.payload.kind, event.payload.message);
break;
case "unsupported":
// Google Pay isn't available — fall back to another method.
break;
}
};
return (
<>
{challengeUrl && (
{
if (e.kind === "complete" || e.kind === "cancelled") {
setChallengeUrl(null);
}
}}
/>
)}
>
);
}
```
***
## Props
| Prop | Type | Required | Default | Description |
| ----------------------- | --------------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | — | Quote `signature` from [`getQuote()`](/platform/sdk-reference/react-native/get-quote). Reactive — updating this prop pushes the new signature into the frame without a remount. |
| `externalTransactionId` | `string` | | — | Partner-assigned identifier for this transaction attempt. Mount-time only. |
| `onEvent` | `(event: GooglePayEvent) => void` | | — | Callback for Google Pay lifecycle events. See [`GooglePayEvent`](#googlepayevent). |
| `style` | `ViewStyle` | | `height: 48` | Container style. |
### `GooglePayEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ----------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The Google Pay UI is rendered and ready. |
| `"buttonPressed"` | — | The customer tapped the Google Pay button, before Android presents the Google Pay sheet. An intent-to-buy signal with no payload — it still fires if the customer then cancels the sheet. Not a purchase outcome; listen for `"complete"` for the result. |
| `"complete"` | `{ transaction: FrameTransaction }` | The payment finished. Inspect `FrameTransaction` for the outcome. |
| `"challenge"` | `{ kind: "frame"; url: string }` | Verification required. Render [``](/platform/sdk-reference/react-native/components/moonpay-challenge) with the provided URL. |
| `"quoteExpired"` | `{ setQuote: (signature: string) => void }` | The quote expired. Fetch a new quote and update the `quote` prop, or call `payload.setQuote(...)` directly. |
| `"error"` | `{ kind: string; message: string }` | The flow encountered an error. See error kinds below. |
| `"unsupported"` | — | Google Pay isn't available in the user's environment. |
**`"error"` kinds:** `"configurationError"` | `"invalidQuote"` |
`"quoteExpired"` | `"genericError"`
Google Pay unavailability is signalled separately through the `"unsupported"`
event, not as an error.
#
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/moonpay-widget
Inline buy widget. Render it wherever it should appear in your layout.
`` renders the MoonPay buy widget inline in your layout. It is
the declarative alternative to
[`client.setupWidget()`](/platform/sdk-reference/react-native/setup-widget).
The widget presents the full buy flow — payment collection, confirmation, and
transaction tracking — in a single embedded frame.
The `quote` prop is mount-time only. Changing it after the component mounts
causes the widget to remount. If you need to update the quote without
remounting, use the headless
[``](/platform/sdk-reference/react-native/components/moonpay-buy-frame)
instead.
```tsx WidgetScreen.tsx theme={null}
import {
MoonPayWidget,
type WidgetEvent,
} from "@moonpay/platform-sdk-react-native";
import { StyleSheet, View } from "react-native";
export function WidgetScreen({ quoteSignature }: { quoteSignature: string }) {
const handleEvent = (event: WidgetEvent) => {
switch (event.kind) {
case "ready":
break;
case "transactionCreated":
console.log(event.payload.transaction.id);
break;
case "complete":
console.log(event.payload.transaction);
break;
case "close":
// The customer closed the widget.
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
};
return (
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});
```
***
## Props
| Prop | Type | Required | Default | Description |
| ----------------------- | ------------------------------ | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | — | Quote `signature` from [`getQuote()`](/platform/sdk-reference/react-native/get-quote). The quote must be executable, or the widget will not render. Mount-time only — changing this prop remounts the widget. |
| `externalTransactionId` | `string` | | — | Partner-assigned identifier for this transaction attempt. Mount-time only. |
| `onEvent` | `(event: WidgetEvent) => void` | | — | Callback for widget lifecycle events. See [`WidgetEvent`](#widgetevent). |
| `style` | `ViewStyle` | | `flex: 1` | Container style. |
### `WidgetEvent`
Use `event.kind` to handle each event.
| kind | Payload | When you receive it |
| ---------------------- | ------------------------------------------------- | ---------------------------------------------------------- |
| `"ready"` | — | The widget UI is rendered and ready. |
| `"transactionCreated"` | `{ transaction: { id: string; status: string } }` | A transaction was created. Use the `id` to track progress. |
| `"complete"` | `{ transaction: FrameTransaction }` | The widget flow finished. |
| `"close"` | — | The customer closed the widget. |
| `"error"` | `{ code: string; message: string }` | The flow encountered an error. |
**`"error"` codes:** `"configurationError"` | `"apiError"` | `"generic"`
# Inline components
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/components/overview
Render MoonPay frames declaratively inside your own layout.
Every MoonPay frame has a corresponding React component you can render directly
in your JSX tree. You control placement, sizing, and animation — the component
mounts the frame where you put it.
## Declarative vs imperative
The SDK exposes two ways to present a frame:
* **Inline components** — you render ``,
``, etc. inside your own layout. The frame appears where the
element sits in the tree.
* **Client methods** — you call `client.setupApplePay()`,
`client.setupWidget()`, etc. The provider presents the frame in a full-screen
modal that it manages for you.
You can mix both approaches in the same app. Inline components are a good fit
when you want the frame embedded in a custom screen. Client methods are
convenient when a full-screen presentation is acceptable.
All components must be rendered inside a
[``](/platform/sdk-reference/react-native/provider).
## The `quote` prop
The `quote` prop on the button and buy frame components accepts a quote
`signature` string from
[`client.getQuote()`](/platform/sdk-reference/react-native/get-quote). This
prop is **reactive**: when you update it, the SDK pushes the new signature
directly into the live frame via `setQuote` without remounting or reloading.
```tsx theme={null}
const [quoteSignature, setQuoteSignature] = React.useState(initialSignature);
// Later, after re-quoting:
setQuoteSignature(newQuote.signature);
// The frame receives the update without a remount.
;
```
All other props — `theme`, `url`, `externalTransactionId` — are **mount-time
only**. Changing them after the component mounts causes the frame to remount.
## Headless components
Three components render nothing visible (0×0) and exist for API uniformity —
they are the declarative alternatives to their imperative counterparts:
| Component | Imperative equivalent |
| -------------------------- | -------------------------- |
| `` | `client.getConnection()` |
| `` | `client.setupBuy()` |
| `` | `client.resetConnection()` |
Results from these components arrive through their `onEvent` callback.
## Component reference
| Component | Description |
| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| [``](/platform/sdk-reference/react-native/components/moonpay-apple-pay-button) | Inline Apple Pay button. `quote` is reactive. |
| [``](/platform/sdk-reference/react-native/components/moonpay-google-pay-button) | Inline Google Pay button. `quote` is reactive. |
| [``](/platform/sdk-reference/react-native/components/moonpay-buy-button) | Inline card buy button. `quote` is reactive. |
| [``](/platform/sdk-reference/react-native/components/moonpay-connect) | Inline connect flow. Credentials applied on active completion. |
| [``](/platform/sdk-reference/react-native/components/moonpay-auth) | Inline auth (email/OTP). Requires `clientToken` in context. |
| [``](/platform/sdk-reference/react-native/components/moonpay-widget) | Inline buy widget. `quote` is mount-time only. |
| [``](/platform/sdk-reference/react-native/components/moonpay-challenge) | Inline 3DS/challenge frame. Mount when a payment frame emits `challenge`. |
| [``](/platform/sdk-reference/react-native/components/moonpay-add-card) | Inline card entry frame. |
| [``](/platform/sdk-reference/react-native/components/moonpay-connection-check) | Headless — checks connection status. |
| [``](/platform/sdk-reference/react-native/components/moonpay-buy-frame) | Headless — drives the buy pipeline. `quote` is reactive. |
| [``](/platform/sdk-reference/react-native/components/moonpay-connection-reset) | Headless — resets the customer's connection. |
# client.connect()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/connect
Render the connect flow in your app.
Use `client.connect()` when you need to connect a customer's MoonPay account in
your app. The provider presents the connect flow in a full-screen native modal
with a slide animation and streams lifecycle events through `onEvent`.
If you want the full end-to-end flow (create session → check connection → connect), start with the [Connect a customer](/platform/guides/connect-a-customer) guide.
For UI recommendations, see [Presentation and appearance](/platform/guides/presentation-and-appearance).
```tsx Connect a customer focus={5-34} theme={null}
import {
useMoonPay,
type ConnectEvent,
} from "@moonpay/platform-sdk-react-native";
export function ConnectScreen() {
const { client } = useMoonPay();
const startConnect = async () => {
const connectResult = await client.connect({
onEvent: (event: ConnectEvent) => {
switch (event.kind) {
case "ready":
// The UI is ready. Reveal the surface if you hid it while loading.
break;
case "complete":
// event.payload is the Connection — same shape as client.getConnection() returns.
if (event.payload.status === "active") {
console.log(event.payload.customer.id);
}
break;
case "error":
// event.payload has a `code` discriminator. On validationError it carries
// a list of field-level errors; on generic it may carry a developer message.
console.error(event.payload);
break;
}
},
});
if (!connectResult.ok) {
// Handle error
console.error(connectResult.error.message);
return;
}
const connectFrame = connectResult.value;
// You can dispose the frame at any time:
// connectFrame.dispose();
};
// ...
}
```
***
## Parameters
| Field | Type | Required | Description |
| ------------------ | ------------------------------- | -------- | ------------------------------------------------------------------------------ |
| `theme` | `object` | | Optional appearance settings for the connect flow. |
| `theme.appearance` | `"dark"` \| `"light"` | | Force a specific appearance. If omitted, the frame uses the system appearance. |
| `onEvent` | `(event: ConnectEvent) => void` | | Callback invoked for connect flow events. See [`ConnectEvent`](#connectevent). |
The provider renders the connect frame in a full-screen modal automatically.
You don't need to pass a container.
### `ConnectEvent`
`onEvent` receives events as the connect flow progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
| `"ready"` | — | The connect UI is rendered and ready to be shown. |
| `"complete"` | [`Connection`](/platform/sdk-reference/react-native/get-connection#connection) | The customer completed the connect flow. Same shape as `client.getConnection()`. |
| `"error"` | [`ConnectEventError`](#connecteventerror) | The flow encountered an error. |
To remove the frame after the flow completes, call `connectFrame.dispose()` on the [`ConnectFrame`](#connectframe) returned from `client.connect()`.
#### `ConnectEventError`
The error event payload comes from the underlying connect frame. It is discriminated by `code`.
| Field | Type | Required | Description |
| --------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `"validationError"` \| `"generic"` | ✅ | The error category. |
| `errors` | `ConfigValidationFieldError[]` | | Present when `code` is `"validationError"`. Field-level errors describing which inputs (such as the session token) failed validation. |
| `message` | `string` | | Present on `"generic"` errors. Developer-friendly details. |
`ConfigValidationFieldError` entries have a `code` of `"invalidSessionToken"`, `"invalidClientToken"`, or `"invalidPublicKey"`, plus a developer-facing `message`.
```ts types.ts theme={null}
type ConfigValidationFieldError = {
code: "invalidSessionToken" | "invalidClientToken" | "invalidPublicKey";
message: string;
};
type ConnectEventError =
| { code: "validationError"; errors: ConfigValidationFieldError[] }
| { code: "generic"; message?: string };
```
## Result
`client.connect()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`ConnectFrame`](#connectframe) | | Present when `ok` is `true`. |
| `error` | [`ConnectError`](#connecterror) | | Present when `ok` is `false`. |
### `ConnectFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ------------------------------------------------ |
| `dispose` | `() => void` | ✅ | Unmounts the frame and detaches event listeners. |
### `ConnectError`
`ConnectError` covers failures to mount the frame or complete the handshake,
and customer dismissal. Per-flow failures inside the frame surface with full
details through the `"error"` event payload,
[`ConnectEventError`](#connecteventerror); the method then resolves with a
generic `ConnectError`.
| Field | Type | Required | Description |
| --------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message` | `string` | ✅ | A developer-friendly description of the failure. `"Connect flow dismissed"` when the customer closes the modal before completing (including mid-loading). |
```ts types.ts theme={null}
type ConnectFrame = {
dispose: () => void;
};
type ConnectError = {
message: string;
};
```
## Resources
For full protocol details, see the [connect frame reference](/platform/frames/connect).
# client.deletePaymentMethod()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/delete-payment-method
Delete a stored payment method for the connected customer.
Use this method to remove a stored payment method — for example, when a customer asks to forget a card, or after a one-off purchase where you don't want to retain card details. For request and response details, see the [Delete payment method API](/api-reference/platform/endpoints/payment-methods/delete).
To obtain a `paymentMethodId`, list the customer's stored payment methods with [`client.getPaymentMethods()`](/platform/sdk-reference/react-native/get-payment-methods).
```tsx Delete a payment method focus={5-19} theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function ForgetCardButton({
paymentMethodId,
}: {
paymentMethodId: string;
}) {
const { client } = useMoonPay();
const handleDelete = async () => {
// Call this after the client has an active connection.
const result = await client.deletePaymentMethod(paymentMethodId);
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
// On success, `result.value` is `undefined`.
};
// ...
}
```
***
## Parameters
| Parameter | Type | Required | Description |
| ----------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodId` | `string` | ✅ | The `id` of the stored card to delete. Obtain this from [`client.getPaymentMethods()`](/platform/sdk-reference/react-native/get-payment-methods#storedcardpaymentmethod). |
## Result
`client.deletePaymentMethod()` returns a `Result`.
The API responds with `204 No Content` on success, so `result.value` is `undefined` — there is no response body to read. Check `result.ok` to confirm the delete succeeded.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------------- | -------- | ------------------------------------------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `undefined` | | Present when `ok` is `true`. Always `undefined` (no response body). |
| `error` | [`DevPlatformApiError`](#devplatformapierror) | | Present when `ok` is `false`. |
### `DevPlatformApiError`
The standard MoonPay Platform API error shape.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"not_found"`, `"unauthorized"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
```ts types.ts theme={null}
type DevPlatformApiError = {
code: DevPlatformApiErrorCode;
message: string;
errors?: DevPlatformApiErrorDetail[];
};
type DevPlatformApiErrorDetail = {
field?: string;
message: string;
};
```
# client.getConnection()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/get-connection
Check whether the customer already has an active connection.
Use this method to check whether the current customer already has an active connection. If the customer is not connected (or the connection expired), run the connect flow with [`client.connect()`](/platform/sdk-reference/react-native/connect).
The SDK runs the check in a hidden WebView, decrypts the returned credentials, and primes the client so that subsequent SDK calls (such as `getPaymentMethods()` or `getQuote()`) are authenticated automatically.
```tsx Get a connection focus={5-30} theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function ConnectionGate() {
const { client } = useMoonPay();
const checkConnection = async () => {
const result = await client.getConnection();
if (!result.ok) {
// Handle error
console.error(result.error.message);
return;
}
switch (result.value.status) {
case "active":
console.log(result.value.customer.id);
break;
case "connectionRequired":
// Render the connect flow with client.connect().
break;
case "termsAcceptanceRequired":
// Show your own Terms of Use UI, then create a new session with
// termsAcceptedAt and relaunch the flow.
break;
case "pending":
// KYC decision delayed. Retry later.
break;
case "failed":
console.error(result.value.reason);
break;
case "unavailable":
// Restricted location. Surface a fallback experience.
break;
}
};
// ...
}
```
***
## Parameters
`client.getConnection()` accepts an optional `options` object.
| Field | Type | Required | Description |
| --------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `skipKyc` | `boolean` | | Pass `true` for headless/Customer API integrations so the check frame opts out of KYC-based statuses. Legal statuses (such as `"termsAcceptanceRequired"`) are always surfaced regardless. Defaults to `false`. |
```ts Usage theme={null}
const result = await client.getConnection({ skipKyc: true });
```
***
## Result
`client.getConnection()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`Connection`](#connection) | | Present when `ok` is `true`. |
| `error` | [`GetConnectionError`](#getconnectionerror) | | Present when `ok` is `false`. |
### `Connection`
`Connection` is a discriminated union over `status`. The fields present depend on the status:
| Variant | Fields | Description |
| --------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"active"` | `status`, `customer`, `credentials`, `capabilities` | The customer is connected. The SDK has already decrypted credentials internally — you do not need to send the `credentials` string anywhere. |
| `"connectionRequired"` | `status`, `credentials`, `capabilities?`, `mismatch?` | The customer needs to complete the connect flow. The SDK has decrypted the anonymous tokens internally; `capabilities` is present when guest checkout is enabled for the partner. `mismatch` is present and `true` when the session's email and phone number resolve to different MoonPay customers. |
| `"termsAcceptanceRequired"` | `status` | The customer has no valid Terms of Use attestation on file (headless/Customer API partners). Display the Terms of Use yourself, pass the acceptance timestamp as `termsAcceptedAt` when you create a new session (`POST /platform/v1/sessions`), and relaunch the flow. Requires the Identity or Guest Checkout account capability. See [Terms acceptance](/platform/guides/terms-acceptance). |
| `"pending"` | `status` | The KYC decision is delayed and may resolve on a subsequent visit. |
| `"failed"` | `status`, `reason` | Terminal failure (for example, KYC rejection). |
| `"unavailable"` | `status` | The customer is in a restricted location. |
#### Fields
| Field | Type | Required | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status` | `"active"` \| `"connectionRequired"` \| `"termsAcceptanceRequired"` \| `"pending"` \| `"failed"` \| `"unavailable"` | ✅ | The connection status. |
| `customer` | `object` | | Present when `status` is `"active"`. Contains the connected MoonPay customer. |
| `customer.id` | `string` | | The MoonPay customer identifier. |
| `customer.country` | `string` | | The customer's ISO 3166-1 alpha-3 residential country code (for example, `"USA"` or `"FRA"`). Use this to determine which payment disclosures apply. |
| `customer.administrativeArea` | `string` | | The customer's state or province code, included when disclosures apply at the subdivision level (for example, `"NY"` or `"WA"`). |
| `customer.area` | `string` | | The broader regulatory area for the customer's country, when applicable. Currently `"EEA"`. |
| `credentials` | `string` | | Present when `status` is `"active"` or `"connectionRequired"`. A base64-encoded, X25519+AES-GCM-encrypted token blob. The SDK decrypts this internally — you don't need to handle it directly. |
| `capabilities` | [`CustomerCapabilities`](#customercapabilities) | | Required on `"active"`; optional on `"connectionRequired"` (present when guest checkout is enabled for the partner). |
| `mismatch` | `boolean` | | Present only when `status` is `"connectionRequired"` and the session's email and phone number resolve to different MoonPay customers (a conflict). When `true`, route the customer through the connect flow before rendering payment UI such as Apple Pay or Google Pay. Absent when there is no conflict — never `false`. |
| `reason` | `string` | | Present only when `status` is `"failed"`. Developer-friendly failure details. |
#### `CustomerCapabilities`
Capabilities and regulatory requirements for the customer.
| Field | Type | Required | Description |
| --------------- | ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `ramps` | [`RampsCapability`](#rampscapability) | | Capabilities for buy and sell flows. |
| `guestCheckout` | [`RampsCapability`](#rampscapability) | | Capabilities for the guest checkout flow. Present when guest checkout is enabled for the partner and the session is a guest-checkout session. |
#### `RampsCapability`
| Field | Type | Required | Description |
| -------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `requirements` | `RampsRequirements` | ✅ | Regulatory requirements that apply to ramps for the customer. `paymentDisclosures` is deprecated — read geography from `customer.country`, `customer.administrativeArea`, and `customer.area` instead. |
```ts types.ts theme={null}
type Customer = {
id: string;
country?: string;
administrativeArea?: string;
area?: string;
};
type Connection =
| {
status: "active";
customer: Customer;
credentials: string;
capabilities: CustomerCapabilities;
}
| {
status: "connectionRequired";
credentials: string;
capabilities?: CustomerCapabilities;
mismatch?: boolean;
}
| { status: "termsAcceptanceRequired" }
| { status: "pending" }
| { status: "failed"; reason: string }
| { status: "unavailable" };
```
### `GetConnectionError`
`GetConnectionError` covers failures to run the connection check (for example, a frame handshake timeout).
| Field | Type | Required | Description |
| --------- | -------- | -------- | ------------------------------------------------ |
| `message` | `string` | ✅ | A developer-friendly description of the failure. |
# client.getPaymentMethods()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/get-payment-methods
List payment methods available for the connected customer.
Use this method to list the customer's available payment methods, plus any cards they have on file. For request and response details, see the [List payment methods API](/api-reference/platform/endpoints/payment-methods/list).
```tsx Get payment methods focus={5-16} theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function PaymentMethodsList() {
const { client } = useMoonPay();
const load = async () => {
// Call this after the client has an active connection (for example, after
// `client.getConnection()` returns `status: "active"` or after `client.connect()`
// completes).
const result = await client.getPaymentMethods();
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
console.log(result.value.data.paymentMethodConfigs); // Available payment-method configs
console.log(result.value.data.paymentMethods); // Stored payment methods, such as saved cards (if any)
};
// ...
}
```
***
## Result
`client.getPaymentMethods()` returns a `Result<{ data: ListPaymentMethodsResponse }, GetPaymentMethodsError>`.
### Result envelope
`Result<{ data: ListPaymentMethodsResponse }, GetPaymentMethodsError>`
| Field | Type | Required | Description |
| ------- | ------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`ListPaymentMethodsResponse`](#listpaymentmethodsresponse) `}` | | Present when `ok` is `true`. |
| `error` | [`GetPaymentMethodsError`](#getpaymentmethodserror) | | Present when `ok` is `false`. |
### `ListPaymentMethodsResponse`
| Field | Type | Required | Description |
| ---------------------- | ------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodConfigs` | [`PaymentMethodConfig`](#paymentmethodconfig)`[]` | | Payment methods (Apple Pay, Google Pay, card, etc.) available to the customer with their capabilities and availability. |
| `paymentMethods` | [`StoredPaymentMethod`](#storedpaymentmethod)`[]` | | Payment methods the customer has stored on file, such as saved cards. |
#### `PaymentMethodConfig`
| Field | Type | Required | Description |
| -------------- | --------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `PaymentMethodType` | ✅ | The payment method type. See the [Get a quote API reference](/api-reference/platform/endpoints/quotes/get) for supported values. |
| `capabilities` | [`PaymentMethodCapabilities`](#paymentmethodcapabilities) | ✅ | Details about how this payment method can be used. |
| `availability` | [`PaymentMethodAvailability`](#paymentmethodavailability) | ✅ | Whether this payment method is available for the current session. |
Bank transfers appear here alongside cards and wallets: `sepa` for SEPA (EUR). Offer `sepa` in the headless flow only when `availability.active` is `true` and `capabilities.requiresWidget` is `false` (a `requiresWidget: true` method must complete in the MoonPay widget instead). SEPA doesn't create a stored payment method, so it never appears in `paymentMethods` (which lists stored cards). To pay with a bank transfer, quote against its `type` and open the headless Buy frame. See [Bank transfer](/platform/sdk-reference/react-native/setup-buy#bank-transfer).
#### `PaymentMethodCapabilities`
| Field | Type | Required | Description |
| --------------------------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `supportedCurrencies` | `string[]` | ✅ | A list of [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) fiat currency codes that this payment method can be used with. |
| `supportedTransactionTypes` | `string[]` | ✅ | The kinds of transactions this payment method can be used for. |
| `allowsDeletion` | `boolean` | ✅ | Whether this payment method can be deleted via `client.deletePaymentMethod()`. |
| `requiresWidget` | `boolean` | ✅ | Whether this payment method requires the MoonPay widget to complete the payment flow. |
#### `PaymentMethodAvailability`
| Field | Type | Required | Description |
| --------- | ---------- | -------- | ------------------------------------------------------------------------- |
| `active` | `boolean` | ✅ | Whether this payment method is available for the current session. |
| `reasons` | `string[]` | | If the payment method is unavailable, a list of machine-readable reasons. |
#### `StoredPaymentMethod`
A payment method the customer has stored on file. Use the `id` directly in `client.getQuote()` to quote against a specific stored payment method. Stored cards extend the base shape with card details — see the [API reference](/api-reference/platform/endpoints/payment-methods/list) for the full shape, including network brand, expiry, and `last4`.
| Field | Type | Required | Description |
| ------ | -------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The stored payment method identifier. Pass this to `getQuote({ paymentMethod: { type: "card", id } })`. |
| `type` | `string` | ✅ | The payment method type, typically `"card"`. |
### `GetPaymentMethodsError`
`GetPaymentMethodsError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"unauthorized"`, `"not_found"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
```ts types.ts theme={null}
type ListPaymentMethodsResponse = {
paymentMethodConfigs?: PaymentMethodConfig[];
paymentMethods?: StoredPaymentMethod[];
};
type StoredPaymentMethod = {
id: string;
type: PaymentMethodType;
};
type PaymentMethodConfig = {
type: PaymentMethodType;
capabilities: PaymentMethodCapabilities;
availability: PaymentMethodAvailability;
};
type PaymentMethodCapabilities = {
supportedCurrencies: string[];
supportedTransactionTypes: string[];
allowsDeletion: boolean;
requiresWidget: boolean;
};
type PaymentMethodAvailability = {
active: boolean;
reasons?: string[];
};
type GetPaymentMethodsError = {
code: DevPlatformApiErrorCode;
message: string;
errors?: DevPlatformApiErrorDetail[];
};
```
# client.getQuote()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/get-quote
Request a quote for a transaction.
Use this method to request a quote for fiat-to-crypto transactions. Quotes include fees and limits, and they expire after a short time window. A quote with `executable: true` can be used to execute a transaction.
```tsx Get a quote theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function QuoteScreen() {
const { client } = useMoonPay();
const requestQuote = async () => {
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890123456789012345678901234567890" },
paymentMethod: { type: "apple_pay" },
});
if (!quoteResult.ok) {
// Handle error
console.error(quoteResult.error.code, quoteResult.error.message);
return;
}
console.log(quoteResult.value.data);
};
// ...
}
```
***
## Parameters
`client.getQuote()` takes a single `input` object. The shape matches the [Get quotes API](/api-reference/platform/endpoints/quotes/get) request body — see that page for the complete field reference.
| Field | Type | Required | Description |
| ------------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source` | `object` | ✅ | The fiat side of the quote. |
| `source.asset.code` | `string` | ✅ | The fiat currency code as [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) (for example, `"USD"`). |
| `source.amount` | `string` | | The amount to spend, as a string (for example, `"100.00"`). Set the amount on either `source` or `destination`, not both. |
| `destination` | `object` | ✅ | The crypto side of the quote. |
| `destination.asset.code` | `string` | ✅ | The crypto asset code the customer receives (for example, `"ETH"`, `"USDC_SOL"`). |
| `destination.amount` | `string` | | The amount the customer receives, if known. Set the amount on either `source` or `destination`, not both. |
| `wallet` | `object` | | Where the crypto is sent. Required for the quote to be `executable`. |
| `wallet.address` | `string` | ✅ | The destination wallet address. |
| `wallet.tag` | `string` | | An optional memo or destination tag, used by some blockchains such as XRP or XLM. |
| `paymentMethod` | `object` | | The payment method to quote against. Required for the quote to be `executable`. |
| `paymentMethod.type` | `string` | ✅ | The payment method type. See the [Get a quote API reference](/api-reference/platform/endpoints/quotes/get) for supported values. |
| `paymentMethod.id` | `string` | | The stored payment method ID. Required when `type` is `"card"` to identify the specific card. |
| `feeBehavior` | `string` | | How fees relate to `source.amount`. One of `"inclusive"` or `"exclusive"`. Defaults to `"inclusive"`. Only applies when you quote by `source.amount`; it is ignored when you quote by `destination.amount`. |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
### Payment method types
`paymentMethod.type` accepts the payment methods available to the connected customer, including bank transfers. See the [Get a quote API reference](/api-reference/platform/endpoints/quotes/get) for the full list.
| `type` | Payment method |
| ------------ | ------------------ |
| `apple_pay` | Apple Pay |
| `google_pay` | Google Pay |
| `card` | Stored card |
| `sepa` | SEPA bank transfer |
The fiat currencies each method supports come from [`getPaymentMethods`](/platform/sdk-reference/react-native/get-payment-methods) (`capabilities.supportedCurrencies`); `sepa` is EUR only.
For bank transfers, quote with `paymentMethod.type` set to `"sepa"` (EUR), then execute the quote with the same headless Buy frame you use for cards. See [Bank transfer](/platform/sdk-reference/react-native/setup-buy#bank-transfer).
Bank transfers are a floating payment method: the quote is an estimate, and the exact amount is set when the customer's funds settle. Bank-transfer quotes return `exchangeRateType: "floating"`; card and wallet methods return `exchangeRateType: "fixed"`. When `exchangeRateType` is `"floating"`, render the estimated crypto amount with a tilde (for example, `~0.2345 BTC`) and tell the customer the final amount is set when the transfer settles. See [Exchange rate type](#exchange-rate-type).
```tsx Quote for a SEPA bank transfer theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "EUR" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890123456789012345678901234567890" },
paymentMethod: { type: "sepa" },
});
```
Bank transfers don't support DeFi assets. A DeFi asset is a
`destination.asset` identified by `caip19`. If you request a bank-transfer
quote (`sepa`) for one, the request fails with `400` and the message "Bank
transfers are not supported for DeFi assets." Offer a card or wallet payment
method for these assets instead.
### Fee behavior
`feeBehavior` controls whether fees are taken out of `source.amount` or added on top of it. It only applies when you quote by `source.amount`.
* `"inclusive"` (default): the customer pays exactly `source.amount`, and fees are carved out of it. Less of the source amount is converted, so the customer receives less crypto. This is the existing behavior, so omitting `feeBehavior` keeps quotes unchanged.
* `"exclusive"`: fees are added on top of `source.amount`, so the customer pays `source.amount` plus fees. The full `source.amount` is converted, so the customer receives more crypto than the inclusive quote for the same input.
When you quote by `destination.amount`, MoonPay ignores `feeBehavior` and the quote is always fees-inclusive. The response always echoes the effective `feeBehavior`: the value you requested for source-amount quotes, or `"inclusive"` for destination-amount quotes.
```tsx Quote with fees added on top theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890123456789012345678901234567890" },
paymentMethod: { type: "apple_pay" },
feeBehavior: "exclusive",
});
```
## Result
`client.getQuote()` returns a `Result<{ data: Quote }, GetQuoteError>`.
### Result envelope
`Result<{ data: Quote }, GetQuoteError>`
| Field | Type | Required | Description |
| ------- | --------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`Quote`](#quote) `}` | | Present when `ok` is `true`. |
| `error` | [`GetQuoteError`](#getquoteerror) | | Present when `ok` is `false`. |
### `Quote`
A quote includes a `signature` you use to execute a transaction, plus fees and limits. See the [API reference](/api-reference/platform/endpoints/quotes/get) for the full shape.
| Field | Type | Required | Description |
| ------------------ | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `signature` | `string` | ✅ | A stringified JSON object that contains quote data and an embedded hash. Don't deserialize this value. Use it as-is to execute a transaction. |
| `expiresAt` | `string` | ✅ | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp for when the quote expires. |
| `source` | `object` | ✅ | The source (fiat) asset details. |
| `destination` | `object` | ✅ | The destination (crypto) asset details. |
| `wallet` | `object` | ✅ | The destination wallet details. |
| `fees` | `object` | ✅ | Fee details for the transaction. |
| `feeBehavior` | `string` | ✅ | The effective fee behavior for this quote, `"inclusive"` or `"exclusive"`. Echoes the requested value for source-amount quotes, or `"inclusive"` for destination-amount quotes. |
| `executable` | `boolean` | ✅ | Whether the quote can be used to execute a transaction. See the [API reference](/api-reference/platform/endpoints/quotes/get) for the request fields required to receive `executable: true`. |
| `exchangeRate` | `string` | ✅ | The rate used to convert between fiat and crypto, as the fiat value of one unit of crypto. Pairs with `exchangeRateType`. |
| `exchangeRateType` | `string` | ✅ | `"fixed"` when the exchange rate is locked at quote time (card/wallet), or `"floating"` when it's an estimate confirmed at settlement (bank transfers such as SEPA). Pairs with the `exchangeRate` field. See [Exchange rate type](#exchange-rate-type). |
| `challenge` | `object` | | Present when the customer must clear a step before this quote can be executed. Carries `kind` and `url`. Mount the [challenge frame](/platform/frames/challenge) at `url`, then request the quote again. See [Upgrade a guest account](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up). |
### Exchange rate type
`exchangeRateType` tells you whether the quoted amount is final or an estimate, and pairs with the `exchangeRate` field. Bank transfers (SEPA) are a floating payment method: the quote is an estimate, and the exact amount is confirmed when the customer's funds settle. These quotes return `exchangeRateType: "floating"`. Card and wallet methods lock the rate at quote time and return `exchangeRateType: "fixed"`.
When `exchangeRateType` is `"floating"`, render the estimated crypto amount with a tilde (for example, `~0.2345 BTC`) and tell the customer the final amount is set when the transfer settles. When it is `"fixed"`, the quoted amount is final.
Branch on `exchangeRateType` when you format the amount the customer receives, so floating quotes always render with a tilde:
```tsx Render the crypto amount theme={null}
// Frontend: the customer receives the destination (crypto) amount
function formatCryptoAmount(quote: Quote): string {
const amount = `${quote.destination.amount} ${quote.destination.asset.code}`;
// Floating quotes stay estimates until the transfer settles, so flag them.
return quote.exchangeRateType === "floating" ? `~${amount}` : amount;
}
// "~0.2345 BTC" for a SEPA quote; "0.2345 BTC" for a card or wallet quote
```
### `GetQuoteError`
`GetQuoteError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"invalid_request"`, `"unauthorized"`, `"unprocessable_entity"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors when the request fails validation. Each entry has a `field` and `message`. |
```ts types.ts theme={null}
type DevPlatformApiErrorCode =
| "unknown_error"
| "unauthorized"
| "forbidden"
| "not_found"
| "invalid_request"
| "not_implemented"
| "too_many_requests"
| "not_allowed"
| "conflict"
| "unprocessable_entity"
| "sse_timeout"
| "sse_error"
| "service_unavailable"
| "requirements_incomplete"
| "verification_rejected"
| "country_mismatch"
| "unsupported_country";
type DevPlatformApiErrorDetail = {
field?: string;
message: string;
};
type GetQuoteError = {
code: DevPlatformApiErrorCode;
message: string;
errors?: DevPlatformApiErrorDetail[];
};
```
# client.getTransaction()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/get-transaction
Fetch a single transaction by ID.
Use this method to fetch a single transaction by its ID, including its stage breakdown. Call it after a payment flow completes (for example, when [``](/platform/sdk-reference/react-native/components/moonpay-buy-button) or [`client.setupBuy()`](/platform/sdk-reference/react-native/setup-buy) emits `complete`) to poll for the final transaction status.
For bank-transfer payments (SEPA), this endpoint also returns the deposit details your app renders so the customer can pay. See [Bank-transfer deposit details](#bank-transfer-deposit-details).
For request and response details, see the [Get a transaction API](/api-reference/platform/endpoints/transactions/get).
```tsx Get a transaction focus={5-15} theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function TransactionDetails({
transactionId,
}: {
transactionId: string;
}) {
const { client } = useMoonPay();
const load = async () => {
const result = await client.getTransaction(transactionId);
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
console.log(result.value.data);
};
// ...
}
```
***
## Parameters
`client.getTransaction()` takes a single positional argument.
| Argument | Type | Required | Description |
| -------- | -------- | -------- | ---------------------------------- |
| `id` | `string` | ✅ | The MoonPay ID of the transaction. |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
## Result
`client.getTransaction()` returns a `Result<{ data: TransactionWithStages }, GetTransactionsError>`.
### Result envelope
`Result<{ data: TransactionWithStages }, GetTransactionsError>`
| Field | Type | Required | Description |
| ------- | --------------------------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`TransactionWithStages`](#transactionwithstages) `}` | | Present when `ok` is `true`. |
| `error` | [`GetTransactionsError`](#gettransactionserror) | | Present when `ok` is `false`. |
### `TransactionWithStages`
A transaction with its stage breakdown. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for every field, and the [Get a transaction API](/api-reference/platform/endpoints/transactions/get) for the response shape.
| Field | Type | Required | Description |
| ------------------------- | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The MoonPay ID of the transaction. |
| `status` | `TransactionStatus` | ✅ | The current transaction status. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for the full list of statuses. |
| `source` | `object` | ✅ | The source amount and asset (fiat currency). |
| `destination` | `object` | ✅ | The destination amount and asset (cryptocurrency). |
| `createdAt` | `string` | ✅ | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp of when the transaction was created. |
| `stages` | `Stage[]` | ✅ | The transaction's pipeline stages, each with a `kind` and `status`. |
| `bankTransferDepositInfo` | `object` | | The bank account details the customer sends funds to. Present only on bank-transfer transactions (SEPA). See [Bank-transfer deposit details](#bank-transfer-deposit-details). |
### Bank-transfer deposit details
Bank-transfer payments (SEPA) settle when the customer sends funds to a MoonPay bank account. For these transactions, the response includes a `bankTransferDepositInfo` object with the account details and the payment reference. The transaction's `source.amount` (with `source.asset.code`) is the exact fiat amount the customer must send; render it alongside the deposit details.
Bank transfers are a floating payment method: the crypto amount is only final at settlement. While the transaction is pending, its `destination.amount` is a floating estimate, the same estimate the quote returns, so you don't need to retain the original quote. Render `destination.amount` with a tilde, making clear it's an estimate until the transfer lands. At settlement it becomes the final amount delivered. See [Exchange rate type](/platform/sdk-reference/react-native/get-quote#exchange-rate-type).
Your app renders these details natively so the customer can complete the transfer from their banking app. MoonPay does not render the deposit UI. Read the fields from `bankTransferDepositInfo` and display them in your own screen, then keep polling `client.getTransaction()` to track the transaction to a terminal status.
The customer must include the payment `reference` with their bank transfer.
Transfers sent without the reference are rejected. Surface this prominently in
your own UI, for example, "Always include your payment reference or your
transfer will be rejected."
| Field | Type | Required | Description |
| ------------------ | -------- | -------- | ------------------------------------------------------------------ |
| `reference` | `string` | ✅ | The payment reference the customer must include with the transfer. |
| `recipientName` | `string` | ✅ | The name of the recipient that receives the funds. |
| `recipientAddress` | `string` | ✅ | The address of the recipient. |
| `iban` | `string` | | The IBAN. Present for SEPA (EUR). |
| `bic` | `string` | | The BIC / SWIFT code. Present for SEPA (EUR). |
| `bankName` | `string` | | The name of the receiving bank. |
| `bankAddress` | `string` | | The address of the receiving bank. |
Show the customer the exact `reference` value. MoonPay uses it to match the
incoming transfer to this transaction. A missing or altered reference means
the transfer is rejected.
### `GetTransactionsError`
`GetTransactionsError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"not_found"`, `"unauthorized"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
## Example: poll until terminal status
After a payment flow emits `complete`, poll `client.getTransaction()` until the transaction reaches a terminal status. The set of terminal statuses depends on the flow — check the [Transaction object](/api-reference/platform/objects-and-types/transaction) reference for the values that apply.
```ts Poll for final status theme={null}
const TERMINAL_STATUSES = new Set(["completed", "failed"]);
async function pollTransaction(transactionId: string) {
while (true) {
const result = await client.getTransaction(transactionId);
if (!result.ok) {
throw new Error(result.error.message);
}
const transaction = result.value.data;
if (TERMINAL_STATUSES.has(transaction.status)) {
return transaction;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
```
Poll this endpoint to track status, or use the existing
[`transaction-updated`](/api-reference/widget/webhooks/transaction-updated)
webhook if you already consume MoonPay webhooks. Bank-transfer transactions
stay `pending` from creation until they settle. When the customer's deposit
arrives, the `status` is still `pending`, so check the `stages` array: when
the `waiting_payment` stage's `status` is `"success"`, the deposit has
arrived.
# client.listTransactions()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/list-transactions
List the connected customer's transactions with optional filters and pagination.
Use this method to list the connected customer's transactions. You can filter by date range and page through results with a cursor.
For request and response details, see the [List transactions API](/api-reference/platform/endpoints/transactions/list).
```tsx List transactions focus={5-16} theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function TransactionsScreen() {
const { client } = useMoonPay();
const load = async () => {
const result = await client.listTransactions({
startDate: "2026-01-01",
endDate: "2026-01-31",
limit: 20,
});
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
console.log(result.value.data); // Transaction[]
console.log(result.value.pageInfo); // Pagination info
};
// ...
}
```
***
## Parameters
`client.listTransactions()` takes an optional `params` object. Call it with no arguments to fetch the most recent transactions without filters.
| Field | Type | Required | Description |
| ----------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `startDate` | `string` | | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date. Only return transactions created on or after this date. |
| `endDate` | `string` | | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date. Only return transactions created on or before this date. |
| `cursor` | `string` | | A pagination cursor returned from a previous call. Use it to fetch the next page. |
| `limit` | `number` | | The maximum number of transactions to return in one page. |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
## Result
`client.listTransactions()` returns a `Result<{ data: Transaction[]; pageInfo: PaginationInfo }, GetTransactionsError>`.
### Result envelope
`Result<{ data: Transaction[]; pageInfo: PaginationInfo }, GetTransactionsError>`
| Field | Type | Required | Description |
| ------- | ---------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`Transaction`](#transaction)`[]; pageInfo:` [`PaginationInfo`](#paginationinfo) `}` | | Present when `ok` is `true`. |
| `error` | [`GetTransactionsError`](#gettransactionserror) | | Present when `ok` is `false`. |
### `Transaction`
Each entry in `data` is a full transaction. Key fields include `id`, `status`, `source.amount`, `destination.amount`, and `createdAt`. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for every field.
| Field | Type | Required | Description |
| ------------- | ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The MoonPay ID of the transaction. |
| `status` | `TransactionStatus` | ✅ | The current transaction status. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for the full list of statuses. |
| `source` | `object` | ✅ | The source amount and asset (fiat currency). |
| `destination` | `object` | ✅ | The destination amount and asset (cryptocurrency). |
| `createdAt` | `string` | ✅ | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp of when the transaction was created. |
### `PaginationInfo`
Cursor-based pagination details returned alongside the data. See the [List transactions API](/api-reference/platform/endpoints/transactions/list) for the exact field names used in this response.
### `GetTransactionsError`
`GetTransactionsError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"unauthorized"`, `"invalid_request"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
## Example: page through transactions
Use the cursor returned in `pageInfo` to fetch additional pages. The exact cursor field name is documented in the [List transactions API](/api-reference/platform/endpoints/transactions/list) response.
```ts Page through transactions theme={null}
async function listAllTransactions() {
const all = [];
let cursor: string | undefined;
do {
const result = await client.listTransactions({ limit: 50, cursor });
if (!result.ok) {
throw new Error(result.error.message);
}
all.push(...result.value.data);
// pageInfo carries the cursor for the next page; see the API reference
// for the exact field name (for example, `pageInfo.endCursor`).
cursor = result.value.pageInfo.endCursor;
} while (cursor);
return all;
}
```
# Overview
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/overview
Use the React Native SDK to build fiat-to-crypto ramps with headless payments in your mobile app.
The React Native SDK wraps the MoonPay Developer Platform in a `` and a `useMoonPay()` hook. The SDK ships with TypeScript types for autocomplete and inference.
The SDK is tested against:
* React Native 0.73+
* React `>=18`
* `react-native-webview` 13.0+
* iOS 14+ and Android API 26+
## Install
Install the SDK package and its peer dependency:
```bash pnpm theme={null}
pnpm i @moonpay/platform-sdk-react-native react-native-webview
```
```bash bun theme={null}
bun add @moonpay/platform-sdk-react-native react-native-webview
```
```bash npm theme={null}
npm i @moonpay/platform-sdk-react-native react-native-webview
```
Follow the [`react-native-webview` install guide](https://github.com/react-native-webview/react-native-webview/blob/master/docs/Getting-Started.md) to link the native module. On iOS, run `pod install` from your `ios/` directory.
## Conventions
### Provider + hook
Mount `` near the top of your app tree and call `useMoonPay()`
from any descendant component to get the client. Pass the
[`sessionToken`](/platform/sdk-reference/react-native/provider#props) at mount,
or supply it later with [`initialize()`](/platform/sdk-reference/react-native/use-moonpay#initialize).
The SDK presents frames in two ways:
* **Client methods** such as `client.connect()` or `client.setupWidget()` open
a full-screen modal managed by the provider. Hidden utility frames (connection
check, headless buy, reset) render at zero size.
* **Inline components** such as `` or `` render
the frame wherever you place them in your layout.
```tsx App.tsx theme={null}
import { MoonPayProvider } from "@moonpay/platform-sdk-react-native";
export default function App() {
return (
);
}
```
### `Result`
Most SDK functions return a `Result` instead of throwing.
| Field | Type | Required | Description |
| ------- | --------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `T` | | Present when `ok` is `true`. |
| `error` | `E` | | Present when `ok` is `false`. |
Use this pattern to branch on success vs failure:
```ts theme={null}
const result = await client.getConnection();
if (!result.ok) {
// Handle error
console.error(result.error);
return;
}
console.log(result.value);
```
## Function reference
These pages document the provider, hook, and individual SDK functions:
## Inline components
Every frame also has a declarative component you can render directly in your
JSX tree. Components are a good fit when you want a frame embedded in a custom
screen. The three deprecated methods below have direct component replacements.
| Deprecated method | Component replacement |
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `client.setupApplePay()` | [``](/platform/sdk-reference/react-native/components/moonpay-apple-pay-button) |
| `client.setupGooglePay()` | [``](/platform/sdk-reference/react-native/components/moonpay-google-pay-button) |
| `client.setupBuyButton()` | [``](/platform/sdk-reference/react-native/components/moonpay-buy-button) |
# MoonPayProvider
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/provider
Mount the provider once to make the SDK available throughout your React Native app.
`` initializes the SDK with a [`sessionToken`](/platform/guides/api-and-sdk-credentials#session-token) and renders MoonPay frames as `react-native-webview` instances when you call SDK methods. Wrap the part of your app that needs access to the client.
```tsx App.tsx theme={null}
import { MoonPayProvider } from "@moonpay/platform-sdk-react-native";
export default function App() {
return (
);
}
```
Read the client from descendant components with [`useMoonPay()`](/platform/sdk-reference/react-native/use-moonpay).
***
## Props
| Property | Type | Required | Description |
| -------------- | ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionToken` | `string` | | The [sessionToken](/platform/guides/api-and-sdk-credentials#session-token) created on your server. Optional — omit it and call [`initialize()`](/platform/sdk-reference/react-native/use-moonpay#initialize) once you have the token. When set, the prop takes precedence over an imperatively-set token. |
| `children` | `ReactNode` | ✅ | Your app tree. Components below the provider can call `useMoonPay()`. |
| `apiBaseUrl` | `string` | | Override the MoonPay Platform API base URL. Defaults to production. Use this only when MoonPay support has asked you to point at a non-production environment. |
| `frameBaseUrl` | `string` | | Override the MoonPay frame base URL. Defaults to production. Use this only when MoonPay support has asked you to point at a non-production environment. |
## Providing the session token later
The `sessionToken` prop is optional. If you don't have the token when the provider mounts — for example, you fetch it from your server after the customer signs in — mount `` without it and call [`initialize()`](/platform/sdk-reference/react-native/use-moonpay#initialize) once the token is available.
```tsx App.tsx theme={null}
import { MoonPayProvider } from "@moonpay/platform-sdk-react-native";
export default function App() {
return (
);
}
```
```tsx SignInScreen.tsx theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function SignInScreen() {
const { initialize } = useMoonPay();
const handleSignIn = async () => {
const sessionToken = await fetchSessionTokenFromYourServer();
initialize(sessionToken);
};
// ...
}
```
The client that carries the new token becomes available on the next render, so drive your connection flow from a component that reads [`isInitialized`](/platform/sdk-reference/react-native/use-moonpay#isinitialized) rather than calling a method on a `client` you captured before `initialize()`. Calling a connection method before a token is set returns an `err()` result.
If you pass the `sessionToken` prop and also call `initialize()`, the prop
wins — pick one approach. Calling `initialize()` again replaces the token (for
example, after a refresh or when switching accounts) and clears any
credentials cached from the previous session.
## Frame rendering
When you call a method that opens a visible frame — such as `client.connect()`,
`client.setupWidget()`, or `client.setupAddCard()` — the provider presents it
in a full-screen native `Modal` with a slide animation. Multiple frames stack
naturally: for example, a challenge frame appears on top of an active widget.
On Android, the hardware back button dismisses the top frame and disposes it.
Hidden utility frames (used by `client.getConnection()`, `client.setupBuy()`,
and `client.resetConnection()`) render as zero-size siblings with no visible
impact on your layout.
```tsx Layout theme={null}
{/* Hidden utility frames are mounted here as zero-size siblings.
Visible frames appear in full-screen modals above your content. */}
```
## Client methods
The hook exposes a `client` object with every integration method:
* [`client.getConnection()`](/platform/sdk-reference/react-native/get-connection)
* [`client.connect()`](/platform/sdk-reference/react-native/connect)
* [`client.setupAuth()`](/platform/sdk-reference/react-native/setup-auth)
* [`client.resetConnection()`](/platform/sdk-reference/react-native/reset-connection)
* [`client.getPaymentMethods()`](/platform/sdk-reference/react-native/get-payment-methods)
* [`client.deletePaymentMethod()`](/platform/sdk-reference/react-native/delete-payment-method)
* [`client.getQuote()`](/platform/sdk-reference/react-native/get-quote)
* [`client.getTransaction()`](/platform/sdk-reference/react-native/get-transaction)
* [`client.listTransactions()`](/platform/sdk-reference/react-native/list-transactions)
* [`client.setupWidget()`](/platform/sdk-reference/react-native/setup-widget)
* [`client.setupBuy()`](/platform/sdk-reference/react-native/setup-buy)
* [`client.setupChallenge()`](/platform/sdk-reference/react-native/setup-challenge)
* [`client.setupAddCard()`](/platform/sdk-reference/react-native/setup-add-card)
# client.resetConnection()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/reset-connection
Clear the customer's MoonPay connection for this partner.
Use this method to clear the customer's MoonPay connection on this device when they sign out of your app. It runs the [reset frame](/platform/frames/reset) in a hidden WebView and resolves once the reset completes — or after a 5-second timeout, whichever happens first.
`resetConnection()` always resolves with `ok` set to `true`. The method intentionally does not surface errors — a failed reset should never block your sign-out flow.
```tsx Reset the connection focus={5-13} theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function SignOutButton() {
const { client } = useMoonPay();
const handleSignOut = async () => {
// Clear your own local auth state first, then call resetConnection.
await client.resetConnection();
};
// ...
}
```
***
## Parameters
`client.resetConnection()` takes no parameters.
## Result
`client.resetConnection()` returns a `Result`. It always resolves with `ok: true` — even if the underlying frame fails to load or times out — so reset failures never block sign-out.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ----------------------------------------------- | -------- | ------------------------------------------------ |
| `ok` | `boolean` | ✅ | Always `true` for this method. |
| `value` | `undefined` | | Present when `ok` is `true`. Always `undefined`. |
| `error` | [`ResetConnectionError`](#resetconnectionerror) | | Reserved for future use. Not currently emitted. |
### `ResetConnectionError`
Reserved for future use. The current SDK never surfaces a reset error — failures are silently ignored so sign-out can always proceed.
| Field | Type | Required | Description |
| --------- | -------- | -------- | ------------------------------------------------ |
| `message` | `string` | ✅ | A developer-friendly description of the failure. |
# client.setupAddCard()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/setup-add-card
Render the Add Card frame to capture a new card from the customer.
Render the Add Card frame so the customer can save a new credit or debit card.
The provider presents the frame in a full-screen native modal. Card data is
captured inside a PCI-compliant MoonPay-hosted UI and never touches your app
code.
```tsx Setup Add Card focus={5-22} theme={null}
import {
useMoonPay,
type AddCardEvent,
} from "@moonpay/platform-sdk-react-native";
export function AddCardScreen() {
const { client } = useMoonPay();
const start = async () => {
const addCardResult = await client.setupAddCard({
onEvent: (event: AddCardEvent) => {
switch (event.kind) {
case "complete":
// Card added. Use event.payload.card.id to get a quote.
console.log(event.payload.card);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!addCardResult.ok) {
// Handle error
console.error(addCardResult.error.kind, addCardResult.error.message);
return;
}
const addCard = addCardResult.value;
};
// ...
}
```
***
## Parameters
| Property | Type | Required | Description |
| --------- | ------------------------------- | -------- | ------------------------------------------------------------------------------- |
| `onEvent` | `(event: AddCardEvent) => void` | | Callback invoked for Add Card flow events. See [`AddCardEvent`](#addcardevent). |
This method does not require a separate auth token. The client uses stored
credentials from an active connection.
### `AddCardEvent`
`onEvent` receives events as the Add Card flow progresses. Use `event.kind` to
decide how to handle each event.
| kind | Payload | When you receive it |
| ------------ | --------------------------------------------- | -------------------------------------------------------- |
| `"ready"` | — | The frame finished loading and the card form is visible. |
| `"complete"` | `{ card: `[`CardResponse`](#cardresponse)` }` | The card was saved successfully. |
| `"error"` | [`AddCardEventError`](#addcardeventerror) | The flow encountered an error. |
#### `CardResponse`
The card object returned when the customer finishes adding a card. Use `id`
directly in [`getQuote`](/platform/sdk-reference/react-native/get-quote) — there
is no need to re-fetch payment methods.
| Field | Type | Required | Description |
| ----------------- | ------------------ | -------- | ------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The stored card identifier. |
| `type` | `string` | ✅ | The payment method type, typically `"card"`. |
| `cardType` | `string` | ✅ | The card sub-type (for example, `"credit"` or `"debit"`). |
| `brand` | `string` | ✅ | The card network brand (for example, `"visa"`, `"mastercard"`). |
| `last4` | `string` | ✅ | The last four digits of the card number. |
| `expirationMonth` | `string` | ✅ | The card expiration month, as a two-digit string (for example, `"04"`). |
| `expirationYear` | `string` | ✅ | The card expiration year, as a four-digit string (for example, `"2030"`). |
| `availability` | `{ active: true }` | ✅ | The card is available for new transactions when the frame returns it. |
#### `AddCardEventError`
| Field | Type | Required | Description |
| --------- | ------------------------------------- | -------- | --------------------------- |
| `code` | `"configurationError"` \| `"generic"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Result
`client.setupAddCard()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ----------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`AddCardFrame`](#addcardframe) | | Present when `ok` is `true`. |
| `error` | [`SetupAddCardError`](#setupaddcarderror) | | Present when `ok` is `false`. |
### `AddCardFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
The Add Card frame does not expose a `setQuote` method — quotes are issued
after the card is saved.
### `SetupAddCardError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Example: Add a card, then buy
After the `complete` event fires, pass `card.id` to
[`getQuote`](/platform/sdk-reference/react-native/get-quote) and use the returned
signature with [`setupBuy`](/platform/sdk-reference/react-native/setup-buy) to
execute the transaction. For the full end-to-end walkthrough, see the [Pay with
card](/platform/guides/pay-with-card) guide.
```tsx Add card, then buy theme={null}
import {
useMoonPay,
type AddCardEvent,
type BuyEvent,
} from "@moonpay/platform-sdk-react-native";
export function AddCardThenBuy() {
const { client } = useMoonPay();
const start = async () => {
const addCardResult = await client.setupAddCard({
onEvent: async (event: AddCardEvent) => {
if (event.kind !== "complete") return;
const cardId = event.payload.card.id;
// Tear down the Add Card frame — we have the card we need.
addCardResult.value.dispose();
// Request a quote for the new card.
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
paymentMethod: { type: "card", id: cardId },
});
if (!quoteResult.ok) {
// Handle error
return;
}
// Execute the transaction with the headless buy frame.
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
onEvent: (event: BuyEvent) => {
// Handle buy events (see the Pay with card guide).
},
});
if (!buyResult.ok) {
// Handle error
return;
}
},
});
if (!addCardResult.ok) {
// Handle error
return;
}
};
// ...
}
```
```ts types.ts theme={null}
type AddCardFrame = {
dispose: () => void;
};
type CardResponse = {
id: string;
/** Payment method type, typically "card". */
type: string;
/** Card sub-type, for example "credit" or "debit". */
cardType: string;
/** Card network brand, for example "visa" or "mastercard". */
brand: string;
last4: string;
/** Two-digit month string, for example "04". */
expirationMonth: string;
/** Four-digit year string, for example "2030". */
expirationYear: string;
availability: { active: true };
};
type AddCardEvent =
| {
kind: "ready";
}
| {
kind: "complete";
payload: {
card: CardResponse;
};
}
| {
kind: "error";
payload: AddCardEventError;
};
type AddCardEventError = {
code: "configurationError" | "generic";
/** A developer-facing error message. Not intended to be rendered in UI. */
message: string;
};
type SetupAddCardError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupAuth()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/setup-auth
Present the auth frame and drive the customer through email/OTP authentication.
`client.setupAuth()` is the lighter-weight counterpart to
[`client.connect()`](/platform/sdk-reference/react-native/connect) for headless
and Customer API integrations. It drives the customer through email/OTP
authentication only, without the full connect UI, then delivers encrypted
credentials on completion.
The provider presents the auth frame in a full-screen modal. For an inline
alternative, render
[``](/platform/sdk-reference/react-native/components/moonpay-auth)
instead.
**Precondition:** a `clientToken` must be present in the client context before
calling this method. The token is populated automatically when
[`client.getConnection()`](/platform/sdk-reference/react-native/get-connection)
resolves with `status: "connectionRequired"`. If no token is present,
`setupAuth()` resolves immediately with
`err({ kind: "configurationError", ... })` without mounting a frame.
```tsx AuthFlow.tsx theme={null}
import { useMoonPay, type AuthEvent } from "@moonpay/platform-sdk-react-native";
export function AuthFlow() {
const { client } = useMoonPay();
const startAuth = async () => {
// Ensure the client context has a clientToken first.
const connectionResult = await client.getConnection();
if (!connectionResult.ok) {
console.error(connectionResult.error.message);
return;
}
if (connectionResult.value.status !== "connectionRequired") {
// Already connected — no auth step needed.
return;
}
const authResult = await client.setupAuth({
onEvent: (event: AuthEvent) => {
switch (event.kind) {
case "ready":
break;
case "complete":
// Emitted only for active or termsAcceptanceRequired connections.
// Credentials are already applied to the client.
console.log(event.payload.status);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!authResult.ok) {
console.error(authResult.error.kind, authResult.error.message);
return;
}
const authFrame = authResult.value;
// You can dispose the frame at any time:
// authFrame.dispose();
};
// ...
}
```
***
## Parameters
| Property | Type | Required | Description |
| --------- | ---------------------------- | -------- | --------------------------------------------------------------------- |
| `onEvent` | `(event: AuthEvent) => void` | | Callback invoked for auth flow events. See [`AuthEvent`](#authevent). |
### `AuthEvent`
`onEvent` receives events as the auth flow progresses. Use `event.kind` to
decide how to handle each event.
| kind | Payload | When you receive it |
| ------------ | ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The auth UI is rendered and ready to be shown. |
| `"complete"` | `Connection` | Emitted only when the connection is `active` or `termsAcceptanceRequired`. Credentials are already applied. |
| `"error"` | `ConnectionError` | The flow encountered an error. |
**`ConnectionError`** is discriminated by `code`:
| `code` | Extra fields | Description |
| ------------------- | -------------------------------------- | --------------------------------------------------------- |
| `"validationError"` | `errors: ConfigValidationFieldError[]` | One or more session/client/public-key fields are invalid. |
| `"generic"` | `message?: string` | A generic connection failure. |
`ConfigValidationFieldError` entries have `code:
"invalidSessionToken" | "invalidClientToken" | "invalidPublicKey"` and a
developer-facing `message`.
## Result
`client.setupAuth()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ----------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`AuthFrame`](#authframe) | | Present when `ok` is `true`. |
| `error` | [`SetupAuthError`](#setupautherror) | | Present when `ok` is `false`. |
### `AuthFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ------------------------------------------------ |
| `dispose` | `() => void` | ✅ | Unmounts the frame and detaches event listeners. |
### `SetupAuthError`
`SetupAuthError` covers failures to mount the frame, a missing `clientToken`,
customer dismissal, and in-frame errors. Per-flow errors inside the frame
surface with full details through the `"error"` event; the method then resolves
with a generic `SetupAuthError`.
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
`"configurationError"` is returned when no `clientToken` is present in the
client context. `"genericError"` is returned when the customer dismisses the
modal (`message: "Auth flow dismissed"`), when the frame emits an `"error"`
event, or when the frame fails to load.
```ts types.ts theme={null}
type AuthFrame = {
dispose: () => void;
};
type SetupAuthError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupBuy()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/setup-buy
Mount the headless Buy frame and execute a transaction from a quote.
Mount the headless [Buy frame](/platform/frames/buy) in your app and execute a transaction from a quote signature. The frame renders no visible UI — the provider mounts it as a zero-dimension `react-native-webview` that drives the buy pipeline in the background and emits events you handle from your own purchase screen.
The Buy frame is headless. When it emits a `challenge` event, render a
separate challenge frame with
[`client.setupChallenge()`](/platform/sdk-reference/react-native/setup-challenge)
at the URL from the event payload. See the [Handle
challenges](/platform/guides/handling-challenges) guide for the full flow.
```tsx Setup buy focus={5-40} theme={null}
import { useMoonPay, type BuyEvent } from "@moonpay/platform-sdk-react-native";
export function BuyScreen({ quoteSignature }: { quoteSignature: string }) {
const { client } = useMoonPay();
const start = async () => {
const buyResult = await client.setupBuy({
quote: quoteSignature,
externalTransactionId: "order_12345",
onEvent: (event: BuyEvent) => {
switch (event.kind) {
case "ready":
// Pipeline starting — show a loading indicator
break;
case "complete":
// Transaction created. Track final status via polling.
console.log(event.payload.transaction);
break;
case "challenge":
// Verification required — render the challenge frame at the URL.
// See: /platform/guides/handling-challenges
openChallengeFrame(event.payload.url);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!buyResult.ok) {
// Handle error
console.error(buyResult.error.kind, buyResult.error.message);
return;
}
const buy = buyResult.value;
};
// ...
}
```
***
## Parameters
| Property | Type | Required | Description |
| ----------------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/react-native/get-quote). |
| `externalTransactionId` | `string` | | Your own identifier for the transaction. Stored on the MoonPay transaction for correlation. |
| `onEvent` | `(event: BuyEvent) => void` | | Callback invoked for buy flow events. See [`BuyEvent`](#buyevent). |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
### `BuyEvent`
`onEvent` receives events as the buy pipeline progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The buy pipeline has started. Show a loading state in your own UI. |
| `"complete"` | `{ transaction: `[`FrameTransaction`](#frametransaction)` }` | The transaction was created. Use the transaction `id` to poll for final status. |
| `"challenge"` | [`BuyChallengePayload`](#buychallengepayload) | Verification is required before the transaction can proceed. Render the [challenge frame](/platform/guides/handling-challenges) at the provided `url` using `setupChallenge()`. |
| `"error"` | [`BuyEventError`](#buyeventerror) | The flow encountered an error. Surface the message to developers and tear down the frame. |
#### `FrameTransaction`
This is the transaction object returned when the buy pipeline completes. `FrameTransaction` is a discriminated union — the failure variant carries `failureReason`, the non-failure variant always carries `id`. Pass `id` to [`client.getTransaction()`](/platform/sdk-reference/react-native/get-transaction) to poll for the final status.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `status` | `string` | ✅ | The transaction status. On the failure variant, `"failed"`. |
| `id` | `string` | | Required on the non-failure variant; optional when `status` is `"failed"` (a transaction may not exist yet on early failure). |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). A developer-friendly reason. |
#### `BuyChallengePayload`
| Field | Type | Required | Description |
| ------ | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `kind` | `string` | ✅ | The challenge type (currently always `"frame"`, but typed as `string` for forward compatibility). |
| `url` | `string` | ✅ | A fully-formed URL to pass directly to [`setupChallenge()`](/platform/sdk-reference/react-native/setup-challenge). Do not modify it. |
#### `BuyEventError`
| Field | Type | Required | Description |
| --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `code` | `string` | ✅ | The error category. Includes `configurationError`, `invalidQuote`, and backend-specific error codes. |
| `message` | `string` | ✅ | Developer-friendly details. Not intended to be rendered in UI. |
## Result
`client.setupBuy()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`BuyFrame`](#buyframe) | | Present when `ok` is `true`. |
| `error` | [`SetupBuyError`](#setupbuyerror) | | Present when `ok` is `false`. |
### `BuyFrame`
| Field | Type | Required | Description |
| ---------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setQuote` | `(signature: string) => void` | ✅ | Updates the quote signature used by the frame. Use this when the current quote expires before the customer completes the purchase — fetch a new quote and pass its `signature`. |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. Always call `dispose()` once the flow finishes or errors out. |
### `SetupBuyError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Full example
The following example walks through the full card-payment flow: get a quote for a stored card, mount the headless Buy frame, hand off to a challenge frame when verification is required, and dispose of the frame when the transaction completes.
```tsx Buy with a stored card theme={null}
import {
useMoonPay,
type BuyEvent,
type ChallengeEvent,
} from "@moonpay/platform-sdk-react-native";
export function CardCheckout({ cardId }: { cardId: string }) {
const { client } = useMoonPay();
const start = async () => {
// 1. Get a quote for the selected stored card.
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
paymentMethod: { type: "card", id: cardId },
});
if (!quoteResult.ok) throw new Error(quoteResult.error.message);
// 2. Mount the headless Buy frame.
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
externalTransactionId: "order_12345",
onEvent: (event: BuyEvent) => {
switch (event.kind) {
case "ready":
showLoadingIndicator();
break;
case "challenge":
// 3. Hand off to the challenge frame for verification.
handleChallenge(event.payload.url);
break;
case "complete":
// The transaction was created (or failed). Inspect FrameTransaction
// before polling — `id` is only required on the non-failure variant.
buyResult.value.dispose();
if (event.payload.transaction.status !== "failed") {
pollTransaction(event.payload.transaction.id);
}
break;
case "error":
buyResult.value.dispose();
console.error(event.payload.code, event.payload.message);
showError(event.payload.message);
break;
}
},
});
if (!buyResult.ok) {
console.error(buyResult.error.kind, buyResult.error.message);
return;
}
async function handleChallenge(challengeUrl: string) {
await client.setupChallenge({
url: challengeUrl,
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "complete":
// Verification resolved. For buy challenges, the transaction is in payload.
if (event.payload.flow === "buy") {
pollTransaction(event.payload.transaction.id);
}
buyResult.value.dispose();
break;
case "cancelled":
buyResult.value.dispose();
showRetryOption();
break;
case "error":
buyResult.value.dispose();
console.error(event.payload.message);
break;
}
},
});
}
};
// ...
}
```
For the end-to-end card payment walkthrough — listing payment methods, adding a card, and tracking the transaction to a terminal status — see the [Pay with card](/platform/guides/pay-with-card) guide. For details on the challenge flow, see [Handle challenges](/platform/guides/handling-challenges).
## Bank transfer
Bank-transfer payments (SEPA for EUR) use the same headless flow as cards. You fetch a bank-transfer quote, mount the Buy frame with its signature, and read the transaction `id` from the `complete` event. The difference is what happens after the transaction is created: instead of charging a card, the customer sends funds to a MoonPay bank account, so you render the deposit details yourself.
Bank transfers are a floating payment method, so the amounts are estimates until the transfer settles. The quote returns `exchangeRateType: "floating"`; render the estimated crypto amount with a tilde (for example, `~0.2345 BTC`) and tell the customer the final amount is set when their funds settle. See [Exchange rate type](/platform/sdk-reference/react-native/get-quote#exchange-rate-type).
For the full walkthrough, including confirming availability, rendering the
deposit details, handling requotes and cancellations, and tracking the
transaction to a terminal status, see the
[Pay with bank transfer](/platform/guides/pay-with-bank-transfer) guide.
The customer must include the payment `reference` with their bank transfer.
Transfers sent without it are rejected. Surface this prominently in your own
UI, for example, "Always include your payment reference or your transfer will
be rejected." See [Bank-transfer deposit
details](/platform/sdk-reference/react-native/get-transaction#bank-transfer-deposit-details).
```ts types.ts theme={null}
type BuyFrame = {
setQuote: (signature: string) => void;
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type BuyEvent =
| {
kind: "ready";
}
| {
kind: "complete";
payload: {
transaction: FrameTransaction;
};
}
| {
kind: "challenge";
payload: {
/** Currently "frame", but typed as `string` for forward compatibility. */
kind: string;
/** Fully-formed URL to pass directly to setupChallenge(). */
url: string;
};
}
| {
kind: "error";
payload: BuyEventError;
};
type BuyEventError = {
/**
* Includes "configurationError", "invalidQuote", and backend-specific
* error codes returned during the buy pipeline.
*/
code: string;
message: string;
};
type SetupBuyError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupChallenge()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/setup-challenge
Render the Challenge frame to resolve verification required by another flow.
Render the Challenge frame to resolve verification steps required by another
flow (for example, a buy transaction or an identity capture). The provider
presents the frame in a full-screen native modal. The challenge frame is
self-driving — after initialization, it sequences through all required
verification steps and emits `complete` when the pipeline finishes.
Unlike other setup methods, `setupChallenge()` takes a `url` provided by the
upstream flow's `challenge` event, returned in `kyc.challenge` by
`PATCH /platform/v1/customers/{id}/kyc` for Customer API integrations, or
returned as `challenge.url` on a buy quote for [guest checkout limit
upgrades](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up). Pass
it through as-is and do not modify the URL yourself. If the URL doesn't carry a
`channelId` query parameter, the SDK generates one automatically.
For more context, see the [Handle
challenges](/platform/guides/handling-challenges) guide.
```tsx Setup challenge focus={6-50} theme={null}
import {
useMoonPay,
type BuyEvent,
type ChallengeEvent,
} from "@moonpay/platform-sdk-react-native";
export function CheckoutScreen({ quoteSignature }: { quoteSignature: string }) {
const { client } = useMoonPay();
const start = async () => {
const buyResult = await client.setupBuy({
quote: quoteSignature,
onEvent: async (event: BuyEvent) => {
if (event.kind !== "challenge") return;
// The url comes from the challenge event — pass it through as-is.
const challengeResult = await client.setupChallenge({
url: event.payload.url,
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "ready":
// Challenge UI is rendered and visible to the customer
break;
case "complete":
if (event.payload.flow === "buy") {
console.log(event.payload.transaction);
} else if (event.payload.flow === "identity") {
console.log(event.payload.identityId);
}
buyResult.value.dispose();
break;
case "cancelled":
// Customer dismissed the challenge — offer a retry path
buyResult.value.dispose();
break;
case "error":
console.error(event.payload.message);
buyResult.value.dispose();
break;
}
},
});
if (!challengeResult.ok) {
// Handle error
console.error(
challengeResult.error.kind,
challengeResult.error.message,
);
return;
}
},
});
};
// ...
}
```
***
## Parameters
| Property | Type | Required | Description |
| --------- | --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url` | `string` | ✅ | The URL from the upstream flow's `challenge` event payload, or from an identity verification response. Pass it through unchanged. If the URL has no `channelId` query parameter, the SDK adds one. |
| `onEvent` | `(event: ChallengeEvent) => void` | | Callback invoked for Challenge flow events. See [`ChallengeEvent`](#challengeevent). |
This method does not require a separate auth token. The client uses stored
credentials from an active connection.
### `ChallengeEvent`
`onEvent` receives events as the challenge flow progresses. Use `event.kind` to
decide how to handle each event. The `complete` and `cancelled` payloads are
discriminated by `payload.flow` so you can branch on the originating flow.
| kind | Payload | When you receive it |
| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `"ready"` | — | The Challenge UI is rendered and visible to the customer. |
| `"complete"` | [`ChallengeCompleteResult`](#challengecompleteresult) | All verification steps resolved. Discriminated by `flow`. |
| `"cancelled"` | [`ChallengeCancellation`](#challengecancellation) | The customer dismissed the challenge. Discriminated by `flow`. Offer a retry path or exit. |
| `"error"` | [`ChallengeEventError`](#challengeeventerror) | The challenge failed with a terminal error. |
The challenge frame is **self-driving**. After acknowledging the initial
handshake, the SDK does not send further messages to the frame. The frame
internally handles all verification types automatically — including CVC
confirmation, 3D Secure, identity verification (KYC), Strong Customer
Authentication (SCA), micro-deposit authorization, and wallet ownership proof.
You never need to distinguish between them.
#### `ChallengeCompleteResult`
The `complete` payload is discriminated by `flow`:
| Field | Type | Required | Description |
| ------------- | --------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `flow` | `"buy"` \| `"identity"` \| `"guest_checkout_limit_upgrade"` | ✅ | Identifies which upstream flow the challenge resolved. |
| `transaction` | [`Transaction`](#transaction) | | Present when `flow` is `"buy"`. The created or updated transaction. |
| `identityId` | `string` | | Present when `flow` is `"identity"`. The identity record that was verified. |
| `status` | [`GuestCheckoutLimitUpgradeStatus`](#guestcheckoutlimitupgradestatus) | | Present when `flow` is `"guest_checkout_limit_upgrade"`. The terminal outcome of the limit upgrade. |
##### `GuestCheckoutLimitUpgradeStatus`
| Value | What it means |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `"upgraded"` | The customer's guest limit is raised. Request the quote again to get one with `executable: true`. |
| `"rejected"` | Verification failed. The limit is unchanged, and running the upgrade again does not change it. Offer full verification instead. |
| `"pending"` | Verification is still running. The frame polls to a terminal outcome before it emits `complete`, so you rarely see this value. |
#### `ChallengeCancellation`
The `cancelled` payload is discriminated by `flow`:
| Field | Type | Required | Description |
| ---------------- | ----------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `flow` | `"buy"` \| `"identity"` \| `"guest_checkout_limit_upgrade"` | ✅ | Identifies which upstream flow was cancelled. |
| `transactionId` | `string` | | Present when `flow` is `"buy"`. The transaction associated with the cancelled challenge. |
| `challengeToken` | `string` | | Present when `flow` is `"buy"`. The challenge token from the original `challenge` event. |
Cancelling a `guest_checkout_limit_upgrade` challenge submits nothing and
leaves the limit unchanged. Offer a retry path.
#### `Transaction`
This is the transaction object returned when the buy challenge completes. It uses the same [`FrameTransaction`](/platform/sdk-reference/react-native/setup-buy#frametransaction) shape as `setupBuy()`.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | -------------------------------------------------------------------------- |
| `status` | `string` | ✅ | The transaction status. On the failure variant, `"failed"`. |
| `id` | `string` | | Required on the non-failure variant; optional when `status` is `"failed"`. |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). |
#### `ChallengeEventError`
| Field | Type | Required | Description |
| --------- | -------- | -------- | ----------------------------------------------------------------------------------------------- |
| `code` | `string` | ✅ | A machine-readable error category propagated from the challenge frame. Surface to logs, not UI. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Result
`client.setupChallenge()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`ChallengeFrame`](#challengeframe) | | Present when `ok` is `true`. |
| `error` | [`SetupChallengeError`](#setupchallengeerror) | | Present when `ok` is `false`. |
### `ChallengeFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
Unlike `setupBuy()`, `setupApplePay()`, and `setupGooglePay()`, the
`ChallengeFrame` does not expose a `setQuote()` method. The challenge frame
runs to completion on its own.
### `SetupChallengeError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
```ts types.ts theme={null}
type ChallengeFrame = {
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type GuestCheckoutLimitUpgradeStatus = "pending" | "upgraded" | "rejected";
type ChallengeCompleteResult =
| { flow: "buy"; transaction: FrameTransaction }
| { flow: "identity"; identityId: string }
| {
flow: "guest_checkout_limit_upgrade";
status: GuestCheckoutLimitUpgradeStatus;
};
type ChallengeCancellation =
| { flow: "buy"; transactionId?: string; challengeToken?: string }
| { flow: "identity" }
| { flow: "guest_checkout_limit_upgrade" };
type ChallengeEvent =
| { kind: "ready" }
| { kind: "complete"; payload: ChallengeCompleteResult }
| { kind: "cancelled"; payload: ChallengeCancellation }
| { kind: "error"; payload: ChallengeEventError };
type ChallengeEventError = {
code: string;
message: string;
};
type SetupChallengeError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupWidget()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/setup-widget
Render the MoonPay buy widget and start a transaction.
Render the MoonPay buy widget and initiate a transaction with a quote
signature. The quote must have `executable: true` — a non-executable quote will
not render. The provider presents the widget in a full-screen native modal and
handles the full purchase flow — including payment collection, verification,
and transaction confirmation.
```tsx Setup widget focus={5-30} theme={null}
import {
useMoonPay,
type WidgetEvent,
} from "@moonpay/platform-sdk-react-native";
export function WidgetScreen({ quoteSignature }: { quoteSignature: string }) {
const { client } = useMoonPay();
const start = async () => {
const widgetResult = await client.setupWidget({
quote: quoteSignature,
onEvent: (event: WidgetEvent) => {
switch (event.kind) {
case "ready":
break;
case "transactionCreated":
console.log(event.payload.transaction);
break;
case "complete":
console.log(event.payload.transaction);
break;
case "close":
// The customer closed the widget
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!widgetResult.ok) {
// Handle error
console.error(widgetResult.error.kind, widgetResult.error.message);
return;
}
const widget = widgetResult.value;
};
// ...
}
```
***
## Parameters
| Property | Type | Required | Description |
| ----------------------- | ------------------------------ | -------- | ------------------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/react-native/get-quote). |
| `externalTransactionId` | `string` | | Partner-assigned identifier for this transaction attempt. Useful for reconciliation. |
| `onEvent` | `(event: WidgetEvent) => void` | | Callback invoked for widget flow events. See [`WidgetEvent`](#widgetevent). |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
### `WidgetEvent`
`onEvent` receives events as the widget flow progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The widget is loaded and ready to be shown. |
| `"transactionCreated"` | `{ transaction: { id: string; status: string } }` | A transaction has been created. The customer may still need to complete additional steps (for example, 3-D Secure). |
| `"complete"` | `{ transaction:` [`FrameTransaction`](#frametransaction) `}` | The widget flow finished. `FrameTransaction` is a discriminated union — handle the `"failed"` variant separately. |
| `"close"` | — | The customer closed the widget. |
| `"error"` | [`WidgetEventError`](#widgeteventerror) | The flow encountered an error. |
#### `FrameTransaction`
The transaction reported by the widget on `"complete"`. The shape depends on the outcome.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------- |
| `id` | `string` | | Present on the success variant. Required when `status !== "failed"`; optional when `status` is `"failed"`. |
| `status` | `string` | ✅ | The transaction status. On the failure variant this is `"failed"`. |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). |
To track the final status after `"complete"`, pass `transaction.id` to [`client.getTransaction()`](/platform/sdk-reference/react-native/get-transaction).
#### `WidgetEventError`
| Field | Type | Required | Description |
| --------- | ----------------------------------------------------- | -------- | --------------------------- |
| `code` | `"configurationError"` \| `"apiError"` \| `"generic"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Result
`client.setupWidget()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`WidgetFrame`](#widgetframe) | | Present when `ok` is `true`. |
| `error` | [`SetupWidgetError`](#setupwidgeterror) | | Present when `ok` is `false`. |
### `WidgetFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
### `SetupWidgetError`
| Field | Type | Required | Description |
| --------- | ---------------------------------------------------------- | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"apiError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
```ts types.ts theme={null}
type WidgetFrame = {
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type WidgetEvent =
| { kind: "ready" }
| {
kind: "transactionCreated";
payload: { transaction: { id: string; status: string } };
}
| {
kind: "complete";
payload: { transaction: FrameTransaction };
}
| { kind: "close" }
| { kind: "error"; payload: WidgetEventError };
type WidgetEventError = {
code: "configurationError" | "apiError" | "generic";
message: string;
};
type SetupWidgetError = {
kind: "configurationError" | "apiError" | "genericError";
message: string;
};
```
# useMoonPay()
Source: https://dev.moonpay.com/platform/sdk-reference/react-native/use-moonpay
Read the SDK client from any component below the provider.
`useMoonPay()` returns the SDK client made available by [``](/platform/sdk-reference/react-native/provider). Call it from any function component below the provider to access integration methods.
```tsx ConnectScreen.tsx theme={null}
import { useMoonPay } from "@moonpay/platform-sdk-react-native";
export function ConnectScreen() {
const { client } = useMoonPay();
const handleConnect = async () => {
const result = await client.getConnection();
if (!result.ok) {
console.error(result.error.message);
return;
}
console.log(result.value);
};
// ...
}
```
***
## Return value
`useMoonPay()` returns the `client` plus helpers for supplying the session token after mount. The client exposes every integration method — see the [`MoonPayProvider`](/platform/sdk-reference/react-native/provider#client-methods) reference for the full list.
| Field | Type | Required | Description |
| --------------- | -------------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| `client` | `object` | ✅ | The SDK client used to call MoonPay Platform methods. |
| `initialize` | `(sessionToken: string) => void` | ✅ | Set or replace the session token after the provider mounts. See [`initialize()`](#initialize). |
| `isInitialized` | `boolean` | ✅ | `true` once a session token is available, via the prop or `initialize()`. |
## `initialize()`
Set the session token after the provider has mounted. Use this when you don't have the token at mount time: mount [``](/platform/sdk-reference/react-native/provider) without the `sessionToken` prop, then call `initialize()` once your server returns the token.
```tsx theme={null}
const { initialize } = useMoonPay();
initialize(sessionToken);
```
`initialize()` updates the provider's state, so the `client` that carries the new token is available on the **next render** — not synchronously within the same call. Gate your flow on [`isInitialized`](#isinitialized) (or read `client` again after the re-render) instead of reusing a `client` reference captured before `initialize()`.
Call `initialize()` again to replace the token — for example, after a refresh or when switching accounts. Replacing the token clears any credentials cached from the previous session, so call [`client.getConnection()`](/platform/sdk-reference/react-native/get-connection) again to re-establish the customer's connection.
## `isInitialized`
`true` once a session token is available — set either through the [`sessionToken` prop](/platform/sdk-reference/react-native/provider#props) or a call to [`initialize()`](#initialize). Use it to defer connection calls until the SDK has a token; calling a connection method beforehand returns an `err()` result.
## Errors
`useMoonPay()` throws if called outside a ``:
```
useMoonPay must be used within a
```
Mount the provider higher in the tree, or call the hook from a child of the provider.
# client.connect()
Source: https://dev.moonpay.com/platform/sdk-reference/web/connect
Render the connect flow in your UI.
Use `client.connect()` when you need to connect a customer's MoonPay account in your app. This method mounts the co-branded connect flow into your UI and streams lifecycle events through `onEvent`.
If you want the full end-to-end flow (create session → check connection → connect), start with the [Connect a customer](/platform/guides/connect-a-customer) guide.
For UI recommendations, see [Presentation and appearance](/platform/guides/presentation-and-appearance).
```ts Connect a customer focus={6-31} theme={null}
import { createClient, type ConnectEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const connectResult = await client.connect({
container: document.querySelector("#connectContainer"),
onEvent: (event: ConnectEvent) => {
switch (event.kind) {
case "ready":
// The UI is ready. Reveal the container if you hide it while loading.
break;
case "complete":
// event.payload is the Connection — same shape as client.getConnection() returns.
if (event.payload.status === "active") {
console.log(event.payload.customer.id);
}
break;
case "error":
// event.payload has a `code` discriminator. On validationError it carries
// a list of field-level errors; on generic it may carry a developer message.
console.error(event.payload);
break;
}
},
});
if (!connectResult.ok) {
// Handle error
console.error(connectResult.error.message);
return;
}
const connectFrame = connectResult.value;
// Remove the frame from the DOM now that the flow has completed:
// connectFrame.dispose();
```
The promise returned by `client.connect()` resolves after the customer
completes the connect flow (or an error ends it). Track flow progress —
including the moment the UI is ready to show — through `onEvent`, not by
awaiting the promise.
***
## Parameters
| Field | Type | Required | Description |
| ------------------ | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `container` | `HTMLElement` | ✅ | A [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) DOM element to render the connect frame into. |
| `theme` | `object` | | Optional appearance settings for the connect flow. |
| `theme.appearance` | `"dark"` \| `"light"` | | Force a specific appearance. If omitted, the frame uses the user's system appearance. |
| `onEvent` | `(event: ConnectEvent) => void` | | Callback invoked for connect flow events. See [`ConnectEvent`](#connectevent). |
### `ConnectEvent`
`onEvent` receives events as the connect flow progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `"ready"` | — | The connect UI is rendered and ready to be shown. |
| `"complete"` | [`Connection`](/platform/sdk-reference/web/get-connection#connection) | The customer completed the connect flow. Same shape as `client.getConnection()`. |
| `"error"` | [`ConnectEventError`](#connecteventerror) | The flow encountered an error. |
To remove the frame from the DOM after the flow completes, call `connectFrame.dispose()` on the [`ConnectFrame`](#connectframe) returned from `client.connect()`. The frame handle only becomes available once the flow completes — the returned promise stays pending while the customer works through the flow.
#### `ConnectEventError`
The error event payload comes from the underlying connect frame. It is discriminated by `code`.
| Field | Type | Required | Description |
| --------- | ---------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `"validationError"` \| `"generic"` | ✅ | The error category. |
| `errors` | `ConfigValidationFieldError[]` | | Present when `code` is `"validationError"`. Field-level errors describing which inputs (such as the session token) failed validation. |
| `message` | `string` | | Present on `"generic"` errors. Developer-friendly details. |
`ConfigValidationFieldError` entries have a `code` of `"invalidSessionToken"`, `"invalidClientToken"`, or `"invalidPublicKey"`, plus a developer-facing `message`.
```ts types.ts theme={null}
type ConfigValidationFieldError = {
code: "invalidSessionToken" | "invalidClientToken" | "invalidPublicKey";
message: string;
};
type ConnectEventError =
| { code: "validationError"; errors: ConfigValidationFieldError[] }
| { code: "generic"; message?: string };
```
## Result
`client.connect()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`ConnectFrame`](#connectframe) | | Present when `ok` is `true`. |
| `error` | [`ConnectError`](#connecterror) | | Present when `ok` is `false`. |
### `ConnectFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ------------------------------------------------------------ |
| `dispose` | `() => void` | ✅ | Removes the frame from the DOM and detaches event listeners. |
### `ConnectError`
`ConnectError` covers failures to mount the frame or complete the handshake. Per-flow failures inside the frame surface through the `"error"` event payload, [`ConnectEventError`](#connecteventerror).
| Field | Type | Required | Description |
| --------- | -------- | -------- | ----------------------------------------------------------------------------------- |
| `message` | `string` | ✅ | A developer-friendly description of the failure (for example, a handshake timeout). |
```ts types.ts theme={null}
type ConnectFrame = {
dispose: () => void;
};
type ConnectError = {
message: string;
};
```
## Resources
For full protocol details, see the [connect frame reference](/platform/frames/connect).
# createClient()
Source: https://dev.moonpay.com/platform/sdk-reference/web/create-client
Initialize a reusable SDK client with a `sessionToken`.
Initialize a client with a [sessionToken](/platform/guides/api-and-sdk-credentials#session-token) provided by your backend. The client is optional, but it gives you a single place to manage the [integration credentials](/platform/guides/api-and-sdk-credentials#client-credentials) used across the SDK.
```ts Initialize client theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
```
`createClient()` returns the client synchronously. Method calls on the client (for example, `client.getConnection()`) return a `Result` you check before reading `value`.
***
## Parameters
| Property | Type | Required | Description |
| -------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionToken` | `string` | ✅ | The [sessionToken](/platform/guides/api-and-sdk-credentials#session-token) created on your server. |
| `apiBaseUrl` | `string` | | Override the MoonPay Platform API base URL. Defaults to production. Use this only when MoonPay support has asked you to point at a non-production environment. |
| `frameBaseUrl` | `string` | | Override the MoonPay frame base URL. Defaults to production. Use this only when MoonPay support has asked you to point at a non-production environment. |
## Return value
`createClient()` returns a `Client` instance. It does not throw and does not return a `Result`.
### `Client`
The client instance exposes every integration method:
* [`client.getConnection()`](/platform/sdk-reference/web/get-connection)
* [`client.connect()`](/platform/sdk-reference/web/connect)
* [`client.setupAuth()`](/platform/sdk-reference/web/setup-auth)
* [`client.resetConnection()`](/platform/sdk-reference/web/reset-connection)
* [`client.getPaymentMethods()`](/platform/sdk-reference/web/get-payment-methods)
* [`client.deletePaymentMethod()`](/platform/sdk-reference/web/delete-payment-method)
* [`client.getQuote()`](/platform/sdk-reference/web/get-quote)
* [`client.getTransaction()`](/platform/sdk-reference/web/get-transaction)
* [`client.listTransactions()`](/platform/sdk-reference/web/list-transactions)
* [`client.setupWidget()`](/platform/sdk-reference/web/setup-widget)
* [`client.setupApplePay()`](/platform/sdk-reference/web/setup-apple-pay)
* [`client.setupGooglePay()`](/platform/sdk-reference/web/setup-google-pay)
* [`client.setupBuy()`](/platform/sdk-reference/web/setup-buy)
* [`client.setupBuyButton()`](/platform/sdk-reference/web/setup-buy-button)
* [`client.setupChallenge()`](/platform/sdk-reference/web/setup-challenge)
* [`client.setupAddCard()`](/platform/sdk-reference/web/setup-add-card)
* [Identity methods](/platform/sdk-reference/web/identity) — `createIdentity()`, `getIdentity()`, `updateIdentity()`, `verifyIdentity()`, `getIdentityUploadUrl()`, `submitIdentityFiles()`
# client.deletePaymentMethod()
Source: https://dev.moonpay.com/platform/sdk-reference/web/delete-payment-method
Delete a stored payment method for the connected customer.
Use this method to remove a stored payment method — for example, when a customer asks to forget a card, or after a one-off purchase where you don't want to retain card details. For request and response details, see the [Delete payment method API](/api-reference/platform/endpoints/payment-methods/delete).
To obtain a `paymentMethodId`, list the customer's stored payment methods with [`client.getPaymentMethods()`](/platform/sdk-reference/web/get-payment-methods).
```ts Delete a payment method focus={5-15} theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
// Call this after the client has an active connection.
const paymentMethodId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const result = await client.deletePaymentMethod(paymentMethodId);
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
// On success, `result.value` is `undefined`.
```
***
## Parameters
| Parameter | Type | Required | Description |
| ----------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodId` | `string` | ✅ | The `id` of the stored card to delete. Obtain this from [`client.getPaymentMethods()`](/platform/sdk-reference/web/get-payment-methods#storedcardpaymentmethod). |
## Result
`client.deletePaymentMethod()` returns a `Result`.
The API responds with `204 No Content` on success, so `result.value` is `undefined` — there is no response body to read. Check `result.ok` to confirm the delete succeeded.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------------- | -------- | ------------------------------------------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `undefined` | | Present when `ok` is `true`. Always `undefined` (no response body). |
| `error` | [`DevPlatformApiError`](#devplatformapierror) | | Present when `ok` is `false`. |
### `DevPlatformApiError`
The standard MoonPay Platform API error shape.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"not_found"`, `"unauthorized"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
```ts types.ts theme={null}
type DevPlatformApiError = {
code: DevPlatformApiErrorCode;
message: string;
errors?: DevPlatformApiErrorDetail[];
};
type DevPlatformApiErrorDetail = {
field?: string;
message: string;
};
```
# client.getConnection()
Source: https://dev.moonpay.com/platform/sdk-reference/web/get-connection
Check whether the customer already has an active connection.
Use this method to check whether the current customer already has an active connection. If the customer is not connected (or the connection expired), run the connect flow with [`client.connect()`](/platform/sdk-reference/web/connect).
The SDK runs the check in a hidden frame, decrypts the returned credentials, and primes the client so that subsequent SDK calls (such as `getPaymentMethods()` or `getQuote()`) are authenticated automatically.
```ts Get a connection focus={5-30} theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const result = await client.getConnection();
if (!result.ok) {
// Handle error
console.error(result.error.message);
return;
}
switch (result.value.status) {
case "active":
console.log(result.value.customer.id);
break;
case "connectionRequired":
// Render the connect flow with client.connect().
break;
case "termsAcceptanceRequired":
// Show your own Terms of Use UI, then create a new session with
// termsAcceptedAt and relaunch the flow.
break;
case "pending":
// KYC decision delayed. Retry later.
break;
case "failed":
console.error(result.value.reason);
break;
case "unavailable":
// Restricted location. Surface a fallback experience.
break;
}
```
***
## Parameters
`client.getConnection()` takes an optional `options` object.
| Field | Type | Required | Description |
| --------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `skipKyc` | `boolean` | | Pass `true` for headless/Customer API integrations so the check skips KYC-based statuses: the result won't resolve to `"pending"` or `"failed"` due to KYC alone. `"termsAcceptanceRequired"` is still surfaced (legal requirements can't be skipped). Defaults to `false`. |
## Result
`client.getConnection()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`Connection`](#connection) | | Present when `ok` is `true`. |
| `error` | [`GetConnectionError`](#getconnectionerror) | | Present when `ok` is `false`. |
### `Connection`
`Connection` is a discriminated union over `status`. The fields present depend on the status:
| Variant | Fields | Description |
| --------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"active"` | `status`, `customer`, `credentials`, `capabilities` | The customer is connected. The SDK has already decrypted credentials internally — you do not need to send the `credentials` string anywhere. |
| `"connectionRequired"` | `status`, `credentials`, `capabilities?`, `mismatch?` | The customer needs to complete the connect flow. The SDK has decrypted the anonymous tokens internally; `capabilities` is present when guest checkout is enabled for the partner. `mismatch` is present and `true` when the session's email and phone number resolve to different MoonPay customers. |
| `"termsAcceptanceRequired"` | `status` | The customer has no valid Terms of Use attestation on file (headless/Customer API partners). Display the Terms of Use yourself, pass the acceptance timestamp as `termsAcceptedAt` when you create a new session (`POST /platform/v1/sessions`), and relaunch the flow. Requires the Identity or Guest Checkout account capability. See [Terms acceptance](/platform/guides/terms-acceptance). |
| `"pending"` | `status` | The KYC decision is delayed and may resolve on a subsequent visit. |
| `"failed"` | `status`, `reason` | Terminal failure (for example, KYC rejection). |
| `"unavailable"` | `status` | The customer is in a restricted location. |
#### Fields
| Field | Type | Required | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status` | `"active"` \| `"connectionRequired"` \| `"termsAcceptanceRequired"` \| `"pending"` \| `"failed"` \| `"unavailable"` | ✅ | The connection status. |
| `customer` | `object` | | Present when `status` is `"active"`. Contains the connected MoonPay customer. |
| `customer.id` | `string` | | The MoonPay customer identifier. |
| `customer.country` | `string` | | The customer's ISO 3166-1 alpha-3 residential country code (for example, `"USA"` or `"FRA"`). Use this to determine which payment disclosures apply. |
| `customer.administrativeArea` | `string` | | The customer's state or province code, included when disclosures apply at the subdivision level (for example, `"NY"` or `"WA"`). |
| `customer.area` | `string` | | The broader regulatory area for the customer's country, when applicable. Currently `"EEA"`. |
| `credentials` | `string` | | Present when `status` is `"active"` or `"connectionRequired"`. A base64-encoded, X25519+AES-GCM-encrypted token blob. The SDK decrypts this internally — you don't need to handle it directly. |
| `capabilities` | [`CustomerCapabilities`](#customercapabilities) | | Required on `"active"`; optional on `"connectionRequired"` (present when guest checkout is enabled for the partner). |
| `mismatch` | `boolean` | | Present only when `status` is `"connectionRequired"` and the session's email and phone number resolve to different MoonPay customers (a conflict). When `true`, route the customer through the connect flow before rendering payment UI such as Apple Pay or Google Pay. Absent when there is no conflict — never `false`. |
| `reason` | `string` | | Present only when `status` is `"failed"`. Developer-friendly failure details. |
#### `CustomerCapabilities`
Capabilities and regulatory requirements for the customer.
| Field | Type | Required | Description |
| --------------- | ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `ramps` | [`RampsCapability`](#rampscapability) | | Capabilities for buy and sell flows. |
| `guestCheckout` | [`RampsCapability`](#rampscapability) | | Capabilities for the guest checkout flow. Present when guest checkout is enabled for the partner and the session is a guest-checkout session. |
#### `RampsCapability`
| Field | Type | Required | Description |
| -------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `requirements` | `RampsRequirements` | ✅ | Regulatory requirements that apply to ramps for the customer. `paymentDisclosures` is deprecated — read geography from `customer.country`, `customer.administrativeArea`, and `customer.area` instead. |
```ts types.ts theme={null}
type Customer = {
id: string;
country?: string;
administrativeArea?: string;
area?: string;
};
type Connection =
| {
status: "active";
customer: Customer;
credentials: string;
capabilities: CustomerCapabilities;
}
| {
status: "connectionRequired";
credentials: string;
capabilities?: CustomerCapabilities;
mismatch?: boolean;
}
| { status: "termsAcceptanceRequired" }
| { status: "pending" }
| { status: "failed"; reason: string }
| { status: "unavailable" };
```
### `GetConnectionError`
`GetConnectionError` covers failures to run the connection check (for example, a frame handshake timeout).
| Field | Type | Required | Description |
| --------- | -------- | -------- | ------------------------------------------------ |
| `message` | `string` | ✅ | A developer-friendly description of the failure. |
# client.getPaymentMethods()
Source: https://dev.moonpay.com/platform/sdk-reference/web/get-payment-methods
List payment methods available for the connected customer.
Use this method to list the customer's available payment methods, plus any cards they have on file. For request and response details, see the [List payment methods API](/api-reference/platform/endpoints/payment-methods/list).
```ts Get payment methods focus={5-16} theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
// Call this after the client has an active connection (for example, after
// `client.getConnection()` returns `status: "active"` or after `client.connect()`
// completes).
const result = await client.getPaymentMethods();
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
console.log(result.value.data.paymentMethodConfigs); // Available payment-method configs
console.log(result.value.data.paymentMethods); // Stored payment methods, such as saved cards (if any)
```
***
## Result
`client.getPaymentMethods()` returns a `Result<{ data: ListPaymentMethodsResponse }, GetPaymentMethodsError>`.
### Result envelope
`Result<{ data: ListPaymentMethodsResponse }, GetPaymentMethodsError>`
| Field | Type | Required | Description |
| ------- | ------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`ListPaymentMethodsResponse`](#listpaymentmethodsresponse) `}` | | Present when `ok` is `true`. |
| `error` | [`GetPaymentMethodsError`](#getpaymentmethodserror) | | Present when `ok` is `false`. |
### `ListPaymentMethodsResponse`
| Field | Type | Required | Description |
| ---------------------- | ------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `paymentMethodConfigs` | [`PaymentMethodConfig`](#paymentmethodconfig)`[]` | | Payment methods (Apple Pay, Google Pay, card, etc.) available to the customer with their capabilities and availability. |
| `paymentMethods` | [`StoredPaymentMethod`](#storedpaymentmethod)`[]` | | Payment methods the customer has stored on file, such as saved cards. |
Bank transfers appear here alongside cards and wallets: `sepa` for SEPA (EUR). Offer `sepa` in the headless flow only when `availability.active` is `true` and `capabilities.requiresWidget` is `false` (a `requiresWidget: true` method must complete in the MoonPay widget instead). SEPA doesn't create a stored payment method, so it never appears in `paymentMethods` (which lists stored cards). To pay with a bank transfer, quote against its `type` and open the headless Buy frame. See [Bank transfer](/platform/sdk-reference/web/setup-buy#bank-transfer).
#### `PaymentMethodConfig`
| Field | Type | Required | Description |
| -------------- | --------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `type` | `PaymentMethodType` | ✅ | The payment method type. See the [Get a quote API reference](/api-reference/platform/endpoints/quotes/get) for supported values. |
| `capabilities` | [`PaymentMethodCapabilities`](#paymentmethodcapabilities) | ✅ | Details about how this payment method can be used. |
| `availability` | [`PaymentMethodAvailability`](#paymentmethodavailability) | ✅ | Whether this payment method is available for the current session. |
#### `PaymentMethodCapabilities`
| Field | Type | Required | Description |
| --------------------------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `supportedCurrencies` | `string[]` | ✅ | A list of [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) fiat currency codes that this payment method can be used with. |
| `supportedTransactionTypes` | `string[]` | ✅ | The kinds of transactions this payment method can be used for. |
| `allowsDeletion` | `boolean` | ✅ | Whether this payment method can be deleted via `client.deletePaymentMethod()`. |
| `requiresWidget` | `boolean` | ✅ | Whether this payment method requires the MoonPay widget to complete the payment flow. |
#### `PaymentMethodAvailability`
| Field | Type | Required | Description |
| --------- | ---------- | -------- | ------------------------------------------------------------------------- |
| `active` | `boolean` | ✅ | Whether this payment method is available for the current session. |
| `reasons` | `string[]` | | If the payment method is unavailable, a list of machine-readable reasons. |
#### `StoredPaymentMethod`
A payment method the customer has stored on file. Use the `id` directly in `client.getQuote()` to quote against a specific stored payment method. Stored cards extend the base shape with card details — see the [API reference](/api-reference/platform/endpoints/payment-methods/list) for the full shape, including network brand, expiry, and `last4`.
| Field | Type | Required | Description |
| ------ | -------- | -------- | ------------------------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The stored payment method identifier. Pass this to `getQuote({ paymentMethod: { type: "card", id } })`. |
| `type` | `string` | ✅ | The payment method type, typically `"card"`. |
### `GetPaymentMethodsError`
`GetPaymentMethodsError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"unauthorized"`, `"not_found"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
```ts types.ts theme={null}
type ListPaymentMethodsResponse = {
paymentMethodConfigs?: PaymentMethodConfig[];
paymentMethods?: StoredPaymentMethod[];
};
type StoredPaymentMethod = {
id: string;
type: PaymentMethodType;
};
type PaymentMethodConfig = {
type: PaymentMethodType;
capabilities: PaymentMethodCapabilities;
availability: PaymentMethodAvailability;
};
type PaymentMethodCapabilities = {
supportedCurrencies: string[];
supportedTransactionTypes: string[];
allowsDeletion: boolean;
requiresWidget: boolean;
};
type PaymentMethodAvailability = {
active: boolean;
reasons?: string[];
};
type GetPaymentMethodsError = {
code: DevPlatformApiErrorCode;
message: string;
errors?: DevPlatformApiErrorDetail[];
};
```
# client.getQuote()
Source: https://dev.moonpay.com/platform/sdk-reference/web/get-quote
Request a quote for a transaction.
Use this method to request a quote for fiat-to-crypto transactions. Quotes include fees and limits, and they expire after a short time window. A quote with `executable: true` can be used to execute a transaction.
```ts Get a quote theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890123456789012345678901234567890" },
paymentMethod: { type: "apple_pay" },
});
if (!quoteResult.ok) {
// Handle error
console.error(quoteResult.error.code, quoteResult.error.message);
return;
}
console.log(quoteResult.value.data);
```
***
## Parameters
`client.getQuote()` takes a single `input` object. The shape matches the [Get quotes API](/api-reference/platform/endpoints/quotes/get) request body — see that page for the complete field reference.
| Field | Type | Required | Description |
| ------------------------ | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source` | `object` | ✅ | The fiat side of the quote. |
| `source.asset.code` | `string` | ✅ | The fiat currency code as [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) (for example, `"USD"`). |
| `source.amount` | `string` | | The amount to spend, as a string (for example, `"100.00"`). Set the amount on either `source` or `destination`, not both. |
| `destination` | `object` | ✅ | The crypto side of the quote. |
| `destination.asset.code` | `string` | ✅ | The crypto asset code the customer receives (for example, `"ETH"`, `"USDC_SOL"`). |
| `destination.amount` | `string` | | The amount the customer receives, if known. Set the amount on either `source` or `destination`, not both. |
| `wallet` | `object` | | Where the crypto is sent. Required for the quote to be `executable`. |
| `wallet.address` | `string` | ✅ | The destination wallet address. |
| `wallet.tag` | `string` | | An optional memo or destination tag, used by some blockchains such as XRP or XLM. |
| `paymentMethod` | `object` | | The payment method to quote against. Required for the quote to be `executable`. |
| `paymentMethod.type` | `string` | ✅ | The payment method type. See the [Get a quote API reference](/api-reference/platform/endpoints/quotes/get) for supported values. |
| `paymentMethod.id` | `string` | | The stored payment method ID. Required when `type` is `"card"` to identify the specific card. |
| `feeBehavior` | `string` | | How fees relate to `source.amount`. One of `"inclusive"` or `"exclusive"`. Defaults to `"inclusive"`. Only applies when you quote by `source.amount`; it is ignored when you quote by `destination.amount`. |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
### Payment method types
`paymentMethod.type` accepts the payment methods available to the connected customer, including bank transfers. See the [Get a quote API reference](/api-reference/platform/endpoints/quotes/get) for the full list.
| `type` | Payment method |
| ------------ | ------------------ |
| `apple_pay` | Apple Pay |
| `google_pay` | Google Pay |
| `card` | Stored card |
| `sepa` | SEPA bank transfer |
The fiat currencies each method supports come from [`getPaymentMethods`](/platform/sdk-reference/web/get-payment-methods) (`capabilities.supportedCurrencies`); `sepa` is EUR only.
For bank transfers, quote with `paymentMethod.type` set to `"sepa"` (EUR), then execute the quote with the same headless Buy frame you use for cards. See [Bank transfer](/platform/sdk-reference/web/setup-buy#bank-transfer).
Bank transfers are a floating payment method: the quote is an estimate, and the exact amount is set when the customer's funds settle. Bank-transfer quotes return `exchangeRateType: "floating"`; card and wallet methods return `exchangeRateType: "fixed"`. When `exchangeRateType` is `"floating"`, render the estimated crypto amount with a tilde (for example, `~0.2345 BTC`) and tell the customer the final amount is set when the transfer settles. See [Exchange rate type](#exchange-rate-type).
```ts Quote for a SEPA bank transfer theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "EUR" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890123456789012345678901234567890" },
paymentMethod: { type: "sepa" },
});
```
Bank transfers don't support DeFi assets. A DeFi asset is a
`destination.asset` identified by `caip19`. If you request a bank-transfer
quote (`sepa`) for one, the request fails with `400` and the message "Bank
transfers are not supported for DeFi assets." Offer a card or wallet payment
method for these assets instead.
### Fee behavior
`feeBehavior` controls whether fees are taken out of `source.amount` or added on top of it. It only applies when you quote by `source.amount`.
* `"inclusive"` (default): the customer pays exactly `source.amount`, and fees are carved out of it. Less of the source amount is converted, so the customer receives less crypto. This is the existing behavior, so omitting `feeBehavior` keeps quotes unchanged.
* `"exclusive"`: fees are added on top of `source.amount`, so the customer pays `source.amount` plus fees. The full `source.amount` is converted, so the customer receives more crypto than the inclusive quote for the same input.
When you quote by `destination.amount`, MoonPay ignores `feeBehavior` and the quote is always fees-inclusive. The response always echoes the effective `feeBehavior`: the value you requested for source-amount quotes, or `"inclusive"` for destination-amount quotes.
```ts Quote with fees added on top theme={null}
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890123456789012345678901234567890" },
paymentMethod: { type: "apple_pay" },
feeBehavior: "exclusive",
});
```
## Result
`client.getQuote()` returns a `Result<{ data: Quote }, GetQuoteError>`.
### Result envelope
`Result<{ data: Quote }, GetQuoteError>`
| Field | Type | Required | Description |
| ------- | --------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`Quote`](#quote) `}` | | Present when `ok` is `true`. |
| `error` | [`GetQuoteError`](#getquoteerror) | | Present when `ok` is `false`. |
### `Quote`
A quote includes a `signature` you use to execute a transaction, plus fees and limits. See the [API reference](/api-reference/platform/endpoints/quotes/get) for the full shape.
| Field | Type | Required | Description |
| ------------------ | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `signature` | `string` | ✅ | A stringified JSON object that contains quote data and an embedded hash. Don't deserialize this value. Use it as-is to execute a transaction. |
| `expiresAt` | `string` | ✅ | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp for when the quote expires. |
| `source` | `object` | ✅ | The source (fiat) asset details. |
| `destination` | `object` | ✅ | The destination (crypto) asset details. |
| `wallet` | `object` | ✅ | The destination wallet details. |
| `fees` | `object` | ✅ | Fee details for the transaction. |
| `feeBehavior` | `string` | ✅ | The effective fee behavior for this quote, `"inclusive"` or `"exclusive"`. Echoes the requested value for source-amount quotes, or `"inclusive"` for destination-amount quotes. |
| `executable` | `boolean` | ✅ | Whether the quote can be used to execute a transaction. See the [API reference](/api-reference/platform/endpoints/quotes/get) for the request fields required to receive `executable: true`. |
| `exchangeRate` | `string` | ✅ | The rate used to convert between fiat and crypto, as the fiat value of one unit of crypto. Pairs with `exchangeRateType`. |
| `exchangeRateType` | `string` | ✅ | `"fixed"` when the exchange rate is locked at quote time (card/wallet), or `"floating"` when it's an estimate confirmed at settlement (bank transfers such as SEPA). Pairs with the `exchangeRate` field. See [Exchange rate type](#exchange-rate-type). |
| `challenge` | `object` | | Present when the customer must clear a step before this quote can be executed. Carries `kind` and `url`. Mount the [challenge frame](/platform/frames/challenge) at `url`, then request the quote again. See [Upgrade a guest account](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up). |
### Exchange rate type
`exchangeRateType` tells you whether the quoted amount is final or an estimate, and pairs with the `exchangeRate` field. Bank transfers (SEPA) are a floating payment method: the quote is an estimate, and the exact amount is confirmed when the customer's funds settle. These quotes return `exchangeRateType: "floating"`. Card and wallet methods lock the rate at quote time and return `exchangeRateType: "fixed"`.
When `exchangeRateType` is `"floating"`, render the estimated crypto amount with a tilde (for example, `~0.2345 BTC`) and tell the customer the final amount is set when the transfer settles. When it is `"fixed"`, the quoted amount is final.
Branch on `exchangeRateType` when you format the amount the customer receives, so floating quotes always render with a tilde:
```ts Render the crypto amount theme={null}
// Frontend: the customer receives the destination (crypto) amount
function formatCryptoAmount(quote: Quote): string {
const amount = `${quote.destination.amount} ${quote.destination.asset.code}`;
// Floating quotes stay estimates until the transfer settles, so flag them.
return quote.exchangeRateType === "floating" ? `~${amount}` : amount;
}
// "~0.2345 BTC" for a SEPA quote; "0.2345 BTC" for a card or wallet quote
```
### `GetQuoteError`
`GetQuoteError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"invalid_request"`, `"unauthorized"`, `"unprocessable_entity"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors when the request fails validation. Each entry has a `field` and `message`. |
```ts types.ts theme={null}
type DevPlatformApiErrorCode =
| "unknown_error"
| "unauthorized"
| "forbidden"
| "not_found"
| "invalid_request"
| "not_implemented"
| "too_many_requests"
| "not_allowed"
| "conflict"
| "unprocessable_entity"
| "sse_timeout"
| "sse_error"
| "service_unavailable"
| "requirements_incomplete"
| "verification_rejected"
| "country_mismatch"
| "unsupported_country";
type DevPlatformApiErrorDetail = {
field?: string;
message: string;
};
type GetQuoteError = {
code: DevPlatformApiErrorCode;
message: string;
errors?: DevPlatformApiErrorDetail[];
};
```
# client.getTransaction()
Source: https://dev.moonpay.com/platform/sdk-reference/web/get-transaction
Fetch a single transaction by ID.
Use this method to fetch a single transaction by its ID, including its stage breakdown. Call it after a payment flow completes (for example, when [`client.setupApplePay()`](/platform/sdk-reference/web/setup-apple-pay), [`client.setupGooglePay()`](/platform/sdk-reference/web/setup-google-pay), or [`client.setupBuy()`](/platform/sdk-reference/web/setup-buy) emits `complete`) to poll for the final transaction status.
For bank-transfer payments (SEPA), this endpoint also returns the deposit details your app renders so the customer can pay. See [Bank-transfer deposit details](#bank-transfer-deposit-details).
For request and response details, see the [Get a transaction API](/api-reference/platform/endpoints/transactions/get).
```ts Get a transaction focus={5-15} theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const result = await client.getTransaction("tr_abc123");
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
console.log(result.value.data);
```
***
## Parameters
`client.getTransaction()` takes a single positional argument.
| Argument | Type | Required | Description |
| -------- | -------- | -------- | ---------------------------------- |
| `id` | `string` | ✅ | The MoonPay ID of the transaction. |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
## Result
`client.getTransaction()` returns a `Result<{ data: TransactionWithStages }, GetTransactionsError>`.
### Result envelope
`Result<{ data: TransactionWithStages }, GetTransactionsError>`
| Field | Type | Required | Description |
| ------- | --------------------------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`TransactionWithStages`](#transactionwithstages) `}` | | Present when `ok` is `true`. |
| `error` | [`GetTransactionsError`](#gettransactionserror) | | Present when `ok` is `false`. |
### `TransactionWithStages`
A transaction with its stage breakdown. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for every field, and the [Get a transaction API](/api-reference/platform/endpoints/transactions/get) for the response shape.
| Field | Type | Required | Description |
| ------------------------- | ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The MoonPay ID of the transaction. |
| `status` | `TransactionStatus` | ✅ | The current transaction status. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for the full list of statuses. |
| `source` | `object` | ✅ | The source amount and asset (fiat currency). |
| `destination` | `object` | ✅ | The destination amount and asset (cryptocurrency). |
| `createdAt` | `string` | ✅ | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp of when the transaction was created. |
| `stages` | `Stage[]` | ✅ | The transaction's pipeline stages, each with a `kind` and `status`. |
| `bankTransferDepositInfo` | `object` | | The bank account details the customer sends funds to. Present only on bank-transfer transactions (SEPA). See [Bank-transfer deposit details](#bank-transfer-deposit-details). |
### Bank-transfer deposit details
Bank-transfer payments (SEPA) settle when the customer sends funds to a MoonPay bank account. For these transactions, the response includes a `bankTransferDepositInfo` object with the account details and the payment reference. The transaction's `source.amount` (with `source.asset.code`) is the exact fiat amount the customer must send; render it alongside the deposit details.
Bank transfers are a floating payment method: the crypto amount is only final at settlement. While the transaction is pending, its `destination.amount` is a floating estimate, the same estimate the quote returns, so you don't need to retain the original quote. Render `destination.amount` with a tilde, making clear it's an estimate until the transfer lands. At settlement it becomes the final amount delivered. See [Exchange rate type](/platform/sdk-reference/web/get-quote#exchange-rate-type).
Your app renders these details natively so the customer can complete the transfer from their banking app. MoonPay does not render the deposit UI. Read the fields from `bankTransferDepositInfo` and display them in your own screen, then keep polling `client.getTransaction()` to track the transaction to a terminal status.
The customer must include the payment `reference` with their bank transfer.
Transfers sent without the reference are rejected. Surface this prominently in
your own UI, for example, "Always include your payment reference or your
transfer will be rejected."
| Field | Type | Required | Description |
| ------------------ | -------- | -------- | ------------------------------------------------------------------ |
| `reference` | `string` | ✅ | The payment reference the customer must include with the transfer. |
| `recipientName` | `string` | ✅ | The name of the recipient that receives the funds. |
| `recipientAddress` | `string` | ✅ | The address of the recipient. |
| `iban` | `string` | | The IBAN. Present for SEPA (EUR). |
| `bic` | `string` | | The BIC / SWIFT code. Present for SEPA (EUR). |
| `bankName` | `string` | | The name of the receiving bank. |
| `bankAddress` | `string` | | The address of the receiving bank. |
Show the customer the exact `reference` value. MoonPay uses it to match the
incoming transfer to this transaction. A missing or altered reference means
the transfer is rejected.
### `GetTransactionsError`
`GetTransactionsError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"not_found"`, `"unauthorized"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
## Example: poll until terminal status
After a payment flow emits `complete`, poll `client.getTransaction()` until the transaction reaches a terminal status. The set of terminal statuses depends on the flow — check the [Transaction object](/api-reference/platform/objects-and-types/transaction) reference for the values that apply.
```ts Poll for final status theme={null}
const TERMINAL_STATUSES = new Set(["completed", "failed"]);
async function pollTransaction(transactionId: string) {
while (true) {
const result = await client.getTransaction(transactionId);
if (!result.ok) {
throw new Error(result.error.message);
}
const transaction = result.value.data;
if (TERMINAL_STATUSES.has(transaction.status)) {
return transaction;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}
}
```
Poll this endpoint to track status, or use the existing
[`transaction-updated`](/api-reference/widget/webhooks/transaction-updated)
webhook if you already consume MoonPay webhooks. Bank-transfer transactions
stay `pending` from creation until they settle. When the customer's deposit
arrives, the `status` is still `pending`, so check the `stages` array: when
the `waiting_payment` stage's `status` is `"success"`, the deposit has
arrived.
# Identity methods
Source: https://dev.moonpay.com/platform/sdk-reference/web/identity
Create, update, and verify a customer identity, and upload identity documents, for Identity API integrations.
Use the identity methods to run KYC for Identity API integrations: capture the
required customer data in your own UI, submit it through the SDK, and bring the
customer to a verified state. The client handles authentication for you — no
raw access token is required.
These identity methods are deprecated. Use the [Customer
API](/platform/guides/customer-api) instead, which keys on `customerId`
instead of a separate identity resource. Build new integrations against the
Customer API, and migrate existing integrations before MoonPay removes these
methods.
The six methods wrap the Identity API endpoints and follow the same `Result`
pattern as the rest of the client:
| Method | Endpoint | Purpose |
| ------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------- |
| [`createIdentity()`](#clientcreateidentity) | `POST /platform/v1/identities` | Create an identity for the connected customer. |
| [`getIdentity()`](#clientgetidentity) | `GET /platform/v1/identities/{id}` | Fetch the identity, including its requirements. |
| [`updateIdentity()`](#clientupdateidentity) | `PATCH /platform/v1/identities/{id}` | Submit one or more outstanding requirements. |
| [`verifyIdentity()`](#clientverifyidentity) | `POST /platform/v1/identities/{id}/verifications` | Submit the identity for verification. |
| [`getIdentityUploadUrl()`](#clientgetidentityuploadurl) | `POST /platform/v1/identities/{id}/files/upload-url` | Issue a presigned URL for a document upload. |
| [`submitIdentityFiles()`](#clientsubmitidentityfiles) | `POST /platform/v1/identities/{id}/files` | Confirm previously uploaded documents. |
The identity methods require an authenticated client. Run
[`client.getConnection()`](/platform/sdk-reference/web/get-connection) with
`skipKyc: true` first, and authenticate the customer with
[`client.setupAuth()`](/platform/sdk-reference/web/setup-auth) when the status
is `"connectionRequired"`.
For the field and document requirements by country, see [KYC data
requirements](/platform/guides/kyc-data-requirements).
## `client.createIdentity()`
`createIdentity()` is deprecated. Customers are created automatically now, so
call [`getCustomer`](/platform/guides/customer-api#get-a-customer) directly
instead.
Create an identity for the connected customer. The response lists the
requirements you need to fulfil before verification.
```ts Create an identity theme={null}
const result = await client.createIdentity({
residentialAddress: { country: "USA" },
capabilities: [{ product: "ramps" }],
});
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
if (result.value.data === null) {
// The customer is already onboarded — no outstanding requirements.
return;
}
console.log(result.value.data.id, result.value.data.requirements);
```
### `createIdentity()` parameters
| Field | Type | Required | Description |
| ---------------------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------- |
| `residentialAddress.country` | `string` | ✅ | The customer's country of residence as an ISO 3166-1 alpha-3 code (for example, `"USA"`). |
| `capabilities` | `{ product: "ramps" }[]` | ✅ | The capabilities to unlock for the customer. Must contain at least one entry. |
### `createIdentity()` result
Returns a `Result<{ data: `[`Identity`](#identity)` | null }, DevPlatformApiError>`.
`data` is `null` (HTTP 204) when the customer has no outstanding requirements —
they are already onboarded.
## `client.getIdentity()`
`getIdentity()` is deprecated. Use
[`getCustomer`](/platform/guides/customer-api#get-a-customer) instead.
Fetch the identity, including the current status and requirement states.
```ts Get an identity theme={null}
const result = await client.getIdentity(identityId);
if (result.ok) {
console.log(result.value.data.status, result.value.data.requirements);
}
```
### `getIdentity()` parameters
| Field | Type | Required | Description |
| ----- | -------- | -------- | ------------------------ |
| `id` | `string` | ✅ | The identity identifier. |
### `getIdentity()` result
Returns a `Result<{ data: `[`Identity`](#identity)` }, DevPlatformApiError>`.
## `client.updateIdentity()`
`updateIdentity()` is deprecated. Use
[`submitCustomerKyc`](/platform/guides/customer-api#submit-kyc-data) instead.
Submit one or more outstanding requirements on the identity. Each top-level
field maps to a requirement category — submit the categories listed as
`incomplete` in [`requirements`](#requirements).
```ts Update an identity theme={null}
const result = await client.updateIdentity(identityId, {
basicDetails: {
firstName: "Ada",
lastName: "Lovelace",
dateOfBirth: "1990-12-10",
},
residentialAddress: {
street: "123 Main St",
locality: "San Francisco",
administrativeArea: "CA",
postalCode: "94105",
country: "USA",
},
phoneNumber: { number: "+12025550143" },
});
```
### `updateIdentity()` parameters
| Field | Type | Required | Description |
| -------------------- | -------- | -------- | ---------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The identity identifier. |
| `basicDetails` | `object` | | The customer's name, nationality, and date of birth (`YYYY-MM-DD`). |
| `residentialAddress` | `object` | | The customer's residential address. Country codes are ISO 3166-1 alpha-3. |
| `phoneNumber` | `object` | | The customer's phone number in E.164 format (for example, `"+12025550143"`). |
| `taxIdentifiers` | `array` | | Tax identifiers: `{ type: "ssn" \| "cpf", value }` or `{ type: "tin", country, value }`. |
### `updateIdentity()` result
Returns a `Result<{ data: `[`Identity`](#identity)` }, DevPlatformApiError>`
reflecting the updated requirement states.
## `client.verifyIdentity()`
`verifyIdentity()` is deprecated and has no direct replacement call. Customer
API verification starts automatically once you submit the last outstanding
requirement. See the [Customer API guide](/platform/guides/customer-api) for
the flow.
Submit the identity for verification once all requirements are complete.
```ts Verify an identity theme={null}
const result = await client.verifyIdentity(identityId);
if (!result.ok) {
// requirements_incomplete: submit the missing requirements first.
console.error(result.error.code, result.error.message);
return;
}
switch (result.value.data.status) {
case "approved":
// The customer is verified.
break;
case "processing":
// Verification is running — poll getIdentity() for the outcome.
break;
case "challengeRequired":
// Render the challenge frame with setupChallenge().
console.log(result.value.data.challenge.url);
break;
}
```
When the status is `"challengeRequired"`, pass `challenge.url` to
[`client.setupChallenge()`](/platform/sdk-reference/web/setup-challenge). The
challenge completes with `flow: "identity"` and the `identityId`.
### `verifyIdentity()` parameters
| Field | Type | Required | Description |
| ----- | -------- | -------- | ------------------------ |
| `id` | `string` | ✅ | The identity identifier. |
### `verifyIdentity()` result
Returns a `Result<{ data: IdentityVerificationResponse }, DevPlatformApiError>`.
| Field | Type | Required | Description |
| ----------- | ------------------------------------------------------- | -------- | --------------------------------------------------------------------------------- |
| `status` | `"approved"` \| `"processing"` \| `"challengeRequired"` | ✅ | The verification outcome. |
| `challenge` | `{ url: string; expiresAt: string }` | | Present when `status` is `"challengeRequired"`. Pass `url` to `setupChallenge()`. |
## `client.getIdentityUploadUrl()`
`getIdentityUploadUrl()` is deprecated. Use `getCustomerUploadUrl` instead.
See [Upload a file](/platform/guides/customer-api#upload-a-file) in the
Customer API guide for the replacement flow.
Issue a presigned URL to upload an identity document. Upload the file with an
HTTP `PUT` to the returned `url`, sending the returned `headers`.
```ts Upload a document theme={null}
const urlResult = await client.getIdentityUploadUrl(identityId, {
fileType: "passport",
mimeType: "image/jpeg",
});
if (!urlResult.ok) return;
const { uploadId, url, headers } = urlResult.value.data;
await fetch(url, {
method: "PUT",
headers,
body: passportImageBlob,
});
```
### `getIdentityUploadUrl()` parameters
| Field | Type | Required | Description |
| ---------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The identity identifier. |
| `fileType` | `string` | ✅ | One of `"drivingLicence"`, `"nationalIdentityCard"`, `"passport"`, `"residencePermit"`, `"selfie"`, `"proofOfAddress"`. |
| `side` | `string` | | `"front"` or `"back"`. Required for two-sided documents (`drivingLicence`, `nationalIdentityCard`, `residencePermit`); omit otherwise. |
| `mimeType` | `string` | ✅ | `"image/jpeg"`, `"image/png"`, or `"application/pdf"`. Must match the `Content-Type` header you send on the upload request. |
### `getIdentityUploadUrl()` result
Returns a `Result<{ data: IdentityFileUploadUrl }, DevPlatformApiError>`.
| Field | Type | Required | Description |
| ----------- | -------- | -------- | -------------------------------------------------------------- |
| `uploadId` | `string` | ✅ | The upload identifier to confirm with `submitIdentityFiles()`. |
| `url` | `string` | ✅ | The presigned upload URL. |
| `expiresAt` | `string` | ✅ | An ISO 8601 timestamp for when the URL expires. |
| `headers` | `object` | ✅ | Headers to send on the upload request (`Content-Type`). |
## `client.submitIdentityFiles()`
`submitIdentityFiles()` is deprecated. Use `submitCustomerFiles` instead. See
[Upload a file](/platform/guides/customer-api#upload-a-file) in the Customer
API guide for the replacement flow.
Confirm one or more uploaded documents so MoonPay processes them against the
identity's requirements.
```ts Submit uploaded documents theme={null}
const result = await client.submitIdentityFiles(identityId, {
files: [{ uploadId, fileType: "passport" }],
});
if (result.ok) {
console.log(result.value.data.requirements);
}
```
### `submitIdentityFiles()` parameters
| Field | Type | Required | Description |
| ------- | -------- | -------- | ----------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The identity identifier. |
| `files` | `array` | ✅ | The uploads to confirm: `{ uploadId, fileType, side? }`, matching the issued upload URLs. |
### `submitIdentityFiles()` result
Returns a `Result<{ data: `[`Identity`](#identity)` }, DevPlatformApiError>`
reflecting the updated requirement states.
## Shared types
### `Identity`
| Field | Type | Required | Description |
| -------------- | ------------------------------------ | -------- | -------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The identity identifier. |
| `status` | [`IdentityStatus`](#identitystatus) | ✅ | The identity's place in the KYC lifecycle. |
| `capabilities` | `{ product: "ramps" }[]` | ✅ | The capabilities requested for the customer. |
| `requirements` | [`Requirements`](#requirements) | ✅ | Outstanding and completed requirements, keyed by category. |
| `challenge` | `{ url: string; expiresAt: string }` | | Present only when `status` is `"challengeRequired"`. Pass `url` to `setupChallenge()`. |
| `createdAt` | `string` | ✅ | An ISO 8601 creation timestamp. |
| `updatedAt` | `string` | ✅ | An ISO 8601 last-update timestamp. |
### `IdentityStatus`
`"created"`, `"collecting"`, `"verifying"`, `"challengeRequired"`,
`"approved"`, `"rejected"`, `"manualReview"`, or `"blocked"`.
### `Requirements`
Each category is present when it applies to the customer's jurisdiction, with a
`status` of `"incomplete"` or `"complete"` and, for field-based categories, the
`requiredFields` still outstanding.
| Category | Fulfilled by |
| -------------------- | -------------------------------------------------- |
| `basicDetails` | `updateIdentity()` |
| `residentialAddress` | `updateIdentity()` |
| `phoneNumber` | `updateIdentity()` |
| `taxIdentifiers` | `updateIdentity()` |
| `identityDocuments` | `getIdentityUploadUrl()` + `submitIdentityFiles()` |
| `selfie` | `getIdentityUploadUrl()` + `submitIdentityFiles()` |
| `proofOfAddress` | `getIdentityUploadUrl()` + `submitIdentityFiles()` |
### Errors
All identity methods return the standard MoonPay Platform API error shape,
`DevPlatformApiError`, with `code`, `message`, and optional field-level
`errors`. Notable codes: `"requirements_incomplete"` when verifying too early,
`"verification_rejected"` on a terminal rejection, and `"country_mismatch"`
when submitted data conflicts with the declared country.
# client.listTransactions()
Source: https://dev.moonpay.com/platform/sdk-reference/web/list-transactions
List the connected customer's transactions with optional filters and pagination.
Use this method to list the connected customer's transactions. You can filter by date range and page through results with a cursor.
For request and response details, see the [List transactions API](/api-reference/platform/endpoints/transactions/list).
```ts List transactions focus={5-16} theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const result = await client.listTransactions({
startDate: "2026-01-01",
endDate: "2026-01-31",
limit: 20,
});
if (!result.ok) {
// Handle error
console.error(result.error.code, result.error.message);
return;
}
console.log(result.value.data); // Transaction[]
console.log(result.value.pageInfo); // Pagination info
```
***
## Parameters
`client.listTransactions()` takes an optional `params` object. Call it with no arguments to fetch the most recent transactions without filters.
| Field | Type | Required | Description |
| ----------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `startDate` | `string` | | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date. Only return transactions created on or after this date. |
| `endDate` | `string` | | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date. Only return transactions created on or before this date. |
| `cursor` | `string` | | A pagination cursor returned from a previous call. Use it to fetch the next page. |
| `limit` | `number` | | The maximum number of transactions to return in one page. |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
## Result
`client.listTransactions()` returns a `Result<{ data: Transaction[]; pageInfo: PaginationInfo }, GetTransactionsError>`.
### Result envelope
`Result<{ data: Transaction[]; pageInfo: PaginationInfo }, GetTransactionsError>`
| Field | Type | Required | Description |
| ------- | ---------------------------------------------------------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `{ data:` [`Transaction`](#transaction)`[]; pageInfo:` [`PaginationInfo`](#paginationinfo) `}` | | Present when `ok` is `true`. |
| `error` | [`GetTransactionsError`](#gettransactionserror) | | Present when `ok` is `false`. |
### `Transaction`
Each entry in `data` is a full transaction. Key fields include `id`, `status`, `source.amount`, `destination.amount`, and `createdAt`. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for every field.
| Field | Type | Required | Description |
| ------------- | ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The MoonPay ID of the transaction. |
| `status` | `TransactionStatus` | ✅ | The current transaction status. See the [Transaction object](/api-reference/platform/objects-and-types/transaction) for the full list of statuses. |
| `source` | `object` | ✅ | The source amount and asset (fiat currency). |
| `destination` | `object` | ✅ | The destination amount and asset (cryptocurrency). |
| `createdAt` | `string` | ✅ | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp of when the transaction was created. |
### `PaginationInfo`
Cursor-based pagination details returned alongside the data. See the [List transactions API](/api-reference/platform/endpoints/transactions/list) for the exact field names used in this response.
### `GetTransactionsError`
`GetTransactionsError` is the standard MoonPay Platform API error shape, `DevPlatformApiError`.
| Field | Type | Required | Description |
| --------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code` | `DevPlatformApiErrorCode` | ✅ | A machine-readable error code (for example, `"unauthorized"`, `"invalid_request"`). See the [error reference](/api-reference/platform/documentation/using-the-api) for the full list. |
| `message` | `string` | ✅ | A developer-friendly message. |
| `errors` | `DevPlatformApiErrorDetail[]` | | Optional list of field-level errors. |
## Example: page through transactions
Use the cursor returned in `pageInfo` to fetch additional pages. The exact cursor field name is documented in the [List transactions API](/api-reference/platform/endpoints/transactions/list) response.
```ts Page through transactions theme={null}
async function listAllTransactions() {
const all = [];
let cursor: string | undefined;
do {
const result = await client.listTransactions({ limit: 50, cursor });
if (!result.ok) {
throw new Error(result.error.message);
}
all.push(...result.value.data);
// pageInfo carries the cursor for the next page; see the API reference
// for the exact field name (for example, `pageInfo.endCursor`).
cursor = result.value.pageInfo.endCursor;
} while (cursor);
return all;
}
```
# Overview
Source: https://dev.moonpay.com/platform/sdk-reference/web/overview
Use the Web SDK to build fiat-to-crypto ramps with headless payments.
The Web SDK targets first-party browser apps. It ships with TypeScript types for autocomplete and inference.
The SDK is tested against the latest versions of:
* Chrome (Desktop, iOS, Android)
* Safari (macOS)
* Safari (iOS)
* Firefox
Building a React Native app? Use the [React Native
SDK](/platform/sdk-reference/react-native/overview) instead. For iOS, Android,
or Flutter, drive [frames](/platform/frames) directly — see the [manual
integration overview](/platform/guides/manual-integration/overview).
## Install
Install the SDK package:
```bash pnpm theme={null}
pnpm i @moonpay/platform-sdk-web
```
```bash bun theme={null}
bun add @moonpay/platform-sdk-web
```
```bash npm theme={null}
npm i @moonpay/platform-sdk-web
```
## Conventions
### `Result`
Most SDK functions return a `Result` instead of throwing.
| Field | Type | Required | Description |
| ------- | --------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | `T` | | Present when `ok` is `true`. |
| `error` | `E` | | Present when `ok` is `false`. |
Use this pattern to branch on success vs failure:
```ts theme={null}
const result = await someSdkCall();
if (!result.ok) {
// Handle error
console.error(result.error);
return;
}
console.log(result.value);
```
## Function reference
These pages document individual SDK functions, including parameters and return types:
# client.resetConnection()
Source: https://dev.moonpay.com/platform/sdk-reference/web/reset-connection
Clear the customer's MoonPay connection for this partner.
Use this method to clear the customer's MoonPay connection in the current browser when they sign out of your app. It runs the [reset frame](/platform/frames/reset) in a hidden iframe and resolves once the reset completes — or after a 5-second timeout, whichever happens first.
`resetConnection()` always resolves with `ok` set to `true`. The method intentionally does not surface errors — a failed reset should never block your sign-out flow.
```ts Reset the connection theme={null}
import { createClient } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
// Call this after clearing your own local auth state.
await client.resetConnection();
```
***
## Parameters
`client.resetConnection()` takes no parameters.
## Result
`client.resetConnection()` returns a `Result`. It always resolves with `ok: true` — even if the underlying frame fails to load or times out — so reset failures never block sign-out.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ----------------------------------------------- | -------- | ------------------------------------------------ |
| `ok` | `boolean` | ✅ | Always `true` for this method. |
| `value` | `undefined` | | Present when `ok` is `true`. Always `undefined`. |
| `error` | [`ResetConnectionError`](#resetconnectionerror) | | Reserved for future use. Not currently emitted. |
### `ResetConnectionError`
Reserved for future use. The current SDK never surfaces a reset error — failures are silently ignored so sign-out can always proceed.
| Field | Type | Required | Description |
| --------- | -------- | -------- | ------------------------------------------------ |
| `message` | `string` | ✅ | A developer-friendly description of the failure. |
# client.setupAddCard()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-add-card
Render the Add Card frame to capture a new card from the customer.
Render the Add Card frame into your UI so the customer can save a new credit or
debit card. Card data is captured inside a PCI-compliant MoonPay-hosted UI and
never touches your domain.
```ts Setup Add Card focus={5-22} theme={null}
import { createClient, type AddCardEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const addCardResult = await client.setupAddCard({
container: document.querySelector("#addCardContainer"),
onEvent: (event: AddCardEvent) => {
switch (event.kind) {
case "complete":
// Card added. Use event.payload.card.id to get a quote.
console.log(event.payload.card);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!addCardResult.ok) {
// Handle error
console.error(addCardResult.error.kind, addCardResult.error.message);
return;
}
const addCard = addCardResult.value;
```
***
## Parameters
| Property | Type | Required | Description |
| ----------- | ------------------------------- | -------- | ------------------------------------------------------------------------------- |
| `container` | `HTMLElement` | ✅ | A DOM element to render the Add Card frame into. |
| `onEvent` | `(event: AddCardEvent) => void` | | Callback invoked for Add Card flow events. See [`AddCardEvent`](#addcardevent). |
This method does not require a separate auth token. The client uses stored
credentials from an active connection.
### `AddCardEvent`
`onEvent` receives events as the Add Card flow progresses. Use `event.kind` to
decide how to handle each event.
| kind | Payload | When you receive it |
| ------------ | --------------------------------------------- | -------------------------------------------------------- |
| `"ready"` | — | The frame finished loading and the card form is visible. |
| `"complete"` | `{ card: `[`CardResponse`](#cardresponse)` }` | The card was saved successfully. |
| `"error"` | [`AddCardEventError`](#addcardeventerror) | The flow encountered an error. |
#### `CardResponse`
The card object returned when the customer finishes adding a card. Use `id`
directly in [`getQuote`](/platform/sdk-reference/web/get-quote) — there is no
need to re-fetch payment methods.
| Field | Type | Required | Description |
| ----------------- | ------------------ | -------- | ------------------------------------------------------------------------- |
| `id` | `string` | ✅ | The stored card identifier. |
| `type` | `string` | ✅ | The payment method type, typically `"card"`. |
| `cardType` | `string` | ✅ | The card sub-type (for example, `"credit"` or `"debit"`). |
| `brand` | `string` | ✅ | The card network brand (for example, `"visa"`, `"mastercard"`). |
| `last4` | `string` | ✅ | The last four digits of the card number. |
| `expirationMonth` | `string` | ✅ | The card expiration month, as a two-digit string (for example, `"04"`). |
| `expirationYear` | `string` | ✅ | The card expiration year, as a four-digit string (for example, `"2030"`). |
| `availability` | `{ active: true }` | ✅ | The card is available for new transactions when the frame returns it. |
#### `AddCardEventError`
| Field | Type | Required | Description |
| --------- | ------------------------------------- | -------- | --------------------------- |
| `code` | `"configurationError"` \| `"generic"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Result
`client.setupAddCard()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ----------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`AddCardFrame`](#addcardframe) | | Present when `ok` is `true`. |
| `error` | [`SetupAddCardError`](#setupaddcarderror) | | Present when `ok` is `false`. |
### `AddCardFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
The Add Card frame does not expose a `setQuote` method — quotes are issued
after the card is saved.
### `SetupAddCardError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Example: Add a card, then buy
After the `complete` event fires, pass `card.id` to
[`getQuote`](/platform/sdk-reference/web/get-quote) and use the returned
signature with [`setupBuy`](/platform/sdk-reference/web/setup-buy) to execute
the transaction. For the full end-to-end walkthrough, see the [Pay with
card](/platform/guides/pay-with-card) guide.
```ts Add card, then buy theme={null}
import type { AddCardEvent, BuyEvent } from "@moonpay/platform-sdk-web";
const addCardResult = await client.setupAddCard({
container: document.querySelector("#addCardContainer"),
onEvent: async (event: AddCardEvent) => {
if (event.kind !== "complete") return;
const cardId = event.payload.card.id;
// Tear down the Add Card frame — we have the card we need.
addCardResult.value.dispose();
// Request a quote for the new card.
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
paymentMethod: { type: "card", id: cardId },
});
if (!quoteResult.ok) {
// Handle error
return;
}
// Execute the transaction with the headless buy frame.
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
container: document.querySelector("#buyContainer"),
onEvent: (event: BuyEvent) => {
// Handle buy events (see the Pay with card guide).
},
});
if (!buyResult.ok) {
// Handle error
return;
}
},
});
if (!addCardResult.ok) {
// Handle error
return;
}
```
```ts types.ts theme={null}
type AddCardFrame = {
dispose: () => void;
};
type CardResponse = {
id: string;
/** Payment method type, typically "card". */
type: string;
/** Card sub-type, for example "credit" or "debit". */
cardType: string;
/** Card network brand, for example "visa" or "mastercard". */
brand: string;
last4: string;
/** Two-digit month string, for example "04". */
expirationMonth: string;
/** Four-digit year string, for example "2030". */
expirationYear: string;
availability: { active: true };
};
type AddCardEvent =
| {
kind: "ready";
}
| {
kind: "complete";
payload: {
card: CardResponse;
};
}
| {
kind: "error";
payload: AddCardEventError;
};
type AddCardEventError = {
code: "configurationError" | "generic";
/** A developer-facing error message. Not intended to be rendered in UI. */
message: string;
};
type SetupAddCardError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupApplePay()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-apple-pay
Render the Apple Pay frame and start a transaction.
Render the Apple Pay frame into your UI and initiate a transaction with a quote signature. The quote must have `executable: true`.
To let new customers buy with Apple Pay before they have a MoonPay account, see
[Guest checkout](/platform/guides/guest-checkout). Guest checkout uses the same
`setupApplePay` call. Second-factor and KYC step-up arrive as a `"challenge"`
event.
```ts Setup Apple Pay focus={5-30} theme={null}
import { createClient, type ApplePayEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const applePayResult = await client.setupApplePay({
quote: quoteResult.value.data.signature,
container: document.querySelector("#applePayContainer"),
onEvent: (event: ApplePayEvent) => {
switch (event.kind) {
case "ready":
break;
case "buttonPressed":
// Customer tapped the button — show a loading state or fire analytics.
break;
case "complete":
console.log(event.payload.transaction);
break;
case "quoteExpired":
// Fetch a new quote, then update the frame:
// event.payload.setQuote(newQuote.signature);
break;
case "error":
console.error(event.payload.kind, event.payload.message);
break;
case "unsupported":
// Apple Pay isn't available in this environment — fall back to another method.
break;
}
},
});
if (!applePayResult.ok) {
// Handle error
console.error(applePayResult.error.kind, applePayResult.error.message);
return;
}
const applePay = applePayResult.value;
```
***
## Parameters
| Property | Type | Required | Description |
| ----------------------- | -------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/web/get-quote). |
| `container` | `HTMLElement` | ✅ | A DOM element to render the Apple Pay frame into. |
| `externalTransactionId` | `string` | | Partner-assigned identifier for this transaction attempt. Useful for reconciliation. |
| `onEvent` | `(event: ApplePayEvent) => void` | | Callback invoked for Apple Pay flow events. See [`ApplePayEvent`](#applepayevent). |
This method does not require a separate auth token. The client uses stored credentials from an active connection, or from the connection check on [guest checkout](/platform/guides/guest-checkout).
### `ApplePayEvent`
`onEvent` receives events as the Apple Pay flow progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The Apple Pay UI is rendered and ready to be shown. |
| `"buttonPressed"` | — | The customer tapped the Apple Pay button, before iOS presents the payment sheet. An intent-to-buy signal with no payload — it still fires if the customer then cancels the sheet. Not a purchase outcome; listen for `"complete"` for the result. |
| `"complete"` | `{ transaction:` [`FrameTransaction`](#frametransaction) `}` | The Apple Pay flow finished. Inspect the `FrameTransaction` for the outcome. |
| `"challenge"` | `{ kind: "frame"; url: string }` | Verification required. Render the challenge frame at the provided URL using [`setupChallenge()`](/platform/sdk-reference/web/setup-challenge). |
| `"quoteExpired"` | `{ setQuote: (signature: string) => void }` | The quote signature expired. Fetch a new quote and call `payload.setQuote(...)`. |
| `"error"` | [`ApplePayEventError`](#applepayeventerror) | The flow encountered an error. |
| `"unsupported"` | — | Apple Pay isn't available in the user's current environment. |
#### `FrameTransaction`
The transaction object returned on `"complete"`. `FrameTransaction` is a discriminated union — the failure variant carries `failureReason`, the non-failure variant always carries `id`.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `status` | `string` | ✅ | The transaction status. On the failure variant, `"failed"`. |
| `id` | `string` | | Required on the non-failure variant; optional when `status` is `"failed"` (a transaction may not exist yet on early failure). |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). |
#### `ApplePayEventError`
| Field | Type | Required | Description |
| --------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"invalidQuote"` \| `"quoteExpired"` \| `"oneTapApplePaySecondFactorRequired"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
`"oneTapApplePaySecondFactorRequired"` is emitted on the 1TAP Apple Pay flow when MoonPay needs a second authentication factor — typically because the device is new or the customer hasn't completed a recent challenge. Hand off to the full connect flow with [`client.connect()`](/platform/sdk-reference/web/connect), then retry.
Apple Pay being unavailable in the environment (for example, the browser doesn't support it) is signalled separately through the `"unsupported"` event, not as an error.
## Result
`client.setupApplePay()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`ApplePayFrame`](#applepayframe) | | Present when `ok` is `true`. |
| `error` | [`SetupApplePayError`](#setupapplepayerror) | | Present when `ok` is `false`. |
### `ApplePayFrame`
| Field | Type | Required | Description |
| ---------- | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `setQuote` | `(signature: string) => void` | ✅ | Updates the quote signature used by the frame. Use this in response to `quoteExpired`. |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
### `SetupApplePayError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
Setup errors cover frame initialization only (for example, a handshake
timeout). Quote problems, second-factor requirements, and Apple Pay
availability are reported through `onEvent` after setup succeeds — as the
`"error"`, `"quoteExpired"`, and `"unsupported"` events respectively.
```ts types.ts theme={null}
type ApplePayFrame = {
setQuote: (signature: string) => void;
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type ApplePayEvent =
| { kind: "ready" }
| { kind: "buttonPressed" }
| {
kind: "complete";
payload: { transaction: FrameTransaction };
}
| {
kind: "challenge";
payload: {
kind: "frame";
url: string;
};
}
| {
kind: "quoteExpired";
payload: { setQuote: (signature: string) => void };
}
| { kind: "error"; payload: ApplePayEventError }
| { kind: "unsupported" };
type ApplePayEventError = {
kind:
| "configurationError"
| "invalidQuote"
| "quoteExpired"
| "oneTapApplePaySecondFactorRequired"
| "genericError";
message: string;
};
type SetupApplePayError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupAuth()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-auth
Render the Auth frame to authenticate a customer with email and OTP.
Render the Auth frame into your UI to authenticate the customer with email and
a one-time passcode. The Auth frame is the lighter-weight counterpart to
[`client.connect()`](/platform/sdk-reference/web/connect) for headless and
Customer API integrations. It handles authentication only, with no
payment-method setup or KYC steps. When the customer finishes, the SDK
decrypts the returned credentials and primes the client so that subsequent SDK
calls are authenticated automatically.
Call [`client.getConnection()`](/platform/sdk-reference/web/get-connection)
first. The connection check primes the client with the token the Auth frame
needs. If you call `setupAuth()` without a prior connection check that
resolved with `status: "connectionRequired"`, it returns a
`configurationError`.
```ts Setup auth focus={5-30} theme={null}
import { createClient, type AuthEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const connectionResult = await client.getConnection({ skipKyc: true });
if (!connectionResult.ok) {
// Handle error
return;
}
if (connectionResult.value.status === "connectionRequired") {
const authResult = await client.setupAuth({
container: document.querySelector("#authContainer"),
onEvent: (event: AuthEvent) => {
switch (event.kind) {
case "ready":
// The auth UI is rendered. Reveal the container if you hide it while loading.
break;
case "complete":
if (event.payload.status === "active") {
// Customer authenticated — SDK calls are now authenticated.
console.log(event.payload.customer.id);
}
break;
case "error":
console.error(event.payload);
break;
}
},
});
if (!authResult.ok) {
// Handle error
console.error(authResult.error.kind, authResult.error.message);
return;
}
// Remove the frame from the DOM now that the flow has completed:
authResult.value.dispose();
}
```
The promise returned by `client.setupAuth()` resolves after the customer
completes the auth flow (or an error ends it). Track flow progress — including
the moment the UI is ready to show — through `onEvent`, not by awaiting the
promise.
***
## Parameters
| Field | Type | Required | Description |
| ----------- | ---------------------------- | -------- | --------------------------------------------------------------------- |
| `container` | `HTMLElement` | ✅ | A DOM element to render the Auth frame into. |
| `onEvent` | `(event: AuthEvent) => void` | | Callback invoked for auth flow events. See [`AuthEvent`](#authevent). |
### `AuthEvent`
`onEvent` receives events as the auth flow progresses. Use `event.kind` to
decide how to handle each event.
| kind | Payload | When you receive it |
| ------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The auth UI is rendered and ready to be shown. |
| `"complete"` | [`Connection`](/platform/sdk-reference/web/get-connection#connection) | The customer authenticated. `status` is `"active"`, or `"termsAcceptanceRequired"` when a Terms of Use attestation is still outstanding. |
| `"error"` | [`AuthEventError`](#autheventerror) | The flow encountered an error. |
#### `AuthEventError`
The error event payload comes from the underlying auth frame. It is
discriminated by `code`.
| Field | Type | Required | Description |
| --------- | ------------------------------------- | -------- | ---------------------------------------------------------------------- |
| `code` | `"validationError"` \| `"generic"` | ✅ | The error category. |
| `errors` | `{ code: string; message: string }[]` | | Field-level errors. Present when `code` is `"validationError"`. |
| `message` | `string` | | Developer-friendly details. May be present when `code` is `"generic"`. |
## Result
`client.setupAuth()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | ----------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`AuthFrame`](#authframe) | | Present when `ok` is `true`. |
| `error` | [`SetupAuthError`](#setupautherror) | | Present when `ok` is `false`. |
### `AuthFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
### `SetupAuthError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. `"configurationError"` means the client has no token from a prior connection check. |
| `message` | `string` | ✅ | Developer-friendly details. |
```ts types.ts theme={null}
type AuthFrame = {
dispose: () => void;
};
type AuthEvent =
| { kind: "ready" }
| {
kind: "complete";
/** status is "active" or "termsAcceptanceRequired" */
payload: Connection;
}
| { kind: "error"; payload: AuthEventError };
type AuthEventError =
| {
code: "validationError";
errors: { code: string; message: string }[];
}
| {
code: "generic";
message?: string;
};
type SetupAuthError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupBuy()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-buy
Mount the headless Buy frame and execute a transaction from a quote.
Mount the headless [Buy frame](/platform/frames/buy) into your UI and execute a transaction from a quote signature. The frame renders no visible UI — it drives the buy pipeline in the background and emits events you handle from your own purchase screen.
The Buy frame is headless. When it emits a `challenge` event, render a
separate challenge frame with
[`client.setupChallenge()`](/platform/sdk-reference/web/setup-challenge) at
the URL from the event payload. See the [Handle
challenges](/platform/guides/handling-challenges) guide for the full flow.
```ts Setup buy focus={6-40} theme={null}
import { createClient, type BuyEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
container: document.querySelector("#buyContainer"),
externalTransactionId: "order_12345",
onEvent: (event: BuyEvent) => {
switch (event.kind) {
case "ready":
// Pipeline starting — show a loading indicator
break;
case "complete":
// Transaction created. Track final status via polling.
console.log(event.payload.transaction);
break;
case "challenge":
// Verification required — render the challenge frame at the URL.
// See: /platform/guides/handling-challenges
openChallengeFrame(event.payload.url);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!buyResult.ok) {
// Handle error
console.error(buyResult.error.kind, buyResult.error.message);
return;
}
const buy = buyResult.value;
```
***
## Parameters
| Property | Type | Required | Description |
| ----------------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `quote` | `string` | ✅ | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/web/get-quote). |
| `container` | `HTMLElement` | ✅ | A DOM element to mount the headless Buy frame into. The frame has zero dimensions and renders no visible UI. |
| `externalTransactionId` | `string` | | Your own identifier for the transaction. Stored on the MoonPay transaction for correlation. |
| `onEvent` | `(event: BuyEvent) => void` | | Callback invoked for buy flow events. See [`BuyEvent`](#buyevent). |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
### `BuyEvent`
`onEvent` receives events as the buy pipeline progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The buy pipeline has started. Show a loading state in your own UI. |
| `"complete"` | `{ transaction: `[`FrameTransaction`](#frametransaction)` }` | The transaction was created. Use the transaction `id` to poll for final status. |
| `"challenge"` | [`BuyChallengePayload`](#buychallengepayload) | Verification is required before the transaction can proceed. Render the [challenge frame](/platform/guides/handling-challenges) at the provided `url` using `setupChallenge()`. |
| `"error"` | [`BuyEventError`](#buyeventerror) | The flow encountered an error. Surface the message to developers and tear down the frame. |
#### `FrameTransaction`
This is the transaction object returned when the buy pipeline completes. `FrameTransaction` is a discriminated union — the failure variant carries `failureReason`, the non-failure variant always carries `id`. Pass `id` to [`client.getTransaction()`](/platform/sdk-reference/web/get-transaction) to poll for the final status.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `status` | `string` | ✅ | The transaction status. On the failure variant, `"failed"`. |
| `id` | `string` | | Required on the non-failure variant; optional when `status` is `"failed"` (a transaction may not exist yet on early failure). |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). A developer-friendly reason. |
#### `BuyChallengePayload`
| Field | Type | Required | Description |
| ------ | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `kind` | `string` | ✅ | The challenge type (currently always `"frame"`, but typed as `string` for forward compatibility). |
| `url` | `string` | ✅ | A fully-formed URL to pass directly to [`setupChallenge()`](/platform/sdk-reference/web/setup-challenge). Do not modify it. |
#### `BuyEventError`
| Field | Type | Required | Description |
| --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `code` | `string` | ✅ | The error category. Includes `configurationError`, `invalidQuote`, and backend-specific error codes. |
| `message` | `string` | ✅ | Developer-friendly details. Not intended to be rendered in UI. |
## Result
`client.setupBuy()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`BuyFrame`](#buyframe) | | Present when `ok` is `true`. |
| `error` | [`SetupBuyError`](#setupbuyerror) | | Present when `ok` is `false`. |
### `BuyFrame`
| Field | Type | Required | Description |
| ---------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setQuote` | `(signature: string) => void` | ✅ | Updates the quote signature used by the frame. Use this when the current quote expires before the customer completes the purchase — fetch a new quote and pass its `signature`. |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. Always call `dispose()` once the flow finishes or errors out. |
### `SetupBuyError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Full example
The following example walks through the full card-payment flow: get a quote for a stored card, mount the headless Buy frame, hand off to a challenge frame when verification is required, and dispose of the frame when the transaction completes.
```ts Buy with a stored card theme={null}
import {
createClient,
type BuyEvent,
type ChallengeEvent,
} from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
// 1. Get a quote for the selected stored card.
const quoteResult = await client.getQuote({
source: { asset: { code: "USD" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
paymentMethod: { type: "card", id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" },
});
if (!quoteResult.ok) throw new Error(quoteResult.error.message);
// 2. Mount the headless Buy frame.
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
container: document.querySelector("#buyContainer"),
externalTransactionId: "order_12345",
onEvent: (event: BuyEvent) => {
switch (event.kind) {
case "ready":
showLoadingIndicator();
break;
case "challenge":
// 3. Hand off to the challenge frame for verification.
handleChallenge(event.payload.url);
break;
case "complete":
// The transaction was created (or failed). Inspect FrameTransaction
// before polling — `id` is only required on the non-failure variant.
buyResult.value.dispose();
if (event.payload.transaction.status !== "failed") {
pollTransaction(event.payload.transaction.id);
}
break;
case "error":
buyResult.value.dispose();
console.error(event.payload.code, event.payload.message);
showError(event.payload.message);
break;
}
},
});
if (!buyResult.ok) {
console.error(buyResult.error.kind, buyResult.error.message);
return;
}
async function handleChallenge(challengeUrl: string) {
await client.setupChallenge({
url: challengeUrl,
container: document.querySelector("#challengeModal"),
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "complete":
// Verification resolved. For buy challenges, the transaction is in payload.
if (event.payload.flow === "buy") {
pollTransaction(event.payload.transaction.id);
}
buyResult.value.dispose();
break;
case "cancelled":
buyResult.value.dispose();
showRetryOption();
break;
case "error":
buyResult.value.dispose();
console.error(event.payload.message);
break;
}
},
});
}
```
For the end-to-end card payment walkthrough — listing payment methods, adding a card, and tracking the transaction to a terminal status — see the [Pay with card](/platform/guides/pay-with-card) guide. For details on the challenge flow, see [Handle challenges](/platform/guides/handling-challenges).
## Bank transfer
Bank-transfer payments (SEPA for EUR) use the same headless flow as cards. You fetch a bank-transfer quote, mount the Buy frame with its signature, and read the transaction `id` from the `complete` event. The difference is what happens after the transaction is created: instead of charging a card, the customer sends funds to a MoonPay bank account, so you render the deposit details yourself.
Bank transfers are a floating payment method, so the amounts are estimates until the transfer settles. The quote returns `exchangeRateType: "floating"`; render the estimated crypto amount with a tilde (for example, `~0.2345 BTC`) and tell the customer the final amount is set when their funds settle. See [Exchange rate type](/platform/sdk-reference/web/get-quote#exchange-rate-type).
For the full walkthrough, including confirming availability, rendering the
deposit details, handling requotes and cancellations, and tracking the
transaction to a terminal status, see the
[Pay with bank transfer](/platform/guides/pay-with-bank-transfer) guide.
```ts Buy with a bank transfer theme={null}
import { createClient, type BuyEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
// 1. Get a bank-transfer quote (SEPA for EUR).
const quoteResult = await client.getQuote({
source: { asset: { code: "EUR" }, amount: "100.00" },
destination: { asset: { code: "ETH" } },
wallet: { address: "0x1234567890abcdef1234567890abcdef12345678" },
paymentMethod: { type: "sepa" },
});
if (!quoteResult.ok) throw new Error(quoteResult.error.message);
// 2. Mount the headless Buy frame from your own pay button.
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
container: document.querySelector("#buyContainer"),
onEvent: (event: BuyEvent) => {
if (
event.kind === "complete" &&
event.payload.transaction.status !== "failed"
) {
// 3. Read the deposit details and render them natively.
showDepositDetails(event.payload.transaction.id);
}
},
});
if (!buyResult.ok) throw new Error(buyResult.error.message);
async function showDepositDetails(transactionId: string) {
const result = await client.getTransaction(transactionId);
if (!result.ok) throw new Error(result.error.message);
// Render bankTransferDepositInfo in your own UI, then keep polling for status.
renderBankTransferScreen(result.value.data.bankTransferDepositInfo);
}
```
The customer must include the payment `reference` with their bank transfer.
Transfers sent without it are rejected. Surface this prominently in your own
UI, for example, "Always include your payment reference or your transfer will
be rejected." See [Bank-transfer deposit
details](/platform/sdk-reference/web/get-transaction#bank-transfer-deposit-details).
```ts types.ts theme={null}
type BuyFrame = {
setQuote: (signature: string) => void;
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type BuyEvent =
| {
kind: "ready";
}
| {
kind: "complete";
payload: {
transaction: FrameTransaction;
};
}
| {
kind: "challenge";
payload: {
/** Currently "frame", but typed as `string` for forward compatibility. */
kind: string;
/** Fully-formed URL to pass directly to setupChallenge(). */
url: string;
};
}
| {
kind: "error";
payload: BuyEventError;
};
type BuyEventError = {
/**
* Includes "configurationError", "invalidQuote", and backend-specific
* error codes returned during the buy pipeline.
*/
code: string;
message: string;
};
type SetupBuyError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupBuyButton()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-buy-button
Render the MoonPay-hosted buy button and run the buy pipeline on tap.
Render the [Buy button frame](/platform/frames/buy-button) into your UI. The frame renders a payment button — card, Apple Pay, or Google Pay, depending on what's available — and runs the buy pipeline on tap. Use it when you want a single payment button instead of orchestrating individual payment-method frames yourself.
The buy button covers card, Apple Pay, and Google Pay. For bank transfers
(SEPA), render your own button and open the headless Buy frame with
[`client.setupBuy()`](/platform/sdk-reference/web/setup-buy#bank-transfer)
instead.
Like the headless [Buy frame](/platform/sdk-reference/web/setup-buy), the buy
button can emit a `challenge` event mid-pipeline. Render a separate challenge
frame using
[`client.setupChallenge()`](/platform/sdk-reference/web/setup-challenge) at
the URL from the event payload.
```ts Setup buy button focus={5-30} theme={null}
import { createClient, type BuyButtonEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const buyButtonResult = await client.setupBuyButton({
quote: quoteResult.value.data.signature,
container: document.querySelector("#buyButtonContainer"),
onEvent: (event: BuyButtonEvent) => {
switch (event.kind) {
case "ready":
// Button is rendered — hide any loading placeholder.
break;
case "buttonPressed":
// Customer tapped the button — show a loading state or fire analytics.
break;
case "complete":
// Inspect FrameTransaction before polling.
if (event.payload.transaction.status !== "failed") {
pollTransaction(event.payload.transaction.id);
}
break;
case "challenge":
// Hand off to the challenge frame at the provided URL.
handleChallenge(event.payload.url);
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!buyButtonResult.ok) {
// Handle error
console.error(buyButtonResult.error.kind, buyButtonResult.error.message);
return;
}
const buyButton = buyButtonResult.value;
```
***
## Parameters
| Property | Type | Required | Description |
| ----------------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/web/get-quote). |
| `container` | `HTMLElement` | ✅ | A DOM element to render the buy button into. |
| `externalTransactionId` | `string` | | Partner-assigned identifier for this transaction attempt. Useful for reconciliation. |
| `onEvent` | `(event: BuyButtonEvent) => void` | | Callback invoked for buy-button events. See [`BuyButtonEvent`](#buybuttonevent). |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
### `BuyButtonEvent`
`onEvent` receives events as the buy pipeline progresses.
| kind | Payload | When you receive it |
| ----------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The button is rendered and ready for the customer to tap. For Apple Pay and Google Pay, this fires once the device is confirmed to support the wallet. Use it to hide any loading placeholder. |
| `"buttonPressed"` | — | The customer tapped the pay button, before the payment sheet appears. An intent-to-buy signal with no payload — it still fires if the customer then cancels. Not a purchase outcome; listen for `"complete"` for the result. |
| `"complete"` | `{ transaction:` [`FrameTransaction`](#frametransaction) `}` | The pipeline finished. Inspect the `FrameTransaction` to detect the failure variant. |
| `"challenge"` | `{ kind: string; url: string }` | Verification is required before the transaction can proceed. Render the [challenge frame](/platform/sdk-reference/web/setup-challenge) at the provided `url`. |
| `"error"` | [`BuyButtonEventError`](#buybuttoneventerror) | The flow encountered an error. Surface to logs and tear down the frame. |
#### `FrameTransaction`
`FrameTransaction` is a discriminated union — the failure variant carries `failureReason`, the non-failure variant always carries `id`. Pass `id` to [`client.getTransaction()`](/platform/sdk-reference/web/get-transaction) to poll for the final status.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | -------------------------------------------------------------------------- |
| `status` | `string` | ✅ | The transaction status. On the failure variant, `"failed"`. |
| `id` | `string` | | Required on the non-failure variant; optional when `status` is `"failed"`. |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). |
#### `BuyButtonEventError`
| Field | Type | Required | Description |
| --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `code` | `string` | ✅ | The error category. Includes `configurationError`, `invalidQuote`, and backend-specific error codes. |
| `message` | `string` | ✅ | Developer-friendly details. Not intended to be rendered in UI. |
## Result
`client.setupBuyButton()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`BuyButtonFrame`](#buybuttonframe) | | Present when `ok` is `true`. |
| `error` | [`SetupBuyButtonError`](#setupbuybuttonerror) | | Present when `ok` is `false`. |
### `BuyButtonFrame`
| Field | Type | Required | Description |
| ---------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `setQuote` | `(signature: string) => void` | ✅ | Updates the quote signature used by the frame. Use this when the current quote expires before the customer taps the button — fetch a new quote and pass its `signature`. |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
### `SetupBuyButtonError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
```ts types.ts theme={null}
type BuyButtonFrame = {
setQuote: (signature: string) => void;
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type BuyButtonEvent =
| { kind: "ready" }
| { kind: "buttonPressed" }
| {
kind: "complete";
payload: { transaction: FrameTransaction };
}
| {
kind: "challenge";
payload: { kind: string; url: string };
}
| { kind: "error"; payload: BuyButtonEventError };
type BuyButtonEventError = {
code: string;
message: string;
};
type SetupBuyButtonError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupChallenge()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-challenge
Render the Challenge frame to resolve verification required by another flow.
Render the Challenge frame into your UI to resolve verification steps required
by another flow (for example, a buy transaction or an identity capture). The
challenge frame is self-driving — after initialization, it sequences through
all required verification steps and emits `complete` when the pipeline
finishes.
Unlike other setup methods, `setupChallenge()` takes a `url` provided by the
upstream flow's `challenge` event, returned in `kyc.challenge` by
`PATCH /platform/v1/customers/{id}/kyc` for Customer API integrations, or
returned as `challenge.url` on a buy quote for [guest checkout limit
upgrades](/platform/guides/guest-checkout#raise-the-limit-with-a-step-up). Pass
it through as-is and do not modify the URL yourself. If the URL doesn't carry a
`channelId` query parameter, the SDK generates one automatically.
For more context, see the [Handle
challenges](/platform/guides/handling-challenges) guide.
```ts Setup challenge focus={6-50} theme={null}
import {
createClient,
type BuyEvent,
type ChallengeEvent,
} from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const buyResult = await client.setupBuy({
quote: quoteResult.value.data.signature,
container: document.querySelector("#buyContainer"),
onEvent: async (event: BuyEvent) => {
if (event.kind !== "challenge") return;
// The url comes from the challenge event — pass it through as-is.
const challengeResult = await client.setupChallenge({
url: event.payload.url,
container: document.querySelector("#challengeContainer"),
onEvent: (event: ChallengeEvent) => {
switch (event.kind) {
case "ready":
// Challenge UI is rendered and visible to the customer
break;
case "complete":
if (event.payload.flow === "buy") {
console.log(event.payload.transaction);
} else if (event.payload.flow === "identity") {
console.log(event.payload.identityId);
}
buyResult.value.dispose();
break;
case "cancelled":
// Customer dismissed the challenge — offer a retry path
buyResult.value.dispose();
break;
case "error":
console.error(event.payload.message);
buyResult.value.dispose();
break;
}
},
});
if (!challengeResult.ok) {
// Handle error
console.error(challengeResult.error.kind, challengeResult.error.message);
return;
}
},
});
```
***
## Parameters
| Property | Type | Required | Description |
| ----------- | --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url` | `string` | ✅ | The URL from the upstream flow's `challenge` event payload, or from an identity verification response. Pass it through unchanged. If the URL has no `channelId` query parameter, the SDK adds one. |
| `container` | `HTMLElement` | ✅ | A DOM element to render the Challenge frame into. |
| `onEvent` | `(event: ChallengeEvent) => void` | | Callback invoked for Challenge flow events. See [`ChallengeEvent`](#challengeevent). |
This method does not require a separate auth token. The client uses stored
credentials from an active connection.
### `ChallengeEvent`
`onEvent` receives events as the challenge flow progresses. Use `event.kind` to
decide how to handle each event. The `complete` and `cancelled` payloads are
discriminated by `payload.flow` so you can branch on the originating flow.
| kind | Payload | When you receive it |
| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `"ready"` | — | The Challenge UI is rendered and visible to the customer. |
| `"complete"` | [`ChallengeCompleteResult`](#challengecompleteresult) | All verification steps resolved. Discriminated by `flow`. |
| `"cancelled"` | [`ChallengeCancellation`](#challengecancellation) | The customer dismissed the challenge. Discriminated by `flow`. Offer a retry path or exit. |
| `"error"` | [`ChallengeEventError`](#challengeeventerror) | The challenge failed with a terminal error. |
The challenge frame is **self-driving**. After acknowledging the initial
handshake, the SDK does not send further messages to the frame. The frame
internally handles all verification types automatically — including CVC
confirmation, 3D Secure, identity verification (KYC), Strong Customer
Authentication (SCA), micro-deposit authorization, and wallet ownership proof.
You never need to distinguish between them.
#### `ChallengeCompleteResult`
The `complete` payload is discriminated by `flow`:
| Field | Type | Required | Description |
| ------------- | --------------------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `flow` | `"buy"` \| `"identity"` \| `"guest_checkout_limit_upgrade"` | ✅ | Identifies which upstream flow the challenge resolved. |
| `transaction` | [`Transaction`](#transaction) | | Present when `flow` is `"buy"`. The created or updated transaction. |
| `identityId` | `string` | | Present when `flow` is `"identity"`. The identity record that was verified. |
| `status` | [`GuestCheckoutLimitUpgradeStatus`](#guestcheckoutlimitupgradestatus) | | Present when `flow` is `"guest_checkout_limit_upgrade"`. The terminal outcome of the limit upgrade. |
##### `GuestCheckoutLimitUpgradeStatus`
| Value | What it means |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `"upgraded"` | The customer's guest limit is raised. Request the quote again to get one with `executable: true`. |
| `"rejected"` | Verification failed. The limit is unchanged, and running the upgrade again does not change it. Offer full verification instead. |
| `"pending"` | Verification is still running. The frame polls to a terminal outcome before it emits `complete`, so you rarely see this value. |
#### `ChallengeCancellation`
The `cancelled` payload is discriminated by `flow`:
| Field | Type | Required | Description |
| ---------------- | ----------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `flow` | `"buy"` \| `"identity"` \| `"guest_checkout_limit_upgrade"` | ✅ | Identifies which upstream flow was cancelled. |
| `transactionId` | `string` | | Present when `flow` is `"buy"`. The transaction associated with the cancelled challenge. |
| `challengeToken` | `string` | | Present when `flow` is `"buy"`. The challenge token from the original `challenge` event. |
Cancelling a `guest_checkout_limit_upgrade` challenge submits nothing and
leaves the limit unchanged. Offer a retry path.
#### `Transaction`
This is the transaction object returned when the buy challenge completes. It uses the same [`FrameTransaction`](/platform/sdk-reference/web/setup-buy#frametransaction) shape as `setupBuy()`.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | -------------------------------------------------------------------------- |
| `status` | `string` | ✅ | The transaction status. On the failure variant, `"failed"`. |
| `id` | `string` | | Required on the non-failure variant; optional when `status` is `"failed"`. |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). |
#### `ChallengeEventError`
| Field | Type | Required | Description |
| --------- | -------- | -------- | ----------------------------------------------------------------------------------------------- |
| `code` | `string` | ✅ | A machine-readable error category propagated from the challenge frame. Surface to logs, not UI. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Result
`client.setupChallenge()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`ChallengeFrame`](#challengeframe) | | Present when `ok` is `true`. |
| `error` | [`SetupChallengeError`](#setupchallengeerror) | | Present when `ok` is `false`. |
### `ChallengeFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
Unlike `setupBuy()`, `setupApplePay()`, and `setupGooglePay()`, the
`ChallengeFrame` does not expose a `setQuote()` method. The challenge frame
runs to completion on its own.
### `SetupChallengeError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
```ts types.ts theme={null}
type ChallengeFrame = {
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type GuestCheckoutLimitUpgradeStatus = "pending" | "upgraded" | "rejected";
type ChallengeCompleteResult =
| { flow: "buy"; transaction: FrameTransaction }
| { flow: "identity"; identityId: string }
| {
flow: "guest_checkout_limit_upgrade";
status: GuestCheckoutLimitUpgradeStatus;
};
type ChallengeCancellation =
| { flow: "buy"; transactionId?: string; challengeToken?: string }
| { flow: "identity" }
| { flow: "guest_checkout_limit_upgrade" };
type ChallengeEvent =
| { kind: "ready" }
| { kind: "complete"; payload: ChallengeCompleteResult }
| { kind: "cancelled"; payload: ChallengeCancellation }
| { kind: "error"; payload: ChallengeEventError };
type ChallengeEventError = {
code: string;
message: string;
};
type SetupChallengeError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupGooglePay()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-google-pay
Render the Google Pay frame and start a transaction.
Render the Google Pay frame into your UI and initiate a transaction with a quote signature. The quote must have `executable: true`.
To let new customers buy with Google Pay before they have a MoonPay account, see
[Guest checkout](/platform/guides/guest-checkout). Guest checkout uses the same
`setupGooglePay` call. Second-factor and KYC step-up arrive as a `"challenge"`
event.
```ts Setup Google Pay focus={5-35} theme={null}
import { createClient, type GooglePayEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const googlePayResult = await client.setupGooglePay({
quote: quoteResult.value.data.signature,
container: document.querySelector("#googlePayContainer"),
onEvent: (event: GooglePayEvent) => {
switch (event.kind) {
case "ready":
break;
case "buttonPressed":
// Customer tapped the button — show a loading state or fire analytics.
break;
case "complete":
console.log(event.payload.transaction);
break;
case "quoteExpired":
// Fetch a new quote, then update the frame:
// event.payload.setQuote(newQuote.signature);
break;
case "challenge":
// Render the challenge frame at the provided URL
// See: /platform/guides/handling-challenges
console.log(event.payload.url);
break;
case "error":
console.error(event.payload.kind, event.payload.message);
break;
case "unsupported":
// Google Pay isn't available in this environment — fall back to another method.
break;
}
},
});
if (!googlePayResult.ok) {
// Handle error
console.error(googlePayResult.error.kind, googlePayResult.error.message);
return;
}
const googlePay = googlePayResult.value;
```
***
## Parameters
| Property | Type | Required | Description |
| ----------------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/web/get-quote). |
| `container` | `HTMLElement` | ✅ | A DOM element to render the Google Pay frame into. |
| `externalTransactionId` | `string` | | Partner-assigned identifier for this transaction attempt. Useful for reconciliation. |
| `onEvent` | `(event: GooglePayEvent) => void` | | Callback invoked for Google Pay flow events. See [`GooglePayEvent`](#googlepayevent). |
This method does not require a separate auth token. The client uses stored credentials from an active connection, or from the connection check on [guest checkout](/platform/guides/guest-checkout).
### `GooglePayEvent`
`onEvent` receives events as the Google Pay flow progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ----------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The Google Pay UI is rendered and ready to be shown. |
| `"buttonPressed"` | — | The customer tapped the Google Pay button, before Android presents the Google Pay sheet. An intent-to-buy signal with no payload — it still fires if the customer then cancels the sheet. Not a purchase outcome; listen for `"complete"` for the result. |
| `"complete"` | `{ transaction:` [`FrameTransaction`](#frametransaction) `}` | The Google Pay flow finished. Inspect the `FrameTransaction` for the outcome. |
| `"challenge"` | `{ kind: "frame"; url: string }` | Verification required. Render the challenge frame at the provided URL using [`setupChallenge()`](/platform/sdk-reference/web/setup-challenge). |
| `"quoteExpired"` | `{ setQuote: (signature: string) => void }` | The quote signature expired. Fetch a new quote and call `payload.setQuote(...)`. |
| `"error"` | [`GooglePayEventError`](#googlepayeventerror) | The flow encountered an error. |
| `"unsupported"` | — | Google Pay isn't available in the user's current environment. |
#### `FrameTransaction`
The transaction object returned on `"complete"`. `FrameTransaction` is a discriminated union — the failure variant carries `failureReason`, the non-failure variant always carries `id`.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `status` | `string` | ✅ | The transaction status. On the failure variant, `"failed"`. |
| `id` | `string` | | Required on the non-failure variant; optional when `status` is `"failed"` (a transaction may not exist yet on early failure). |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). |
#### `GooglePayEventError`
| Field | Type | Required | Description |
| --------- | ---------------------------------------------------------------------------------- | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"invalidQuote"` \| `"quoteExpired"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
Google Pay unavailability is signalled separately through the `"unsupported"` event, not as an error.
## Result
`client.setupGooglePay()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`GooglePayFrame`](#googlepayframe) | | Present when `ok` is `true`. |
| `error` | [`SetupGooglePayError`](#setupgooglepayerror) | | Present when `ok` is `false`. |
### `GooglePayFrame`
| Field | Type | Required | Description |
| ---------- | ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `setQuote` | `(signature: string) => void` | ✅ | Updates the quote signature used by the frame. Use this in response to `quoteExpired`. |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
### `SetupGooglePayError`
| Field | Type | Required | Description |
| --------- | ------------------------------------------ | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
Setup errors cover frame initialization only (for example, a handshake
timeout). Quote problems and Google Pay availability are reported through
`onEvent` after setup succeeds — as the `"error"`, `"quoteExpired"`, and
`"unsupported"` events respectively.
```ts types.ts theme={null}
type GooglePayFrame = {
setQuote: (signature: string) => void;
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type GooglePayEvent =
| { kind: "ready" }
| { kind: "buttonPressed" }
| {
kind: "complete";
payload: { transaction: FrameTransaction };
}
| {
kind: "challenge";
payload: {
kind: "frame";
/** Fully-formed URL to pass directly to setupChallenge(). */
url: string;
};
}
| {
kind: "quoteExpired";
payload: { setQuote: (signature: string) => void };
}
| { kind: "error"; payload: GooglePayEventError }
| { kind: "unsupported" };
type GooglePayEventError = {
kind: "configurationError" | "invalidQuote" | "quoteExpired" | "genericError";
message: string;
};
type SetupGooglePayError = {
kind: "configurationError" | "genericError";
message: string;
};
```
# client.setupWidget()
Source: https://dev.moonpay.com/platform/sdk-reference/web/setup-widget
Render the MoonPay buy widget and start a transaction.
Render the MoonPay buy widget into your UI and initiate a transaction with a quote signature. The quote must have `executable: true` — a non-executable quote will not render. The widget handles the full purchase flow — including payment collection, verification, and transaction confirmation — inside an iframe.
```ts Setup widget focus={6-30} theme={null}
import { createClient, type WidgetEvent } from "@moonpay/platform-sdk-web";
const client = createClient({ sessionToken: "c3N0XzAwMQ==" });
const widgetResult = await client.setupWidget({
quote: quoteResult.value.data.signature,
container: document.querySelector("#widgetContainer"),
onEvent: (event: WidgetEvent) => {
switch (event.kind) {
case "ready":
break;
case "transactionCreated":
console.log(event.payload.transaction);
break;
case "complete":
console.log(event.payload.transaction);
break;
case "close":
// The customer closed the widget
break;
case "error":
console.error(event.payload.code, event.payload.message);
break;
}
},
});
if (!widgetResult.ok) {
// Handle error
console.error(widgetResult.error.kind, widgetResult.error.message);
return;
}
const widget = widgetResult.value;
```
***
## Parameters
| Property | Type | Required | Description |
| ----------------------- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------- |
| `quote` | `string` | ✅ | The quote `signature` returned from [`getQuote`](/platform/sdk-reference/web/get-quote). |
| `container` | `HTMLElement` | ✅ | A DOM element to render the widget frame into. |
| `externalTransactionId` | `string` | | Partner-assigned identifier for this transaction attempt. Useful for reconciliation. |
| `onEvent` | `(event: WidgetEvent) => void` | | Callback invoked for widget flow events. See [`WidgetEvent`](#widgetevent). |
This method does not require a separate auth token. The client uses stored credentials from an active connection.
### `WidgetEvent`
`onEvent` receives events as the widget flow progresses. Use `event.kind` to decide how to handle each event.
| kind | Payload | When you receive it |
| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| `"ready"` | — | The widget is loaded and ready to be shown. |
| `"transactionCreated"` | `{ transaction: { id: string; status: string } }` | A transaction has been created. The customer may still need to complete additional steps (for example, 3-D Secure). |
| `"complete"` | `{ transaction:` [`FrameTransaction`](#frametransaction) `}` | The widget flow finished. `FrameTransaction` is a discriminated union — handle the `"failed"` variant separately. |
| `"close"` | — | The customer closed the widget. |
| `"error"` | [`WidgetEventError`](#widgeteventerror) | The flow encountered an error. |
#### `FrameTransaction`
The transaction reported by the widget on `"complete"`. The shape depends on the outcome.
| Field | Type | Required | Description |
| --------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------- |
| `id` | `string` | | Present on the success variant. Required when `status !== "failed"`; optional when `status` is `"failed"`. |
| `status` | `string` | ✅ | The transaction status. On the failure variant this is `"failed"`. |
| `failureReason` | `string` | | Present only on the failure variant (`status === "failed"`). |
To track the final status after `"complete"`, pass `transaction.id` to [`client.getTransaction()`](/platform/sdk-reference/web/get-transaction).
#### `WidgetEventError`
| Field | Type | Required | Description |
| --------- | ----------------------------------------------------- | -------- | --------------------------- |
| `code` | `"configurationError"` \| `"apiError"` \| `"generic"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
## Result
`client.setupWidget()` returns a `Result`.
### Result envelope
`Result`
| Field | Type | Required | Description |
| ------- | --------------------------------------- | -------- | -------------------------------- |
| `ok` | `boolean` | ✅ | Whether the operation succeeded. |
| `value` | [`WidgetFrame`](#widgetframe) | | Present when `ok` is `true`. |
| `error` | [`SetupWidgetError`](#setupwidgeterror) | | Present when `ok` is `false`. |
### `WidgetFrame`
| Field | Type | Required | Description |
| --------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `dispose` | `() => void` | ✅ | Unmounts the frame. After you call this, no further events are dispatched to your `onEvent` callback. |
### `SetupWidgetError`
| Field | Type | Required | Description |
| --------- | ---------------------------------------------------------- | -------- | --------------------------- |
| `kind` | `"configurationError"` \| `"apiError"` \| `"genericError"` | ✅ | The error category. |
| `message` | `string` | ✅ | Developer-friendly details. |
```ts types.ts theme={null}
type WidgetFrame = {
dispose: () => void;
};
type FrameTransaction =
| { id: string; status: string }
| { id?: string; status: "failed"; failureReason: string };
type WidgetEvent =
| { kind: "ready" }
| {
kind: "transactionCreated";
payload: { transaction: { id: string; status: string } };
}
| {
kind: "complete";
payload: { transaction: FrameTransaction };
}
| { kind: "close" }
| { kind: "error"; payload: WidgetEventError };
type WidgetEventError = {
code: "configurationError" | "apiError" | "generic";
message: string;
};
type SetupWidgetError = {
kind: "configurationError" | "apiError" | "genericError";
message: string;
};
```
# FAQ
Source: https://dev.moonpay.com/widget/faqs
Common questions about integrating, customizing, and going live with the MoonPay on-ramp and off-ramp widget.
## Integration
### Where can you integrate the widget?
You can integrate it wherever you'd like. We surface an SDK, and you can pass a variety of query parameters to the widget that is spun up, such as the token you want users to buy and the destination wallet address. See the full list of [on-ramp parameters](/widget/on-ramp/customization/parameters) and [off-ramp parameters](/widget/off-ramp/customization/parameters).
### What does the customer journey look like?
This depends on the query parameters you use.
### What do the different team permissions in the dashboard do?
The Partner Dashboard uses role-based access control to manage team member permissions. Each team member is assigned one of the following roles:
| Role | Access Level | Permissions |
| ------------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Administrator | Full access | Complete access to all dashboard features including dashboard analytics, transactions (with CSV export), team management, developer settings, and account settings. |
| Developer | Limited access | Can view dashboard analytics, transactions, and team members. Can manage API keys and developer settings. Cannot access account settings. |
| Analyst | Read & export | Can view dashboard analytics, transactions (with CSV export), and team members. Cannot access developer settings or account settings. |
| Support Specialist | Transactions only | Limited to viewing and managing transactions. Cannot access dashboard analytics, team management, developer settings, or account settings. |
### Does the end user need to provide a wallet, or does MoonPay provide one?
Both are possible. You can pass one, we can let the user create one (BTC and ETH), or you can let users input their own wallet address.
### Are there recommended best practices for integrating?
Yes. Best practices for each product are outlined on their respective documentation pages.
### Can you customize the automated emails users receive?
We can add a partner logo to the automated email and change the partner name (`Partner_Name` via MoonPay) in the email subject on request. Reach out to your MoonPay representative.
## Errors and troubleshooting
### The widget returned an error. How do I escalate it?
Include a screenshot and steps to reproduce the error when you reach out to your MoonPay representative.
### How do I change my API keys?
Go to the [Developers > API Keys](https://dashboard.moonpay.com/developers/) page of your MoonPay dashboard and hit the roll button next to the API key you wish to replace. You will be given the option to immediately delete the old key or set a 12-hour rollover before the old API key expires.
### The iframe shows an error instead of the widget
If you see a CSP error that says `buy.moonpay.com refused to connect` (or `sell.moonpay.com refused to connect` for off-ramp), you'll need to allowlist your production domain(s). We require this when you embed the MoonPay widget in an iframe or use the overlay, drawer, or embedded options, to prevent unauthorized third-parties from embedding the MoonPay widget.
To resolve the issue, add your production domain(s) to the [Settings page](https://dashboard.moonpaycloud.com/settings/) of your MoonPay dashboard.
### Region-based widget error
If you see a widget error that geo-blocks certain users, confirm that the user is in a region supported by MoonPay. See the [non-supported countries, states, and territories for on-ramp](https://support.moonpay.com/en/articles/380968-moonpay-s-unsupported-countries) and the [cryptocurrencies available to sell for off-ramp](/widget/supported-currencies).
### The widget shows an "Unverified Connection" error for iOS users
iCloud Private Relay routes Safari traffic through an Apple relay, so for widgets opened in `SFSafariViewController` MoonPay observes a relay IP address instead of the one signed into the URL, and [IP matching](/widget/on-ramp/customization/ip-matching) fails. Detect Private Relay and omit `allowedIpAddress` for those sessions. See [iCloud Private Relay](/widget/on-ramp/customization/ip-matching#icloud-private-relay).
### Multiple accounts in production
In our production environment, we only allow one account per user. Creating multiple accounts in production may cause your account to be flagged or blocked.
### Test credit cards and KYC info in production
Only genuine card and KYC information may be used in our production environment. Do **not** use test credit card or test KYC information in production, as this will result in your account being blocked.
### Using platforms like Flutter
MoonPay does not offer an SDK for Flutter. You'll have to manually embed our widget in a WebView. Consider the following tips:
* You can create your own HTML that uses our [Web SDK](/widget/on-ramp/integration-methods/sdks/web) to embed our Widget. This will allow you to receive our Widget's events, if you wish. You'll then need to send them out to your Flutter app layer.
* If you're experiencing camera issues in Flutter's WebView, consider using the Flutter plugin compatible with our Web SDK, available at [https://pub.dev/packages/flutter\_inappwebview](https://pub.dev/packages/flutter_inappwebview).
* You can find further guidance on configuring Flutter camera permissions and other related documentation at [https://inappwebview.dev/docs/5.x.x/web-rtc/](https://inappwebview.dev/docs/5.x.x/web-rtc/).
* Custom Origin via `baseUrl` Property: For specific requirements, you can set a custom origin using the `baseUrl` property. This serves as the base URL for any relative paths in the HTML and sets the document's origin. Example:
```dart theme={null}
InAppWebView(
initialData: InAppWebViewInitialData(
data: htmlContent,
baseUrl: Uri.parse("https://your.custom.origin"),
),
// ... other configurations ...
)
```
This custom origin approach can be advantageous for scenarios involving scripts that validate the document's origin or for CORS policies. Use `https://app_name`, `capacitor://` or equivalent as your custom origin for the `baseUrl` property. From here, add the domain to your allowlist in the settings page of your [MoonPay dashboard](https://dashboard.moonpay.com/settings/).
* Scrolling Issues within the MoonPay widget: When using these overflowing containers in Flutter with a WebViewWidget, you have to specify gesture recognizers: Example:
```dart theme={null}
gestureRecognizers: >{
Factory(
() => EagerGestureRecognizer(),
),
},
```
* Avoid using `enableDrag: true` when embedding a WebView inside a `showModalBottomSheet()`, as it may interfere with touch interactions. Instead, provide a dedicated close button for a better user experience.
## KYC and app requirements
When not using a MoonPay SDK, make sure that the following requirements are
met so that users can successfully complete KYC (Know Your Customer) steps.
These requirements make sure that customers can complete each KYC step, including uploading documents and doing a selfie check. Failing to follow these steps will cause new customers to drop off, as they won't be able to finish KYC or make a purchase.
### General app requirements for all implementations
All partner apps should ensure the following:
* `Feature-Policy` header for your webpage / frame or any other container has no restrictions for initializing camera like value `camera 'none'`.
* `Permissions-Policy` header doesn't restrict access to a camera and microphone (for some cases) and if allow is set check for `"camera; microphone"` values.
* When using [iOS WKWebview](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/1614793-allowsinlinemediaplayback) you may need to set `allowsInlineMediaPlayback` to `true` in the `WKWebViewConfiguration` used in your app. This adjustment ensures that media content, like a camera feed, can be displayed properly within the web view, rather than forcing full-screen playback.
* Your website is being run on a secure `https` connection.
### WebView requirements
Partner apps that use `WebView` should ensure the following:
* The web view is able to access device local storage and initialize camera (for older iOS versions, the camera can be accessed only from Safari browser or WebView with `SFSafariViewController`)
* HTML5 video playback is allowed (`