Mercado Pago via Widget
Integrate Mercado Pago with the DEUNA Payment Widget, test the customer flow, and verify the final payment status.
Sur cette page
Prerequisites#
Before implementing Mercado Pago Wallet payments, ensure you have:
- Enabled Mercado Pago in the DEUNA Admin Panel.
- A generated order token.
- Integrated the DEUNA SDK in your project.
- Reviewed the Payment Widget documentation for your platform.
Payment Method configuration#
Display the Payment Widget by passing the Mercado Pago configuration in the paymentMethods parameter.
[
{
"paymentMethod": "wallet",
"processors": [ "mercadopago_wallet" ]
}
]Payment Widget - Web#
DeunaSDK.initPaymentWidget({
orderToken: 'YOUR_ORDER_TOKEN',
paymentMethods: [
{
paymentMethod: 'wallet',
processors: ['mercadopago_wallet'],
},
],
callbacks: { ... },
});Payment Widget - iOS#
deunaSDK.initPaymentWidget(
orderToken: "<DEUNA_ORDER_TOKEN>",
callbacks: PaymentWidgetCallbacks(
onSuccess: { order in
// Close the DEUNA widget
self.deunaSDK.close {
// Handle successful payment:
// - Navigate to confirmation screen
// - Show success message
// - Update order status
}
},
onError: { error in
if error.type == .paymentError {
// Handle payment errors:
// - Show error message
// - Allow retry
// - Log analytics
}
}
),
paymentMethods: [
[
"paymentMethod": "wallet",
"processors": ["mercadopago_wallet"]
]
]
)import DeunaSDK
import SwiftUI
struct PaymentView: View {
let deunaSDK: DeunaSDK
var body: some View {
VStack {
DeunaWidget(
deunaSDK: deunaSDK,
configuration: PaymentWidgetConfiguration(
orderToken: "YOUR_ORDER_TOKEN",
callbacks: PaymentWidgetCallbacks(
onSuccess: { order in
deunaSDK.dispose {
// Handle post-payment flow
}
},
onError: { error in
if error.type == .paymentError {
// Implement error handling
}
}
),
paymentMethods: [
[
"paymentMethod": "wallet",
"processors": ["mercadopago_wallet"]
]
]
)
)
}
}
}Overriding callback URLs per APM (orders tokenized without callback_urls)
Some merchants tokenize the order through a third party (e.g. Sabre) that doesn't pass callback_urls at order creation. Without them, Mercado Pago Wallet has no deep link to return the shopper to, and the default DEUNA redirect bridge is used instead — which won't resume your app via a custom scheme.
To fix this from the client, pass per-APM callback URLs in behavior.paymentMethods.apms, keyed by processor name (order.payment.data.processor):
deunaSDK.initPaymentWidget(
orderToken: "<DEUNA_ORDER_TOKEN>", // tokenized without callback_urls
callbacks: PaymentWidgetCallbacks(
onSuccess: { order in
self.deunaSDK.close { }
},
onError: { error in }
),
paymentMethods: [
[
"paymentMethod": "wallet",
"processors": ["mercadopago_wallet"]
]
],
behavior: [
"paymentMethods": [
"apms": [
"mercadopago_wallet": [
"callbackUrls": [
"onSuccess": "yourapp://payment/success",
"onCanceled": "yourapp://payment/canceled",
"onPending": "yourapp://payment/pending"
]
]
]
]
]
)iOS - Deep links
When the Mercado Pago link opens inside a SFSafariViewController, the system will hand the flow off to the native Mercado Pago app if it’s installed. To return to your app afterwards, add the same deep link scheme you configured in callback_urls to your Info.plist (CFBundleURLSchemes).
<dict>
…
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>your_identifier_here</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>
…
</dict>Payment Widget - Android#
For security reasons, Mercado Pago requires that their payment URLs must be loaded in a Custom Tab instead of a WebView.
It's crucial to understand that while a Custom Tab is open, you should avoid updating your UI or performing actions like navigation or showing dialogs. These operations could cause unexpected behavior, including app crashes.
Therefore, you should only execute such actions after the user has closed the Custom Tab. To detect when the Custom Tab closes, implement the following code in your MainActivity:
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**
* Register for activity result callbacks,
* needed to wait until the custom chrome tab is closed
*/
ExternalUrlHelper.registerForActivityResult(this)
}
}Now show the Payment Widget
deunaSDK.initPaymentWidget(
orderToken = "<DEUNA_ORDER_TOKEN>",
callbacks = PaymentWidgetCallbacks().apply {
onSuccess = { order ->
// Close the DEUNA widget
this.deunaSDK.close {
// Handle successful payment:
// - Navigate to confirmation screen
// - Show success message
// - Update order status
}
}
onError = { error ->
if (error.type == PaymentErrorType.PAYMENT_ERROR) {
// Handle payment errors:
// - Show error message
// - Allow retry
// - Log analytics
}
}
},
paymentMethods = listOf(
mapOf(
"paymentMethod" to "wallet",
"processors" to listOf("mercadopago_wallet")
)
)
)@Composable
fun YourScreen(
deunaSDK: DeunaSDK,
orderToken: String,
userToken: String,
) {
// Maintains the Deuna WebView instance across recompositions
val deunaWidget = remember { mutableStateOf<DeunaWidget?>(null) }
Column(modifier = Modifier.padding(16.dp)) {
// Container for the embedded payment widget
// MUST specify dimensions (e.g., .height(400.dp) or .weight(1f) in parent Column)
Box(
modifier = yourModifier
) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
DeunaWidget(context).apply {
this.widgetConfiguration = PaymentWidgetConfiguration(
sdkInstance = deunaSDK,
orderToken = orderToken,
userToken = userToken,
paymentMethods = listOf(
mapOf(
"paymentMethod" to "wallet",
"processors" to listOf("mercadopago_wallet")
)
),
callbacks = PaymentWidgetCallbacks().apply {
onSuccess = { data ->
deunaWidget.value?.waitUntilExternalUrlIsClosed {
// Handle post-payment flow
}
}
onError = { error ->
// Handle error
}
},
)
this.build() // Render the DEUNA widget
// Store reference for later submission
deunaWidget.value = this
}
}
)
}
Spacer(modifier = Modifier.height(16.dp))
}
// Critical: Clean up DeunaWidget resources when composable leaves composition
DisposableEffect(Unit) {
onDispose {
deunaWidget.value?.destroy()
Log.d("DeunaWidget", "WebView resources cleaned up")
}
}
}Overriding callback URLs per APM (orders tokenized without callback_urls)
Some merchants tokenize the order through a third party (e.g. Sabre) that doesn't pass callback_urls at order creation. Without them, Mercado Pago Wallet has no deep link to return the shopper to, and the default DEUNA redirect bridge is used instead — which won't resume your app via a custom scheme.
To fix this from the client, pass per-APM callback URLs in behavior.paymentMethods.apms, keyed by processor name (order.payment.data.processor):
deunaSDK.initPaymentWidget(
context = this,
orderToken = "<DEUNA_ORDER_TOKEN>", // tokenized without callback_urls
callbacks = PaymentWidgetCallbacks().apply {
onSuccess = { order -> }
onError = { error -> }
},
paymentMethods = listOf(
mapOf(
"paymentMethod" to "wallet",
"processors" to listOf("mercadopago_wallet")
)
),
behavior = mapOf(
"paymentMethods" to mapOf(
"apms" to mapOf(
"mercadopago_wallet" to mapOf(
"callbackUrls" to mapOf(
"onSuccess" to "yourapp://payment/success",
"onCanceled" to "yourapp://payment/canceled",
"onPending" to "yourapp://payment/pending"
)
)
)
)
)
)Android - Deep Links
When the Mercado Pago link launches in a Custom Tab, Android may open the native Mercado Pago app if it's installed. To return the shopper to your app, register an intent filter for your deep link scheme in AndroidManifest.xml on the activity that handles the callback. Keeping android:launchMode="singleTask" is important so the OS reuses your existing task instead of killing the process when Custom Tabs launches the Mercado Pago link.
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="yourapp" />
</intent-filter>
</activity>Don't declare android:host here — a scheme-only filter matches any host and path under yourapp://, so one filter covers yourapp://payment/success, yourapp://payment/pending, yourapp://payment/canceled, or any other value you ever set in callback_urls, with no manifest change needed later.
Payment Widget - React Native#
import React from 'react';
import { View, Button } from 'react-native';
import { DeunaSDK, DeunaWidget } from '@deuna/react-native-sdk';
// Initialize SDK
const deunaSDK = DeunaSDK.initialize({
publicApiKey: 'YOUR_PUBLIC_API_KEY',
environment: 'sandbox', // or "production"
});
const PaymentScreen = () => {
const launchPayment = () => {
deunaSDK.initPaymentWidget({
orderToken: 'YOUR_ORDER_TOKEN',
paymentMethods: [
{
paymentMethod: 'wallet',
processors: ['mercadopago_wallet'],
},
],
callbacks: {
onSuccess: async (order) => {
await deunaSDK.close();
// Handle successful payment
console.log('Payment successful:', order);
},
onError: async (error) => {
// Handle payment errors
console.error('Payment error:', error);
if (...) {
// Always await the close operation
await deunaSDK.close();
// Handle UI updates
console.log('Payment widget closed due to error.');
}
},
},
});
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<DeunaWidget instance={deunaSDK} />
<Button title="Pay with Mercado Pago" onPress={launchPayment} />
</View>
);
};Overriding callback URLs per APM (orders tokenized without callback_urls)
Some merchants tokenize the order through a third party (e.g. Sabre) that doesn't pass callback_urls at order creation. Without them, Mercado Pago Wallet has no deep link to return the shopper to, and the default DEUNA redirect bridge is used instead — which won't resume your app via a custom scheme.
To fix this from the client, pass per-APM callback URLs in behavior.paymentMethods.apms, keyed by processor name (order.payment.data.processor):
await deunaSDK.initPaymentWidget({
orderToken: 'YOUR_ORDER_TOKEN', // tokenized without callback_urls
paymentMethods: [
{
paymentMethod: 'wallet',
processors: ['mercadopago_wallet'],
},
],
behavior: {
paymentMethods: {
apms: {
mercadopago_wallet: {
callbackUrls: {
onSuccess: 'yourapp://payment/success',
onCanceled: 'yourapp://payment/canceled',
onPending: 'yourapp://payment/pending',
},
},
},
},
},
callbacks: {
onSuccess: async (order) => {
await deunaSDK.close();
},
onError: async (error) => {
await deunaSDK.close();
},
},
});Only affects the APM(s) you configure. Every other processor keeps using the order's own callback_urls or the default DEUNA redirect bridge, unchanged.
Per-field fallback. Any of onSuccess, onCanceled, or onPending left out falls back to the default DEUNA redirect bridge URL — same behavior as if apms wasn't configured at all for that field.
onFailed is not available here. Mercado Pago Wallet does not support a custom on_failed callback — that case always uses the default DEUNA redirect bridge, regardless of this configuration.
You still need the deep link configured natively (Info.plist on iOS, AndroidManifest.xml/app.json intent filters on Android) as shown in the sections below — this behavior config only controls which URLs get sent to Mercado Pago, not how your app receives them.
React Native CLI – Deep Links and Redirect Handling (Generic)
Overview
When Mercado Pago opens in a native browser surface (SafariViewController on iOS or Custom Tabs on Android), you must configure deep links so the user returns to your app after the payment outcome. Use the same deep link URLs you send in order.callback_urls.
- Configure Deep Links (iOS) Register the scheme in Info.plist so iOS can route the callback to your app.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.yourcompany.yourapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>-
Configure Deep Links (Android) Add a single VIEW intent filter for your scheme. Keep
android:launchMode="singleTask"so the OS reuses the existing task when the deep link arrives.AndroidManifest.xmlXML<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTask"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="yourapp" /> </intent-filter> </activity>Don't add
android:hosthere — a scheme-only filter matches any host/path underyourapp://, so it covers every outcome incallback_urlswithout further manifest changes. -
Handle Deep Links in React Native Listen for deep-link events and close the in-app browser when the callback arrives.
JavaScriptimport { Linking } from 'react-native'; import InAppBrowser from 'react-native-inappbrowser-reborn'; Linking.addEventListener('url', ({ url }) => { if (url?.startsWith('yourapp://')) { InAppBrowser.close(); // iOS: closes SFSafariViewController // Resume/finish the checkout flow here. } });Match callback_urls to Your Deep Links When creating the order, ensure
callback_urlsmatch the same scheme/host configured above.JSON"callback_urls": { "on_success": "yourapp://payment/success", "on_pending": "yourapp://payment/pending", "on_canceled": "yourapp://payment/canceled" }Notes
-
android:launchMode="singleTask"is important so the OS delivers the callback to the existing activity instead of launching a new instance or killing the previous task. -
Your JS handler should map each URL (success, pending, canceled) to the correct checkout state.
-
Mercado Pago Wallet does not support a custom on_failed callback. Only on_success, on_canceled, and on_pending are configurable for this processor — don't register a deep link for a "failed" outcome, it will never fire for Mercado Pago Wallet.
Only do this with a scheme dedicated to DEUNA payment callbacks. A scheme-only filter is a catch-all — it also intercepts any other feature's deep link that happens to reuse the same scheme (OAuth libraries, Firebase Dynamic Links, etc.), and if another activity in your app also filters on that scheme, Android may show the user a disambiguation ("Open with...") dialog instead of returning cleanly.
If you'd rather declare an explicit android:host instead of going scheme-only, make sure it matches exactly the host you set in callback_urls for every outcome (e.g. payment in yourapp://payment/success). A mismatch isn't a crash or a visible error — Android silently fails to resolve the redirect, and the checkout UI is left stuck on "processing" after the shopper returns from Mercado Pago, since the SDK never receives any signal that the payment finished. This is easy to miss during testing, because the app eventually seems to recover if the user backgrounds and reopens it — that happens to work by coincidence (an unrelated app-foreground fallback), not because the deep link was actually delivered.
Expo – Deep Links and Redirect Handling (Generic)
Overview For Expo apps, use expo-web-browser to open Mercado Pago and configure deep links with the scheme in app.json. Then listen to Linking events to close the browser and resume the flow after the callback.
-
Configure Deep Links in
app.jsonDefine a scheme and add a single Android intent filter for it.JSON{ "expo": { "scheme": "yourapp", "android": { "intentFilters": [ { "action": "VIEW", "data": [{ "scheme": "yourapp" }], "category": ["BROWSABLE", "DEFAULT"] } ] } } }Don't add
hostto thedataentry — a scheme-only filter matches any host/path underyourapp://, so it covers every outcome incallback_urlswithout further changes.
-
Open Mercado Pago with expo-web-browser Implement an InAppBrowserAdapter that uses openBrowserAsync.
JavaScriptimport * as WebBrowser from 'expo-web-browser'; import { InAppBrowserAdapter } from '@deuna/react-native-sdk'; class ExpoWebBrowserAdapter implements InAppBrowserAdapter { async openUrl(url: string): Promise<void> { await WebBrowser.openBrowserAsync(url, { presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET, dismissButtonStyle: 'close', }); } } -
Handle Deep Link Callbacks in JS Listen for Linking events and dismiss the browser on iOS when the callback arrives.
JavaScriptimport { Linking, Platform } from 'react-native'; import * as WebBrowser from 'expo-web-browser'; WebBrowser.maybeCompleteAuthSession(); Linking.addEventListener('url', ({ url }) => { if (url?.startsWith('yourapp://') && Platform.OS === 'ios') { WebBrowser.dismissBrowser(); } // Route success/pending/canceled based on the URL. }); -
Match callback_urls to Your Deep Links Ensure your order uses the same scheme/host values.
JSON"callback_urls": { "on_success": "yourapp://payment/success", "on_pending": "yourapp://payment/pending", "on_canceled": "yourapp://payment/canceled" }
Mercado Pago Wallet does not support a custom on_failed callback. Only on_success, on_canceled, and on_pending are configurable for this processor — leave on_failed out of callback_urls and don't declare an intentFilters entry for it.
Notes
WebBrowser.maybeCompleteAuthSession()is required so iOS can complete and close the session after a deep-link callback.- Ensure your JS handler maps each callback URL to the correct checkout state.
Only do this if yourapp is a scheme dedicated to DEUNA payment callbacks — it will also catch any other deep link that reuses the same scheme (another intentFilters entry, an auth library, etc.), and if more than one activity ends up matching the link, Android may prompt the user with a disambiguation dialog instead of returning cleanly from Mercado Pago.
If you'd rather declare an explicit host per outcome in app.json instead of going scheme-only, it must match exactly the host used in callback_urls (e.g. payment in yourapp://payment/success). If a host is missing or doesn't match, Android cannot resolve the redirect intent — no crash, no JS error, no Linking 'url' event fires. The checkout will appear stuck on "processing" after returning from Mercado Pago, since nothing ever tells the SDK the payment finished. Don't mix a scheme-only intentFilters entry with host-specific ones for the same scheme expecting most-specific-wins matching — Expo/Android don't do that; use either the scheme-only filter or per-host filters, not both.