MoonPay Integration for Crypto Purchase in a Mobile App
MoonPay is one of the most widespread on-ramp providers. It supports 160+ countries, Visa/Mastercard/Amex, Apple Pay, Google Pay, and bank transfers. Integration via widget takes a day, but properly signed URLs and callback handling are separate challenges.
Obtaining API Keys and URL Signing
At dashboard.moonpay.com, create an account and get a publishable key (public, for the app) and secret key (server-only). Without the secret key, you can't sign the URL; without the signature, MoonPay blocks the widget.
Signature is HMAC-SHA256 of the URL query string:
// Forming a signed URL — executed on backend, not in app
// Server-side (Node.js example):
const crypto = require('crypto');
const queryString = new URL(widgetUrl).search; // "?apiKey=...&walletAddress=..."
const signature = crypto
.createHmac('sha256', process.env.MOONPAY_SECRET_KEY)
.update(queryString)
.digest('base64');
const signedUrl = `${widgetUrl}&signature=${encodeURIComponent(signature)}`;
The app requests the signed URL from its own backend and never stores the secret key locally.
Opening the Widget on iOS and Android
// iOS — SFSafariViewController (recommended by MoonPay)
import SafariServices
let vc = SFSafariViewController(url: URL(string: signedUrl)!)
vc.preferredBarTintColor = UIColor(named: "AppBackground")
vc.preferredControlTintColor = UIColor(named: "AppTint")
present(vc, animated: true)
// Android — Chrome Custom Tabs
val customTabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(context, R.color.app_background))
.setShowTitle(true)
.build()
customTabsIntent.launchUrl(context, Uri.parse(signedUrl))
Don't open the widget in a regular WebView — MoonPay explicitly discourages this for 3DS security reasons.
Customization Parameters
Key query parameters for the widget:
-
walletAddress— address to receive crypto -
currencyCode—eth,btc,sol,usdc_ethereum, etc. -
baseCurrencyAmount— pre-filled amount in fiat -
baseCurrencyCode—usd,eur,gbp -
colorCode— accent color (URL-encoded hex,%23FF6600) -
language—en,ru,de, etc. -
email— pre-fill email for KYC (if known) -
redirectURL— URL to return to app after purchase
Handling Result via Deeplink
After a successful purchase, MoonPay redirects to redirectURL. Register a custom URL scheme in your app:
// Android: AndroidManifest.xml
// <intent-filter>
// <data android:scheme="myapp" android:host="moonpay-success"/>
// </intent-filter>
override fun onNewIntent(intent: Intent) {
val uri = intent.data ?: return
if (uri.host == "moonpay-success") {
val txId = uri.getQueryParameter("transactionId")
// Show success screen, update balance after 30 sec
}
}
Additionally, use a webhook on the backend for reliable status retrieval (transaction_completed, transaction_failed). MoonPay sends events to the webhookUrl specified in dashboard settings.
Timeline: 2–3 days for backend URL signing, integrating the widget via SFSafariViewController/CustomTabs, deeplink callback handling, and balance updates after successful purchase.







