Implementing Fiat-to-Crypto (On-Ramp) in a Mobile App
On-ramp is the entry point for fiat money into the crypto ecosystem. Users pay with a card or bank transfer and receive cryptocurrency to their wallet. The mobile app doesn't process payments directly — that's the job of a licensed provider. The developer's task is to properly integrate the widget or API.
Two Approaches: Widget vs API
Widget Approach — the provider supplies a WebView or native SDK. Minimal integration: pass the wallet address, currency, and optionally the amount. The provider handles KYC and payment themselves.
API Approach — full control over UI, but requires a partnership agreement and compliance review from the provider. Not available to everyone.
For most wallets, a widget with deeplink or WebView is sufficient. MoonPay, Transak, and Mercuryo offer both options.
Provider Selection: Key Parameters
| Provider | Fee | Payment Methods | KYC Threshold | Coverage |
|---|---|---|---|---|
| MoonPay | 1–4.5% | Card, bank, Apple Pay | $150/month without KYC | 160+ countries |
| Transak | 0.5–1% | Card, bank | Varies by country | 130+ countries |
| Mercuryo | 1.5–3.9% | Card | $100/month without KYC | 100+ countries |
| Ramp Network | 0.49–2.9% | Card, bank | $250 without KYC | 150+ countries |
Verify coverage and available payment methods for the target country via the provider's API, not documentation — data changes frequently.
Integration via WebView
Basic flow: form a URL with parameters, open it in SFSafariViewController (iOS) or CustomTabsIntent (Android). Don't use WKWebView / WebView for payment forms — they lack full cookie access and may conflict with 3DS.
// iOS — opening MoonPay via SFSafariViewController
import SafariServices
let baseURL = "https://buy.moonpay.com"
var components = URLComponents(string: baseURL)!
components.queryItems = [
.init(name: "apiKey", value: moonPayPublicKey),
.init(name: "walletAddress", value: userWalletAddress),
.init(name: "currencyCode", value: "eth"),
.init(name: "colorCode", value: "%23FF6600"),
.init(name: "language", value: "en")
]
// Sign URL on backend with HMAC-SHA256
let safariVC = SFSafariViewController(url: signedURL)
present(safariVC, animated: true)
The URL must be signed with HMAC-SHA256 on your backend using MoonPay's secret key — this is a mandatory security requirement, not optional.
Deeplink Callback and Status Tracking
MoonPay supports redirectURL — when a purchase is complete, the provider redirects to the specified URL. In a mobile app, this is a custom URL scheme or Universal Link.
// Android — handling deeplink after on-ramp completion
// AndroidManifest.xml: intent-filter with data scheme="myapp" host="onramp-callback"
override fun onNewIntent(intent: Intent) {
val uri = intent.data ?: return
if (uri.host == "onramp-callback") {
val transactionId = uri.getQueryParameter("transactionId")
val status = uri.getQueryParameter("status") // completed, failed, pending
handleOnRampResult(transactionId, status)
}
}
A backend webhook is the more reliable way to get the final status. Callback via deeplink may not work if the user closes the app during purchase.
On-Ramp Provider Aggregator
To offer the best rate from multiple providers, use an aggregator: Onramper (onramper.com/api), Transak One, or Li.Fi's built-in widget. Users see rate comparison and choose the provider. This improves conversion.
Timeline: 5 days to integrate one provider via widget with deeplink callback. Aggregating multiple providers with UI selection — 8–10 days.







