PPI iOS SDK Integration
M2P Headless UPI SDK for iOS — complete technical integration guide for PPI partners covering onboarding, payments, mandates, disputes, and security.
Table of Contents
- Introduction
- Prerequisites
- Installation
- SDK Initialization
- Device Info Helper
- Security Setup – Key Exchange & Encryption
- SIM Binding
- User Registration & Profile
- VPA (UPI ID) Management
- Account Management
- UPI Number (Numeric ID) Management
- Balance Check
- QR Code – Scan & Pay
- Verify VPA (Validate Payee)
- Make Payment (P2P / P2M)
- Send Money Request (Collect)
- Money Request Actions
- Transaction History
- Check Transaction Status
- Mandate Management
- Dispute Management
- Beneficiary Management
- Block / Unblock User
- Profile Deregistration
- Utility Functions
- Error Handling Reference
- Best Practices
Introduction
The M2P UPI Headless SDK for iOS gives partners a fully headless (no built-in UI) way to embed UPI payment capabilities inside their own applications. All UPI business logic, security, and API orchestration are handled by the SDK while your app owns the user experience entirely.
Key Capabilities
| Category | Features |
|---|---|
| Onboarding | SIM binding, profile registration, VPA creation, account linking |
| Payments | P2P, P2M, QR Scan & Pay, Collect, Mandates |
| Management | Balance check, transaction history, status check, disputes |
| Security | Jailbreak & root detection, SSL pinning, end-to-end encryption, VPN check |
| Utilities | QR generation & parsing, date helpers, device info, IP address |
Prerequisites
| Requirement | Minimum Version |
|---|---|
| Xcode | 15.0 |
| iOS Deployment Target | 17.0 |
| Swift | 5.0 |
You will also need the following partner credentials from M2P:
- Base URL (UAT and Production)
- API Endpoint path
- SSL Public Key (base64)
- Channel Code / Tenant identifier
- IP Code (used to generate transaction IDs)
Required Info.plist Permissions
<key>NSCameraUsageDescription</key>
<string>Required to scan UPI QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Required to save and read QR codes from photo library</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to enhance transaction security</string>Installation
The SDK is distributed as a pre-built XCFramework / .framework. CocoaPods and Swift Package Manager are not supported.
Steps
-
Obtain
M2PUPIHLSDK.frameworkfrom M2P (or build it from source by openingM2PUPIHLSDK.xcodeprojand building theM2PUPIHLSDKscheme). -
Drag
M2PUPIHLSDK.frameworkinto your Xcode project navigator.- Check "Copy items if needed".
-
In your app target → General → Frameworks, Libraries, and Embedded Content, set
M2PUPIHLSDK.frameworkto Embed & Sign. -
Add the following Swift Package dependencies to your project via File → Add Package Dependencies:
Package URL Version Rule CryptoSwifthttps://github.com/krzyzanowskim/CryptoSwift Exact – 1.5.1SwCrypthttps://github.com/soyersoyer/SwCrypt Up to Next Major – 5.0.0IOSSecuritySuitehttps://github.com/securing/IOSSecuritySuite.git Up to Next Major – 2.1.0 -
Verify the import compiles:
import M2PUPIHLSDK
SDK Initialization
Initialize the SDK once at application startup — the best place is AppDelegate.application(_:didFinishLaunchingWithOptions:) or your root SceneDelegate.
import M2PUPIHLSDK
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 1. Configure connectivity and security
M2PUPISDKHL.shared.setUpSDKValues(
baseUrl: "https://<your-base-url>/",
endPoint: "<api-endpoint-path>/",
sslPublicKey: "<base64-ssl-public-key>",
enableRootedDevice: true // pass false only during local development
)
// 2. Set per-transaction amount limits (values in INR)
M2PUPISDKHL.shared.setUpLimit(
peerToPeerMaxAmountLimit: "100000.00",
merchantMaxAmountLimit: "200000.00",
collectMaxAmountLimit: "2000.00",
mccMerchantMaxAmountLimit: "500000.00",
offlineNonVerifiedMerchant: "2000.00"
)
return true
}Parameters – `setUpSDKValues`
| Parameter | Description |
|---|---|
baseUrl | Full base URL including trailing slash |
endPoint | API endpoint prefix (can be empty "") |
sslPublicKey | Base64-encoded server SSL public key for certificate pinning |
enableRootedDevice | true = block jailbroken/rooted devices; false = allow (dev only) |
Device Info Helper
Most API request models contain a CommonDeviceInfo object. Build a helper once and reuse it across calls.
func buildDeviceInfo() -> CommonDeviceInfo {
CommonDeviceInfo(
deviceId: M2PUPISDKHL.shared.getDeviceId(), // vendor UUID (no dashes)
deviceType: "MOB",
os: M2PUPISDKHL.shared.getOSDetails(), // e.g. "ios 17.2"
telecom: "sim1", // SIM slot identifier
geoCode: "13.010,80.208", // lat,long
appId: M2PUPISDKHL.shared.getAppId(), // bundle identifier
ipAddress: M2PUPISDKHL.shared.getIPAddress() ?? "",
location: "Chennai",
mobile: "91\(userMobileNumber)" // country code + 10-digit number
)
}Field length limits enforced by the SDK
| Field | Max length |
|---|---|
appId | 75 |
mobile | 12 |
deviceId | 45 |
location | 40 |
geoCode | 15 |
ipAddress | 45 |
telecom | 15 |
Security Setup – Key Exchange & Encryption
All sensitive payloads are encrypted using ECDH key exchange. Perform this handshake before making any business API calls.
Step 1 – Generate a local key pair
// key.0 = clientPrivateKeyString
// key.1 = clientPublicKeyString (send this to the server)
let key = M2PUPISDKHL.shared.keyPairExchange()Step 2 – Exchange public keys with the server
let requestBody: [String: Any] = ["publicKey": key.1]
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .keyExchange(isCombine: false, m2pParams: requestBody),
m2pHeaders: ["Content-Type": "application/json", "TENANT": "<your-tenant>"],
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(PairPublicKeyResponse.self, from: data),
let serverPublicKey = decoded.result?.publicKey,
!serverPublicKey.isEmpty
else {
// Handle key exchange failure
return
}
// Step 3 – Enable encryption for all subsequent calls
M2PUPISDKHL.shared.setEncryptionAndDecryptionKey(
isEncryptionRequired: true,
clientPrivateKeyString: key.0,
serverPublickey: serverPublicKey,
requestReferenceId: decoded.requestReferenceId ?? ""
)
// Proceed with onboarding or business flows here
}isCombine flag
false– only performs key exchange. Use this to get the server public key first.true– combines key exchange with profile registration. Response model changes accordingly.
SIM Binding
SIM binding verifies that the user's SIM card is the one registered on the device. It must be completed before profile onboarding.
Initiate SIM Binding Request
let params = SimBindingApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .simBindingRequest(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(SimBindingRequestModels.self, from: data)
else { return }
let callbackRef = decoded.callbackRef ?? ""
// Store callbackRef – needed in step 7.2
// The SDK automatically sends a silent SMS; await delivery
}Check SIM Binding Status
After the silent SMS is delivered, check the status using the callbackRef from step 7.1.
let statusParams = SimBindingStatusCheckApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
callbackRef: callbackRef, // from step 7.1
seqNo: "1"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkSimBindStatus(m2pParams: statusParams),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let _ = try? JSONDecoder().decode(SimBindingStatusCheckModels.self, from: data)
else { return }
// On success, proceed to Step 8 – Register Profile
}User Registration & Profile
Register Profile
Creates a new UPI profile for the user. If the profile already exists, the SDK automatically falls through to getProfile when isOnlyRegister is false.
let params = GetProfileIDApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE",
type: "PERSON",
name: "User Full Name"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .registerProfile(isOnlyRegister: false, m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8)
else { return }
// isOnlyRegister = false → response is GetProfileResponse
if let profile = try? JSONDecoder().decode(GetProfileResponse.self, from: data) {
let profileId = profile.result?.id ?? ""
// Store profileId for subsequent calls
}
// isOnlyRegister = true → response is GetProfileIDResponseModels
}isOnlyRegister flag
false– calls register + getProfile internally; returnsGetProfileResponse.true– calls register only; returnsGetProfileIDResponseModels.
Get Profile
let params = GetProfileApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getProfile(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let profile = try? JSONDecoder().decode(GetProfileResponse.self, from: data)
else { return }
// Use profile.result to populate your UI
}Update User Profile
let params = UpdateProfileApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
// fill additional fields as required
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .updateUserProfile(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle UpdateProfileModel response
}VPA (UPI ID) Management
Check VPA Availability
let params = CheckIfVpaAvailableRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
vpaId: "desiredHandle@m2p"
)
M2PUPISDKHL.shared.getM2PUpiService(
// isCreateVPA = false → check only
// isCreateVPA = true → check AND create in one call
m2pFlowType: .checkVPAAvailability(isCreateVPA: false, m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let result = try? JSONDecoder().decode(CheckIfVpaAvailableResponse.self, from: data)
else { return }
// If VPA is available, call createVPA
}Create VPA
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .createVPA(m2pParams: params), // same params as check
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle CheckIfVpaAvailableResponse
}Set Primary VPA
let params = SetPrimaryUpiIdApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
// vpaId: the VPA to make primary
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .setPrimaryUPIID(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle SetPrimaryUpiIdResponse
}Delete VPA
let params = VPADeregisterApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
// vpaId: the VPA to delete
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .deleteUPIID(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle VPARemoveResponse
}Account Management
Fetch Account List
let params = FetchAccountApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getAccountList(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let accounts = try? JSONDecoder().decode(FetchAccountResponse.self, from: data)
else { return }
// Populate account picker UI from accounts.result
}Add Account
let params = AddAccountApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
// bank details, IFSC etc.
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .addUserAccount(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle AddAccountResponse
}Set Primary Account
let params = SetPrimaryAccountApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
// accountRef: the account to set as primary
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .setPrimaryAccount(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle SetPrimaryAccountResponse
}Remove Account
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .removeAccount(m2pParams: params), // SetPrimaryAccountApiRequest
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle SetPrimaryAccountResponse
}Manage User Account List
let params = AccountListRequestParams(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .manageUserAccountList(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle AccountListModel
}UPI Number (Numeric ID) Management
A UPI Number (e.g., 747377344) is an optional numeric alias for a VPA.
Check UPI Number Availability
let params = CheckUPINumberApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
number: "747377344",
profileId: profileId,
seqNo: "1",
txnType: "CMREGISTRATION",
type: "NUMERICID",
vpa: "mobile@m2p"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkUPINumberAvailability(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let result = try? JSONDecoder().decode(CheckUPINumberApiReseponse.self, from: data)
else { return }
if result.result?.status == "AVAILABLE" {
// Proceed to createUPINumber
}
}Create UPI Number
let params = CreateUPINumberApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
number: "747377344",
opType: "ADD",
seqNo: "1",
profileId: profileId,
status: "ACTIVE",
txnType: "CMREGISTRATION",
type: "NUMERICID",
vpa: "mobile@m2p"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .createUPINumber(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle CreateUPINumberApiReseponse
}UPI Number Action (Modify / Remove)
// Set opType to "UPDATE" or "REMOVE" as needed
let params = CreateUPINumberApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
number: "747377344",
opType: "REMOVE",
seqNo: "1",
profileId: profileId,
status: "INACTIVE",
txnType: "CMREGISTRATION",
type: "NUMERICID",
vpa: "mobile@m2p"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .upiNumberAction(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle CreateUPINumberApiReseponse
}Balance Check
let params = BankBalanceRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
// accountRef and vpaId of the account to check
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkBalance(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let balanceModel = try? JSONDecoder().decode(BankBalanceResponseModel.self, from: data)
else { return }
let balance = balanceModel.result?.balance ?? "--"
// Display balance in your UI
}QR Code – Scan & Pay
Parse a Scanned QR String
After reading a QR code (e.g., via AVFoundation), pass the raw string to the SDK for parsing and validation.
let payModel = M2PUPISDKHL.shared.validateQRData(scannedText: rawQRString)
if payModel.isQRExpired == true {
// Show "QR has expired" error
} else if payModel.isInvalidQR == true {
// Show "Invalid QR" error
} else if payModel.qrCodeType == QRType.internationalQR.rawValue {
// Show "International QR not supported" error
} else if payModel.qrCodeType == QRType.mandateQR.rawValue {
guard payModel.isValidStartDate == true,
payModel.isValidEndDate == true else {
// Show "Mandate QR expired" error
return
}
// Proceed with create mandate flow (Section 20.1)
} else if let upiId = payModel.scannedUpiId, !upiId.isEmpty {
// Verify the UPI ID before payment (Section 14)
verifyVPA(type: payModel.payType, id: upiId, payModel: payModel)
} else {
// Show "Invalid QR" error
}Verify a Signed QR (Optional Security Step)
For merchant QR codes that contain a digital signature:
M2PUPISDKHL.shared.verifySignedQR(
originalText: payModel.originalText ?? "",
publicKey: payModel.merchantKey ?? ""
) { isValid in
if isValid {
// QR signature is authentic – proceed to payment
} else {
// Show "Invalid QR signature" error
}
}Generate a QR Code Image
let qrConfig = ConstructQR(
upiId: "yourvpa@m2p",
payeeName: "Store Name",
amount: "500.00",
description: "Payment for order #1234",
txnRef: M2PUPISDKHL.shared.getTransactionID(ipCode: "PPIW")
)
if let qrImage = M2PUPISDKHL.shared.constructQR(constructQR: qrConfig) {
imageView.image = qrImage
}Verify VPA (Validate Payee)
Always verify the payee VPA before initiating a payment. The response provides masked payee details needed to populate UpiTransferPayModel.
let params = RecentTransactionsRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1",
profileId: profileId,
type: payModel.payType, // e.g. InitiationMode.vpa.rawValue
id: payeeVpaId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .verifyVPA(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let validated = try? JSONDecoder().decode(ValidateUPIModel.self, from: data)
else { return }
// Populate the pay model with verified payee details
var payModel = UpiTransferPayModel()
payModel.payeeName = validated.result?.maskName
payModel.payeeId = validated.result?.addr
payModel.payeeType = validated.result?.type
payModel.payeeCode = validated.result?.code
payModel.accountType = validated.result?.accType
payModel.ifscCode = validated.result?.ifsc
payModel.cmid = validated.result?.cmid
payModel.merchantKey = validated.result?.merchantKey
payModel.merchantGenre = validated.result?.merchant?.identifier?.merchantGenre
payModel.transactionType = InitiationMode.pay.rawValue
payModel.payType = InitiationMode.vpa.rawValue
payModel.isVerifiedMerchant = validated.result?.merchant?.verifiedMerchant
// Let the user enter amount and description, then call makePayment (Section 15)
}Make Payment (P2P / P2M)
After verifying the payee (Section 14), construct the payment request and submit.
// Populate amount and description from user input
payModel.amount = "100.00"
payModel.description = "Payment"
let params = UpiPaymentRequestModel(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE",
profileId: profileId,
seqNo: "1",
payModel: payModel
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .makePayment(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let result = try? JSONDecoder().decode(UPIPaymentReqResponse.self, from: data)
else { return }
let txnId = result.result?.txnId ?? ""
// Display receipt or check transaction status (Section 19)
}Send Money Request (Collect)
A collect request asks the payer to approve a payment.
payModel.transactionType = "collect"
payModel.amount = "500.00"
payModel.description = "Requesting payment for services"
let params = UpiPaymentRequestModel(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE",
profileId: profileId,
seqNo: "1",
expiryDate: "2026-04-30T23:59:59",
payModel: payModel
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .sendMoneyRequest(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle UPIPaymentReqResponse
}Money Request Actions
List Incoming / Outgoing Collect Requests
let params = RecentPaymentListApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
// isSender = true → requests you SENT
// isSender = false → requests you RECEIVED
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .moneyRequestList(isSender: false, m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle RecentPaymentListResponse
}Approve or Decline a Collect Request
let params = CollectAuthModelRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
txnId: incomingTxnId,
action: "APPROVE", // or "DECLINE"
custRef: custRefFromRequest
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .moneyRequestAction(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle CollectAuthModel
}Transaction History
let params = TransactionHistoryApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
// add date range filters as needed
)
// offset and pageNo are used for pagination
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .userTransactionList(offset: 0, pageNo: 1, m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let history = try? JSONDecoder().decode(TransactionHistoryResponse.self, from: data)
else { return }
// Render history.result in a table/collection view
}Pagination: Increment pageNo by 1 on each subsequent fetch. Stop fetching when the returned list count is less than your page size.
Recent Transaction List
let params = RecentTransactionsRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .recentTransactionList(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle RecentTransactionsUPIList
}Check Transaction Status
Use this after a payment to confirm the final outcome, especially on timeout or an ambiguous initial response.
let params = CheckStatusRequestModel(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
orgRrn: originalRrn,
extTxnId: txnId,
txnType: "PAYMENT",
subType: "PAY"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkTransactionStatus(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let statusModel = try? JSONDecoder().decode(CheckStatusResModel.self, from: data)
else { return }
let status = statusModel.result?.status ?? ""
// Possible values: "SUCCESS", "FAILURE", "PENDING"
}Mandate Management
Create Mandate
payModel.amount = "1000.00"
payModel.description = "Monthly subscription"
let params = UpiPaymentRequestModel(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE",
profileId: profileId,
seqNo: "1",
expiryDate: "2027-12-31T23:59:59",
payModel: payModel
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .createmandate(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle CreateMandateModel
}Fetch Mandate List
let params = MyMandatesRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getMandateList(offset: 0, pageNo: 1, m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle MyMandatesFetch
}Mandate Actions
let params = MandateAPIRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
txnId: mandateTxnId,
action: "REVOKE" // APPROVE | REVOKE | PAUSE | UNPAUSE
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .mandateAction(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle MandatesActionResponse
}Mandate Status Grouping Helper
let pendingStatuses = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .pending)
let liveStatuses = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .live)
let completedStatuses = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .completed)
let modifyStatuses = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .modify)Dispute Management
List Disputes
let params = DisputeListApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .disputeList(offset: 0, pageNo: 1, m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle DisputeListResponseModel
}Raise a Dispute
let params = CreateDisputeApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
txnId: disputedTxnId,
reason: "GOODS_NOT_RECEIVED"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .raiseDispute(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle CreateDisputeResponseModel
}Beneficiary Management
View Beneficiaries
let params = ViewBeneficiaryRequestModel(entityId: profileId)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .viewBeneficiary(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let list = try? JSONDecoder().decode(ViewBeneficiaryResponse.self, from: data)
else { return }
// Render list.result in your beneficiary screen
}Add Beneficiary
let params = AddBeneficiaryRequest(
entityId: profileId,
accountType: "SAVINGS",
beneficiaryAccountNo: "1234567890",
beneficiaryId: UUID().uuidString,
beneficiaryName: "John Doe",
ifsc: "HDFC0001234",
transferMode: ["IMPS", "NEFT"],
nickName: "John"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .addBeneficiary(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle ValidateUPIModel (confirmation)
}Block / Unblock User
Block or unblock a payer from sending you collect requests.
let params = CollectAuthModelRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
action: "BLOCK", // or "UNBLOCK"
vpaId: targetVpaId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .blockUnblockUser(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle CollectAuthModel
}Fetch Blocked User List
let params = BlockedUserApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getBlockedUserList(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle BlockedUserResponseModel
}Profile Deregistration
Permanently deregisters the user's UPI profile. This action is irreversible.
let params = VPADeregisterApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .deRegisterUserProfile(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// Handle VPADeregisterResponse
// Clear local session data after success
}Utility Functions
SDK & Device Information
let sdkVersion = M2PUPISDKHL.shared.getSDKVersion()
let appId = M2PUPISDKHL.shared.getAppId() // Bundle identifier
let osDetails = M2PUPISDKHL.shared.getOSDetails() // e.g. "ios 17.2"
let deviceId = M2PUPISDKHL.shared.getDeviceId() // Vendor UUID (no dashes)
let ipAddress = M2PUPISDKHL.shared.getIPAddress()Generate a Unique Transaction ID
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: "PPIW")
// Format: <ipCode><UUID without dashes>Date Utilities
// Get a future date
let futureDate = M2PUPISDKHL.shared.getfurtueDateFromCurrentDate(month: 3)
// Convert Date to String
let dateString = M2PUPISDKHL.shared.getStringFromTheDate(date: futureDate, formate: .ddMMyyyy)
// Convert String to Date
let date = M2PUPISDKHL.shared.getDateFromTheString(dateString: "31/12/2026", formate: .ddMMyyyy)
// Check if a date string is still in the future (e.g. for mandate expiry validation)
let isValid = M2PUPISDKHL.shared.isValidationWithCurrentDate(
expireDateString: "31/12/2026",
dateFormat: .ddMMyyyy
)Stop API Timer
M2PUPISDKHL.shared.stopApiTimer()Error Handling Reference
Every getM2PUpiService call returns a SDKResponseModel in its completion block:
public struct SDKResponseModel {
public let statusCode: String? // HTTP status or SDK error code
public let result: String? // JSON string of the response body
public let error: String? // Human-readable error description
}SDK Internal Error Codes
| Code | Trigger | Message |
|---|---|---|
M2P-000 | SSL pinning mismatch | SSL Pinning Failed |
M2P-001 | Jailbroken / rooted device | Widget cannot be invoked on this device |
M2P-003 | No internet | No Internet connection |
M2P-004 | Malformed URL | URL Not Valid |
M2P-005 | Request validation failed | Validation Failed |
M2P-006 | VPN detected | Please turn VPN off to proceed |
M2P-007 | App not from App Store | App might not be from App Store |
M2P-500 | Server error | Oops! Internal Server Error |
Recommended Error Handling Pattern
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .makePayment(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard let response = response else { return }
if let error = response.error, !error.isEmpty {
switch response.statusCode {
case "M2P-001":
showAlert("Device Not Supported", message: error)
case "M2P-003":
showAlert("No Internet", message: error)
case "M2P-006":
showAlert("VPN Detected", message: error)
default:
showAlert("Error", message: error)
}
return
}
guard response.statusCode == "200",
let data = response.result?.data(using: .utf8)
else { return }
// Decode and handle success response
}Best Practices
-
Always perform key exchange first — enable encryption before any business API call. Re-run key exchange if the app is foregrounded after extended background time.
-
Production security — always set
enableRootedDevice: truein production builds. Never disable SSL pinning in a release build. -
Reuse
CommonDeviceInfo— build it once per session. Avoid creating it inside loops. -
Unique transaction IDs — always use
getTransactionID(ipCode:)to generateseqNo/txnIdvalues. Never reuse an ID. -
Handle pending payment states — a payment can return
PENDING. Always callcheckTransactionStatuson timeout before showing a failure to the user. -
Pagination — use
offsetandpageNoconsistently for transaction history, mandate lists, and dispute lists. Load the next page only when the user scrolls to the bottom. -
Stop timer on view dismiss — call
stopApiTimer()when your payment screen is dismissed mid-flight to prevent stale callbacks. -
Never log sensitive data — do not log credentials, private keys, or full payment responses to the console in production. The SDK suppresses its own internal logs when
enableRootedDeviceistrue. -
Validate before calling — verify that
profileIdis non-empty before any profile-scoped call. Missing required fields returnM2P-005. -
Key exchange on session start — treat the key exchange as a session token. If the user is idle and returns, re-run the exchange to get a fresh encrypted channel.
Last updated: March 31, 2026 | SDK Version: 1.0.0
PPI Android SDK Integration
M2P Headless UPI SDK for Android — complete technical integration guide for PPI partners covering onboarding, payments, mandates, disputes, and security.
PSP/TPAP iOS SDK Integration
M2P UPI Headless SDK with CommonLibrary for iOS — complete integration guide for Bank PSP and TPAP partners covering credentials, payments, UPI Lite, Circle, and all NPCI Common Library flows.
