Prerequisites๐
Dart Version: Supports 3.11.4 and upper
Flutter Version:Supports 3.41.6 and upper
Minimum Version support for ios is 14
Download the SDK from below link
Dart Version: Supports 3.11.4 and upper
Flutter Version:Supports 3.41.6 and upper
Minimum Version support for ios is 14
Download the SDK from below link
Please refer Order API Documentation for more details.
Use the below url to create an order in your (backend) server.
Production URL: https://apiv2.payg.in/payment/api/order/CreateIntent
Authorization: basic authentication with base64encoded
PayGAuthentication โ
<MerchantAuthenticationKey>:<MerchantAuthenticationToken>:M:<MID>
{
"MID": "3EMF9915EF35567",
"UniqueRequestId": "TEST512scx3",
"UserDefinedData": {
"UserDefined1": ""
},
"ProductData": "{'PaymentReason':'OnlineOrder for OrderNo- 1234'}",
"RequestDateTime": "06232023",
"RedirectUrl": "https://payg.in",
"TransactionData": {
"AcceptedPaymentTypes": "",
"PaymentType": "UPIINTENT",
"SurchargeType": "",
"SurchargeValue": "",
"RefTransactionId": "",
"IndustrySpecificationCode": "",
"PartialPaymentOption": ""
},
"OrderAmount": "3",
"OrderType": "",
"OrderAmountData": {
"AmountTypeDesc": "3",
"Amount": "2"
},
"CustomerData": {
"CustomerId": "123456",
"CustomerNotes": "Mens clothing",
"FirstName": "Ayush",
"LastName": "Rane",
"MobileNo": "7887673322",
"Email": "ayushtesting12@gmail.com",
"EmailReceipt": "true",
"BillingAddress": "76, Sch. 115",
"BillingCity": "Indore",
"BillingState": "MP",
"BillingCountry": "India",
"BillingZipCode": "452011",
"ShippingFirstName": "Ayush",
"ShippingLastName": "Rane",
"ShippingAddress": "76, Sch. 115",
"ShippingCity": "Indore",
"ShippingState": "MP",
"ShippingCountry": "India",
"ShippingZipCode": "452011",
"ShippingMobileNo": "9987887733"
},
"IntegrationData": {
"UserName": "Ayush",
"Source": "3213",
"IntegrationType": "11",
"HashData": "",
"PlatformId": "1"
}
}
var data = {
"Merchantkeyid": MerchantKeyId,
"UniqueRequestId": getRandomString(10),
"OrderStatus": 'Initiating',
"UserDefinedData": {"UserDefined1": ''},
"RequestDateTime": "11/08/2021",
"RedirectUrl": RedirectUrl,
"TransactionData": {
"AcceptedPaymentTypes": '',
"PaymentType": orderType,
"SurchargeType": orderType,
"SurchargeValue": orderType,
"RefTransactionId": orderType,
"IndustrySpecificationCode": orderType,
"PartialPaymentOption": orderType,
},
"OrderAmount": orderAmountController.text,
"OrderType": 'MOBILE',
"OrderAmountData": {"AmountTypeDesc": amountTypeDesc, "Amount": "20"},
"CustomerData": {
'CustomerId': CustomerId,
'CustomerNotes': customerNotes,
'FirstName': orderFirstNameController.text,
'LastName': orderLastNameController.text,
'MobileNo': mobileNoController.text,
'Email': email,
'EmailReceipt': emailReceipt,
'BillingAddress': billingAddress,
'BillingCity': billingCity,
'BillingState': billingState,
'BillingCountry': billingCountry,
'BillingZipCode': billingZipCode,
'ShippingFirstName': shippingFirstName,
'ShippingLastName': shippingLastName,
'ShippingAddress': shippingAddress,
'ShippingCity': shippingCity,
'ShippingState': shippingState,
'ShippingCountry': shippingCountry,
'ShippingZipCode': shippingZipCode,
'ShippingMobileNo': shippingMobileNo,
},
"IntegrationData": {
"UserName": userName,
"Source": 'MobileSDK',
"IntegrationType": "11",
"HashData": HashData,
"PlatformId": PlatformId,
}
};
{
"OrderKeyId": "93281250227M10404UTEST512scx3",
"MerchantKeyId": 10121,
"UniqueRequestId": "TEST512scx3",
"OrderType": "PAYMENT",
"OrderAmount": 3,
"OrderId": null,
"OrderStatus": null,
"OrderPaymentStatus": 0,
"OrderPaymentStatusText": null,
"PaymentStatus": 0,
"PaymentTransactionId": null,
"PaymentResponseCode": 0,
"PaymentApprovalCode": null,
"PaymentReasonCode": null,
"PaymentTransactionRefNo": null,
"PaymentResponseText": null,
"PaymentMethod": null,
"PaymentAccount": null,
"OrderRefundTransactionId": 0,
"RefundPaymentResponseCode": 0,
"RefundPaymentResponseText": null,
"RefundDateTime": null,
"CardType": null,
"CardLevel": null,
"OrderNotes": null,
"PaymentDateTime": null,
"UpdatedDateTime": null,
"PaymentProcessUrl": "null",
"CustomerData": {
"CustomerId": "123456",
"CustomerNotes": "Mens clothing",
"FirstName": "Ayush",
"LastName": "Rane",
"MobileNo": "7887673322",
"Email": "ayushtesting12@gmail.com",
"EmailReceipt": true,
"BillingAddress": "76, Sch. 115",
"BillingCity": "Indore",
"BillingState": "MP",
"BillingCountry": "India",
"BillingZipCode": "452011",
"ShippingFirstName": "Ayush",
"ShippingLastName": "Rane",
"ShippingAddress": "76, Sch. 115",
"ShippingCity": "Indore",
"ShippingState": "MP",
"ShippingCountry": "India",
"ShippingZipCode": "452011",
"ShippingMobileNo": "9987887733"
},
"ProductData": "{'PaymentReason':'OnlineOrder for OrderNo- 1234'}",
"OrderPaymentCustomerData": {
"FirstName": "Ayush",
"LastName": null,
"Address": null,
"City": null,
"State": null,
"ZipCode": null,
"Country": null,
"MobileNo": "7887673322",
"Email": "ayushtesting12@gmail.com",
"UserId": null,
"IpAddress": null
},
"UpiLink": "upi://pay?pa=businesspayg@isis&am=1&tr=Y564417909M21764D543216&cu=INR&mc=2006&pn=Test Account"
"UserDefinedData": {
"UserDefined1": "",
"UserDefined2": null,
"UserDefined3": null,
"UserDefined4": null,
"UserDefined5": null,
"UserDefined6": null,
"UserDefined7": null,
"UserDefined8": null,
"UserDefined9": null,
"UserDefined10": null,
"UserDefined11": null,
"UserDefined12": null,
"UserDefined13": null,
"UserDefined14": null,
"UserDefined15": null,
"UserDefined16": null,
"UserDefined17": null,
"UserDefined18": null,
"UserDefined19": null,
"UserDefined20": null
},
"SplitPaymentResponse": null,
"Id": 93281
}
Once your order is created successfully,then you will UPI link in response. This payment url is used to redirect to actual payment screen.
UPI Link is for UPI payment. You can use either payment process URL or UPI based on your requirement.
Add the package to your pubspec.yaml (adjust source depending on whether you're publishing to pub.dev or using a local):
dependencies:
flutter:
sdk: flutter
rana_pay_upi:
path: ../rana_pay_upi # or a pub.dev version
Then run:
flutter pub get
Since UPI apps are launched via Android intents, you need to declare the UPI apps you intend to query/launch in your AndroidManifest.xml (required from Android 11 / API 30 onward due to package visibility restrictions):
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="upi" />
</intent>
</queries>
Without this block, getUpiApps() may return an empty list on newer Android versions even if UPI apps are installed.
import 'package:rana_pay_upi/rana_pay_upi.dart';
Two ways are exposed to fetch the platform version:
final upi = RanaPayUpi();
// Instance method (via platform interface)
String? version1 = await upi.getPlatformVersion();
// Static method (direct MethodChannel call)
String? version2 = await RanaPayUpi.getPlatformVersionValue();
print('Platform version: $version1');
Returns a list of UpiApp objects (Google Pay, PhonePe, Paytm, BHIM, etc.) that are installed on the device.
List<UpiApp> apps = await RanaPayUpi.getUpiApps();
for (final app in apps) {
print('${app.appName} (${app.packageName})');
// app.icon is a Uint8List (decoded PNG/JPEG bytes) or null
}
Field ๐ |
Type ๐ |
Description ๐ |
| packageName | String | Android package name, e.g. com.google.android.apps.nbu.paisa.user |
| appName | String | Human-readable app name, e.g. Google Pay |
| icon | Uint8List? | Decoded app icon bytes (nullable) |
You can render the icon directly in a widget:
if (app.icon != null) Image.memory(app.icon!)
Build a standard UPI deep link and pass it along with the target app's package name:
String upiUrl =
'upi://pay?pa=merchant@bank&pn=MerchantName&am=100.00&cu=INR&tn=Order1234';
String response = await RanaPayUpi.launchUpiApp(
'com.phonepe.app', // packageName from getUpiApps()
upiUrl,
);
print('Raw response: $response');
a raw string returned by the UPI app, typically inkey=value&key=value query format, for example:
Status=SUCCESS&txnId=T12345&responseCode=00
If something goes wrong (exception, no response from the app), the method falls back to:
Status=PENDING&responseCode=&error=EMPTY_RESPONSE
The helper function parseResponse()included in this package expects input shaped like a Dart map string โ{Status: SUCCESS, txnId: T12345} โ and splits on ", " and ": ". The actual string returned by launchUpiApp() is in UPI'skey=value&key=value format, not the {key: value} format, soparseResponse() will not parse it correctly. Use Uri.splitQueryString() instead โ it's built into Dart, handles URL-encoding correctly, and is the approach used in production:
Map<String, String> parseUpiResponse(String response) {
if (response.isEmpty) return {};
try {
return Uri.splitQueryString(response);
} catch (e) {
return {};
}
}
Keep parseResponse() around only if some other part of your native/platform code actually returns the {key: value} format.
import 'package:flutter/material.dart';
import 'package:rana_pay_upi/rana_pay_upi.dart';
class PaymentScreen extends StatefulWidget {
const PaymentScreen({super.key});
@override
State<PaymentScreen> createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State {
List<UpiApp> _apps = [];
@override
void initState() {
super.initState();
_loadApps();
}
Future<void>_loadApps() async {
final apps = await RanaPayUpi.getUpiApps();
setState(() => _apps = apps);
}
Future<void> _pay(UpiApp app) async {
final url =
'upi://pay?pa=merchant@bank&pn=MerchantName&am=100.00&cu=INR&tn=Order1234';
final response = await RanaPayUpi.launchUpiApp(app.packageName, url);
final parsed = parseUpiResponse(response); // see helper above
if (parsed['Status'] == 'SUCCESS') {
// handle success, e.g. show confirmation, verify on backend
} else {
// handle pending/failure
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pay via UPI')),
body: ListView.builder(
itemCount: _apps.length,
itemBuilder: (context, index) {
final app = _apps[index];
return ListTile(
leading: app.icon != null ? Image.memory(app.icon!) : null,
title: Text(app.appName),
onTap: () => _pay(app),
);
},
),
);
}
}
This is the pattern actually recommended for a payment screen โ list apps โ create an order on your backend to get the UPI deep link โ launch the chosen app โ parse the raw response โ always confirm the final result by calling your payment status API, since the app's on-device response is not proof of payment.
enum _UpiOutcome { none, success, failure, pending, cancelled, error }
Future<void> onAppTapped(UpiApp app) async {
// 1. Get (or reuse) the UPI deep link for this order from your backend.
final upiLink = await paymentMethodController.createPaymentOrder(
getPaymentAppOrderInfoModel: orderInfo,
isfrom: "UPIINTENT",
context: context,
);
try {
// 2. Launch the chosen app with that link.
final response = await RanaPayUpi.launchUpiApp(app.packageName, upiLink);
// 3. Parse the raw "key=value&key=value" response.
final parsed = Uri.splitQueryString(response);
final status = (parsed["Status"] ?? "").toUpperCase();
final responseCode = parsed["responseCode"] ?? "";
// 4. Regardless of what the app reported, ask your backend for the
// authoritative status โ payments can settle even if the client-side
// response is empty, delayed, or the user backgrounds the app.
await paymentMethodController.getPaymentStatus(
context: context,
orderKeyId: orderInfo?.orderkeyid,
isFrom: "UPIINTENT",
getPaymentAppOrderInfo: orderInfo,
);
// 5. Use the client-side response only to drive immediate UI feedback
// (e.g. "Payment successful" banner) while the backend call above is
// the source of truth for what actually gets recorded.
if (status == "SUCCESS" && responseCode == "0") {
// show success banner
} else if (status == "FAILURE") {
// show failure banner, offer retry
} else if (status == "CANCELLED" || status == "CANCELED") {
// show "you cancelled" banner
} else {
// treat as pending โ the backend status check will confirm/update this
}
} catch (e) {
// app not installed / launch failed โ show an error banner, don't call
// it a payment failure since no attempt actually reached the bank
}
}
Always call your payment status API after launchUpiApp() returns, even on success โ the returned string is just what the UPI app chose to report locally, not a settlement confirmation.
Treat any status that isn't a clean SUCCESS + responseCode == "0" aspending, not failed โ many UPI apps report ambiguous or empty statuses even when the payment eventually succeeds on the bank's side.
Keep the try/catch around launchUpiApp() separate from response evaluation โ a thrown exception means the app couldn't be opened at all (different from the app opening and reporting a failure).
Use Uri.splitQueryString(response) for parsing โ see ยง4.3 above.
getUpiApps() swallows exceptions internally and returns an empty list on failure โ check for an empty list in your UI (e.g. "No UPI apps found").
launchUpiApp() swallows exceptions and returns a fallbackStatus=PENDING&responseCode=&error=EMPTY_RESPONSE string โ always treatPENDING as "verify with your backend," not as a confirmed failure, since UPI payments can complete on the bank's side even if the app response is lost.
Always verify final payment status server-side (via your payment gateway/bank webhook) rather than trusting the client-side response alone โ this is standard practice for UPI integrations.
UPI itself has no formal iOS equivalent of Android's intent-based app discovery, so iOS support works fundamentally differently and needs its own native setup. This matters because your UI (as shown above) falls back to aDropdownButtonFormField on iOS instead of the app grid used on Android โ which only works if the plugin's iOS side is actually able to detect, open, and receive a callback from installed UPI apps.
There are two separate pieces of iOS setup: (A) letting your app query and open other UPI apps, and (B) letting other UPI apps call back into your app once the payment is done. Both are required.
iOS can't enumerate "anything that handles upi://" the way Android'sPackageManager can. Each app's scheme must be checked one by one viacanOpenURL, and must be whitelisted in Info.plist:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>tez</string>
<string>phonepe</string>
<string>paytmmp</string>
<string>credpay</string>
<string>bhim</string>
<string>upi</string>
</array>
Without this, canOpenURL (and therefore getUpiApps() on iOS) will report every app as "not installed," even if it is.
For a UPI app to hand control back to your app after the payment completes, your app needs its own registered URL scheme that you pass as the callback/return URL when building the UPI link. Add this to Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.payg.upi.callback</string>
<key>CFBundleURLSchemes</key>
<array>
<string>payg</string>
</array>
</dict>
</array>
Here payg is the scheme the plugin listens on โ when a UPI app finishes and redirects to something like payg://response?Status=SUCCESS&..., iOS routes that back into your app rather than dropping it.
Your AppDelegate needs to forward the incoming URL to the plugin so it can parse it and resolve the Future that launchUpiApp() is waiting on:
// MARK: - Handle URL Scheme Callbacks
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
print("๐ฑ AppDelegate - Received URL: \(url.absoluteString)")
// Let the RanaPayUpiPlugin handle the URL
if let plugin = RanaPayUpiPlugin.shared {
let handled = plugin.handleOpenURL(url)
if handled {
print("โ
RanaPayUpiPlugin handled URL successfully")
return true
}
}
// Handle Facebook login callback if needed
if let scheme = url.scheme, scheme.hasPrefix("fb") {
print("๐ฑ Facebook login callback detected")
return super.application(app, open: url, options: options)
}
// If not handled by plugin, pass to Flutter's default handler
print("โ ๏ธ URL not handled by plugin, passing to Flutter")
return super.application(app, open: url, options: options)
}
// MARK: - Handle Universal Links
override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL {
print("๐ฑ Received universal link: \(url.absoluteString)")
if let plugin = RanaPayUpiPlugin.shared {
if plugin.handleOpenURL(url) {
print("โ
Plugin handled universal link")
return true
}
}
}
return super.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
application(_:open:options:) fires when a UPI app redirects back into your app via the payg:// custom scheme (ยง6.2). It hands the URL toRanaPayUpiPlugin.shared.handleOpenURL(url), which is presumably what parses the query string and completes the pending launchUpiApp() call on the Dart side.
application(_:continue:restorationHandler:) covers the case where the redirect comes back as a universal link (https://...) instead of a custom scheme โ some UPI apps prefer this. Both paths funnel into the samehandleOpenURL, so you don't need separate Dart-side handling for each.
The Facebook (fb...) scheme check is just making sure this callback handler doesn't swallow unrelated URL callbacks your app already relies on โ keep any other third-party SDK scheme checks alongside it if you add more in the future.
If neither the plugin nor a known third-party scheme handles it, it falls through to Flutter's own default URL handling so nothing else in your app breaks.
โ ๏ธ Make sure this code lives in your ios/Runner/AppDelegate.swift, and that RanaPayUpiPlugin.shared is actually being set somewhere in the plugin's register(with:) โ otherwise plugin will be nil and every callback silently falls through to Flutter's default handler, meaninglaunchUpiApp() will simply hang/timeout on iOS regardless of correctInfo.plist setup.
but on iOS it goes throughUIApplication.shared.open(url:) rather than an Android Intent. Response handling on the Dart side (Uri.splitQueryString) stays the same regardless of platform, since the returned string format is controlled by your plugin, not by iOS.
Even with ยง6.2/ยง6.3 wired up correctly, some UPI apps on iOS don't reliably redirect back (less consistent than Android's startActivityForResult pattern). Because of this, the backend payment-status check described in ยง4.5 is even more important on iOS โ don't treat a missing callback as a failed payment.
if you whitelist third-party UPI schemes purely to check installation status, be prepared to explain this usage during App Store review, as unused/undeclared scheme queries are sometimes flagged.
Requirement ๐ |
Android ๐ |
iOS ๐ |
| App discovery | PackageManager intent query for upi:// scheme | Hardcoded scheme list + canOpenURL |
| Manifest/plist entry (query) | <queries>block in AndroidManifest.xml | Info.plist |
| Callback registration | Not needed (Activity result is enough) | CFBundleURLTypes custom scheme in Info.plist |
| Callback handling | Handled by Android Intent result | AppDelegate open url + universal link overrides |
| Launch mechanism | Android Intent | UIApplication.shared.open(url:) |
| Callback reliability | Generally reliable | Inconsistent โ always verify via backend |
| UI shown in this app | Grid of app tiles | Dropdown of known apps |
AndroidManifest.xml <queries> block for package visibility (Android 11+).
Info.plist LSApplicationQueriesSchemes entries for iOS (querying apps).
Info.plist CFBundleURLTypes entry for iOS (receiving the callback).
AppDelegate.swift URL + universal-link handlers wired to
RanaPayUpiPlugin.shared.handleOpenURL(url).
Real UPI-enabled bank account and VPA (pa) for testing actual payments.
A backend payment-status endpoint โ required on both platforms, but especially load-bearing on iOS (see ยง6.4).
You can get order details by passing the below request parameters.
Authorization: basic authentication with base64encoded PayGAuthentication โ <MerchantAuthenticationKey>:<MerchantAuthenticationToken>:M:<MID>
{ "OrderKeyId": "93281250227M10404UTEST512scx3", "MID":"3EMF9915EF35567", "PaymentType": "" }
var data = { "Merchantkeyid": MerchantKeyId, "OrderKeyId": orderIdController.text, "PaymentType": '', };
{ "OrderKeyId": "93281250227M10404UTEST512scx3", "MerchantKeyId": 10121, "UniqueRequestId": "TEST512scx3", "OrderType": "PAYMENT", "OrderAmount": 3.00, "OrderId": null, "OrderStatus": "1", "OrderPaymentStatus": 0, "OrderPaymentStatusText": "Paid", "PaymentStatus": 0, "PaymentTransactionId": "767926", "PaymentResponseCode": 1, "PaymentApprovalCode": null, "PaymentReasonCode": null, "PaymentTransactionRefNo": null, "PaymentResponseText": "Approved", "PaymentMethod": "CreditCard", "PaymentAccount": "41111 - XXXX - 1111", "OrderRefundTransactionId": 0, "RefundPaymentResponseCode": 0, "RefundPaymentResponseText": null, "RefundDateTime": null, "CardType": null, "CardLevel": null, "OrderNotes": "Internal", "PaymentDateTime": "2/27/2025 1:57:09 PM", "UpdatedDateTime": "2/27/2025 1:52:35 PM", "PaymentProcessUrl": null, "CustomerData": { "CustomerId": "123456", "CustomerNotes": "Mens clothing", "FirstName": "Ayush", "LastName": "Rane", "MobileNo": "7887673322", "Email": "ayushtesting12@gmail.com", "EmailReceipt": false, "BillingAddress": "76, Sch. 115", "BillingCity": "Indore", "BillingState": "MP", "BillingCountry": "India", "BillingZipCode": "452011", "ShippingFirstName": "Ayush", "ShippingLastName": "Rane", "ShippingAddress": "76, Sch. 115", "ShippingCity": "Indore", "ShippingState": null, "ShippingCountry": "India", "ShippingZipCode": "452011", "ShippingMobileNo": "9987887733" }, "ProductData": "{'PaymentReason':'OnlineOrder for OrderNo- 1234'}", "OrderPaymentCustomerData": null, "UpiLink": null, "OrderPaymentTransactionDetail": [ { "OrderKeyId": "93281250227M10404UTEST512scx3", "MerchantKeyId": 10404, "TransactionId": 767926, "ResponseCode": 1, "ResponseText": "Approved", "TransactionAmount": 3.00 } ], "UserDefinedData": { "UserDefined1": "", "UserDefined2": null, "UserDefined3": null, "UserDefined4": null, "UserDefined5": null, "UserDefined6": null, "UserDefined7": null, "UserDefined8": null, "UserDefined9": null, "UserDefined10": null, "UserDefined11": null, "UserDefined12": null, "UserDefined13": null, "UserDefined14": null, "UserDefined15": null, "UserDefined16": null, "UserDefined17": null, "UserDefined18": null, "UserDefined19": null, "UserDefined20": null }, "SplitPaymentResponse": null, "Id": 93281 }