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.
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
- Bank List
- UPI Number (Numeric ID) Management
- 12.1 Check Availability
- 12.2 Create UPI Number
- 12.3 UPI Number Action (Enable / Disable / De-register)
- 12.4 Port UPI Number
- 12.5 Error Codes
- CommonLibrary Setup
- Open CL Library – All Flows
- 14.1 Set UPI PIN
- 14.2 Change UPI PIN
- 14.3 Check Balance (with CL Library)
- 14.4 Pay (P2P / P2M)
- 14.5 Collect (Approve Incoming Request)
- 14.6 Mandate Action / Create Mandate
- 14.7 UPI Lite – Binding
- 14.8 UPI Lite – Top Up (Load Money)
- 14.9 UPI Lite – Pay
- 14.10 UPI Lite – Disable
- 14.11 UPI Lite – Transfer Out
- 14.12 Circle Full Delegate
- 14.13 Circle Pay Approve
- 14.14 International QR Activation
- 14.15 CL Library Callback Handling
- Balance Check (without CL Library)
- OTP Generation & Aadhaar OTP
- 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
- UPI Circle (Delegate)
- UPI Lite – Device-level Utilities
- International QR Management
- UPI Version Check (UPI Lite Eligibility)
- Validate QR
- Circle Check Status
- Profile Deregistration
- Utility Functions
- Error Handling Reference
- Best Practices
- UpiTransferPayModel – Field Reference
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. The CommonLibrary is an additional framework required for all credential-capture operations — setting/changing UPI PIN, making payments, collecting, mandate actions, and UPI Lite flows. It provides a secure NPCI-certified UI overlay for PIN or OTP entry only, while all other business logic and UI remain fully under your control.
Key Capabilities
| Category | Features |
|---|---|
| Onboarding | SIM binding, profile registration, VPA creation, account linking |
| Credentials | Set/Change UPI PIN via CommonLibrary |
| Payments | P2P, P2M, QR Scan & Pay, Collect, Mandates, Circle Pay |
| UPI Lite | Binding, Top Up, Pay, Disable, Transfer Out |
| UPI Circle | Full Delegate, Partial Delegate, Approve/Reject |
| Management | Balance, 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 |
Prerequisites
| Requirement | Minimum Version |
|---|---|
| Xcode | 15.0 |
| iOS Deployment Target | 17.0 |
| Swift | 5.0 |
You will 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)
CommonLibrary.frameworkbinary (provided separately by M2P)
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
Step 1 – Add M2PUPIHLSDK.framework
- Obtain
M2PUPIHLSDK.frameworkfrom M2P (or build it from source usingM2PUPIHLSDK.xcodeproj). - 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.
Step 2 – Add CommonLibrary.framework
- Obtain
CommonLibrary.frameworkfrom M2P. - Drag
CommonLibrary.frameworkinto your Xcode project navigator.- Check "Copy items if needed".
- In your app target → General → Frameworks, Libraries, and Embedded Content, set
CommonLibrary.frameworkto Embed & Sign.
Step 3 – Add Swift Package Dependencies
Add the following packages via File → Add Package Dependencies:
| Package | URL | Version Rule |
|---|---|---|
CryptoSwift | https://github.com/krzyzanowskim/CryptoSwift | Exact – 1.5.1 |
SwCrypt | https://github.com/soyersoyer/SwCrypt | Up to Next Major – 5.0.0 |
IOSSecuritySuite | https://github.com/securing/IOSSecuritySuite.git | Up to Next Major – 2.1.0 |
Step 4 – Verify
Confirm both imports compile:
import M2PUPIHLSDK
import CommonLibrarySDK Initialization
Initialize once at app startup in AppDelegate or 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: "auth/redirect/",
sslPublicKey: "<base64-ssl-public-key>",
enableRootedDevice: true // false during local development only
)
// 2. Enable CommonLibrary additional parameters (recommended)
M2PUPISDKHL.shared.m2pIsCLAdditionparamsRequired = true
// Enables: forgotUpiPINEnabled, resendAadharOTPFeature,
// resendIssuerOTPFeature, issuerResendOTPLimit, aadharResendOTPLimit
// 3. Set per-transaction amount limits (INR)
M2PUPISDKHL.shared.setUpLimit(
peerToPeerMaxAmountLimit: "100000.00",
merchantMaxAmountLimit: "200000.00",
collectMaxAmountLimit: "2000.00",
mccMerchantMaxAmountLimit: "500000.00",
offlineNonVerifiedMerchant: "2000.00"
)
return true
}`m2pIsCLAdditionparamsRequired`
– When true, the CL Library screen includes forgot UPI PIN, resend OTP options, and configures OTP retry limits automatically.
Device Info Helper
Build once per session and reuse across all API calls.
func buildDeviceInfo() -> CommonDeviceInfo {
CommonDeviceInfo(
deviceId: M2PUPISDKHL.shared.getDeviceId(),
deviceType: "MOB",
os: M2PUPISDKHL.shared.getOSDetails(), // e.g. "ios 17.2"
telecom: "sim1",
geoCode: "13.010,80.208", // lat,long
appId: M2PUPISDKHL.shared.getAppId(),
ipAddress: M2PUPISDKHL.shared.getIPAddress() ?? "",
location: "Chennai",
mobile: "91\(userMobileNumber)" // country code + 10-digit number
)
}Field length limits:
| Field | Max |
|---|---|
appId | 75 |
mobile | 12 |
deviceId | 45 |
location | 40 |
geoCode | 15 |
ipAddress | 45 |
telecom | 15 |
Security Setup – Key Exchange & Encryption
Perform this handshake before any business API call. Re-run when the app returns from a long background session.
Step 1 – Generate key pair
// key.0 = clientPrivateKeyString (keep private)
// key.1 = clientPublicKeyString (send to server)
let key = M2PUPISDKHL.shared.keyPairExchange()Step 2 – Exchange with 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 { 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 / CL Library setup
}SIM Binding
Initiate
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; SDK sends a silent SMS automatically
}Check Status
let params = SimBindingStatusCheckApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
callbackRef: callbackRef,
seqNo: "1"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkSimBindStatus(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200" else { return }
// On success, proceed to register profile
}User Registration & Profile
Register Profile
let params = GetProfileIDApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE",
type: "PERSON",
name: "User Full Name"
)
M2PUPISDKHL.shared.getM2PUpiService(
// isOnlyRegister = false → register + getProfile combined; returns GetProfileResponse
// isOnlyRegister = true → register only; returns GetProfileIDResponseModels
m2pFlowType: .registerProfile(isOnlyRegister: false, 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 }
let profileId = profile.result?.id ?? ""
let ipCode = profile.result?.ipCode ?? "" // store for getTransactionID()
}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 }
// profile.result contains accounts, VPAs, ipCode, etc.
}Update Profile
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .updateUserProfile(m2pParams: UpdateProfileApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)),
m2pHeaders: headers,
m2pMethod: .post
) { response in /* Handle UpdateProfileModel */ }VPA (UPI ID) Management
Check 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 + create in one call
m2pFlowType: .checkVPAAvailability(isCreateVPA: false, m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in /* Handle CheckIfVpaAvailableResponse */ }Create VPA
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .createVPA(m2pParams: params), // reuse params from 9.1
m2pHeaders: headers,
m2pMethod: .post
) { response in /* Handle CheckIfVpaAvailableResponse */ }Set Primary VPA
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .setPrimaryUPIID(m2pParams: SetPrimaryUpiIdApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle SetPrimaryUpiIdResponse */ }Delete VPA
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .deleteUPIID(m2pParams: VPADeregisterApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle VPARemoveResponse */ }Account Management
Fetch Account List
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getAccountList(m2pParams: FetchAccountApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
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 }
// accounts.result is [FetchAccountModel]
// Each FetchAccountModel exposes: maskedAccnumber, accRefNumber, lrnNumber,
// credsAllowed, name, liteEnabled, international, etc.
}Key field — mbeba: Each account in the response carries an mbeba field:
"N"→ the account does not have a UPI PIN set yet. Show the Set UPI PIN option (Section 14.1)."Y"→ UPI PIN is already configured. Do not show Set UPI PIN; show Change UPI PIN (Section 14.2) instead.
Always check mbeba before deciding which PIN management option to present to the user.
Add Account
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .addUserAccount(m2pParams: AddAccountApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle AddAccountResponse */ }Set Primary Account
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .setPrimaryAccount(m2pParams: SetPrimaryAccountApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle SetPrimaryAccountResponse */ }Remove Account
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .removeAccount(m2pParams: SetPrimaryAccountApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle SetPrimaryAccountResponse */ }Manage Account List
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .manageUserAccountList(m2pParams: AccountListRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle AccountListModel */ }Bank List
Fetch Bank List
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getBankList(m2pParams: FetchListBankApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", seqNo: "1"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle BankListResponsemodel */ }Key field — mobRegFormat: Each bank in the response carries a mobRegFormat field that tells you which format the bank supports for setting a UPI PIN:
"FORMAT1"→ set via Debit Card details"FORMAT2"→ set via ATM PIN"FORMAT3"→ set via Aadhaar OTP
Store this value when the user selects a bank. You will need it in Section 14.1 (Set UPI PIN) to pick the correct selectedFormat and to pass as the format field in VerifyPINRequest.
Fetch Credit Card / Credit Line Bank List
M2PUPISDKHL.shared.getM2PUpiService(
// isCreditLine = false → credit card banks
// isCreditLine = true → credit line banks
m2pFlowType: .getCreditCardOrLineBankList(m2pParams: FetchListBankApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", seqNo: "1"
), isCreditLine: false),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle BankListResponsemodel */ }UPI Number (Numeric ID) Management
Check Availability
Before creating a UPI number, check whether it is already taken. This call does not create anything — it only validates availability.
type: "NUMERICID"— user is creating a custom 8–9 digit numbertype: "MOBILE"— user wants to use their own mobile number as the UPI numbertxnTypemust always be"CHECK"for availability checks (not"CMREGISTRATION")
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkUPINumberAvailability(m2pParams: CheckUPINumberApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
number: "747377344", // user-entered 8–9 digit number; or mobile number when type is "MOBILE"
profileId: profileId,
seqNo: "1",
txnType: "CHECK", // ← always "CHECK" for availability check
type: "NUMERICID", // "NUMERICID" | "MOBILE"
vpa: primaryVpaId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let result = try? JSONDecoder().decode(CheckUPINumberApiReseponse.self, from: data)
else { return }
if result.result?.status == "AVAILABLE" {
// UPI number is free — proceed to createUPINumber (Section 12.2)
} else {
// Not available — prompt user for a different number or show suggestions
}
}Tip: You can also call this API while the user is typing to surface real-time suggestions. The .createUPINumber API (below) internally re-checks availability before registering, so the explicit check step is optional if your UX doesn't need it.
Create UPI Number
Registers the chosen number. txnType must be "CMREGISTRATION". On success the new number will appear in the user's profile. Always check for error codes (Section 12.5) after this call.
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .createUPINumber(m2pParams: CreateUPINumberApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
number: "747377344", // the number to register
opType: "ADD",
seqNo: "1",
profileId: profileId,
status: "ACTIVE",
txnType: "CMREGISTRATION",
type: "NUMERICID", // "NUMERICID" | "MOBILE"
vpa: primaryVpaId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let result = try? JSONDecoder().decode(CreateUPINumberApiReseponse.self, from: data)
else { return }
// On success: refresh user profile to surface the new UPI number
if let preVpa = result.result?.preVpa, !preVpa.isEmpty {
// Number already registered in another app — show Port confirmation (Section 12.4)
}
}UPI Number Action (Enable / Disable / De-register)
After a UPI number is created, the user can enable, disable, or permanently remove it. Use .upiNumberAction with opType: "MODIFY" and set the appropriate status.
| Action | status value |
|---|---|
| Enable the number | "ACTIVE" |
| Disable temporarily | "INACTIVE" |
| De-register permanently | "DEREGISTER" |
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .upiNumberAction(m2pParams: CreateUPINumberApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
number: existingUPINumber, // the registered UPI number to act on
opType: "MODIFY",
seqNo: "1",
profileId: profileId,
status: "INACTIVE", // "ACTIVE" | "INACTIVE" | "DEREGISTER"
txnType: "CMREGISTRATION",
type: "NUMERICID", // matches the type used when creating
vpa: primaryVpaId // the VPA the number is linked to
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CreateUPINumberApiReseponse */ }Port UPI Number
If the user already has their mobile number registered as a UPI number in another app (Google Pay, PhonePe, etc.), this flow transfers ownership to your app. You must show a confirmation prompt to the user before porting.
When to trigger:
exception.errorCode == "MM18"is returned from.createUPINumber- The
.createUPINumberresponse has a non-emptyresult.preVpa— this is the VPA where the number is currently registered; pass it back aspreVpain the port request
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .createUPINumber(m2pParams: CreateUPINumberApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
number: userMobileNumber, // the mobile number to port
opType: "UPDATE",
seqNo: "1",
profileId: profileId,
status: "ACTIVE",
txnType: "PORT",
type: "MOBILE",
vpa: primaryVpaId,
preVpa: existingVpa // value received in result.preVpa from the createUPINumber call
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CreateUPINumberApiReseponse — on success the number now belongs to your app */ }Error Codes for UPI Number Flows
exception.errorCode | Meaning | Action |
|---|---|---|
MM21 | Maximum UPI number limit reached | Show error: the user cannot create more UPI numbers |
MM18 | Mobile number already registered in another UPI app | Show Port confirmation → trigger Port flow (Section 12.4) |
result.preVpa non-empty | Same as MM18 — number is mapped elsewhere | Show Port confirmation → trigger Port flow (Section 12.4) |
CommonLibrary Setup
The CommonLibrary has two distinct uses of getListKeys:
| When | Purpose | Store? |
|---|---|---|
| Once per session (app launch / after 90 days) | Register the CL Library via m2pCLLibraryRegister | Yes – store as base64NPCIToken (persisted securely) |
Before every openCLLibrary call | Obtain a fresh XML key payload (xmlResponse) for each credential operation | Use immediately in CLCredentialsRequestModel |
Rule: You must call .getListKeys before each openCLLibrary invocation. The returned keyValue is passed as both xmlResponse and npciToken in CLCredentialsRequestModel. Do not reuse a token from a previous flow.
Session Registration Flow
The one-time registration flow at session start is:
- Get challenge string from the device SDK
- Call
getListKeysAPI to obtain the NPCI token (XML key payload) - Register the CL Library with the token
func setupCLLibrary() {
// Step 1 – Get challenge (use .initial for first-time; .rotate after 90 days)
let challenge = M2PUPISDKHL.shared.m2pGetChallange(
type: .initial,
appId: M2PUPISDKHL.shared.getAppId()
)
// Step 2 – Get List Keys (NPCI token)
let params = MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1",
txnType: "GET_TOKEN",
challenge: challenge,
type: "CHALLENGE",
subType: "INITIAL"
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue,
!npciToken.isEmpty
else { return }
// Step 3 – Register CL Library (once per session)
let isRegistered = M2PUPISDKHL.shared.m2pCLLibraryRegister(
token: npciToken,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: "91\(userMobileNumber)"
)
if isRegistered {
// Store base64NPCIToken securely for session registration only.
// NOTE: For each openCLLibrary call you MUST call getListKeys again
// to get a fresh xmlResponse/npciToken for that specific operation.
self.base64NPCIToken = npciToken
} else {
// Show error – CL Library registration failed
}
}
}Important – Two uses of getListKeys:
- Session registration (above): Call once at app launch (or after 90 days with
type: .rotate). Store the returnedkeyValueasbase64NPCIToken. - Per-flow token refresh: Before every single
openCLLibrarycall, you must call.getListKeysagain. The freshly returnedkeyValueis passed as bothxmlResponseandnpciTokeninCLCredentialsRequestModel. See Sections 14.1–14.14 for the exact pattern.
Open CL Library – All Flows
Key Rule: Call .getListKeys immediately before every openCLLibrary invocation. Do not cache the token across flows. The npciToken value (from result.key.key[0].keyValue) is always passed as both xmlResponse and npciToken in CLCredentialsRequestModel.
All CL Library calls share the same entry point:
M2PUPISDKHL.shared.openCLLibrary(requestModel: clCredentialsRequestModel) { code, error, data, clCallback in
// Handle response – see Section 14.15
}CLCredentialsRequestModel – Parameter Reference
| Parameter | Type | Description |
|---|---|---|
selectedFormat | UPIFormate | Screen type shown by CL Library |
paymentType | UpiPaymentType? | Set only for circle pay approve |
maskedAccnumber | String | Selected account masked number |
xmlResponse | String | NPCI token from getListKeys |
credType | String | CredType enum raw value |
npciToken | String | Same NPCI token (base64) |
primaryVpaId | String | User's active primary VPA ID |
appId | String | App bundle ID |
mobileNumber | String | 10-digit mobile (without country code) |
topVC | UIViewController | Presenting view controller |
clKeyCode | String | Always "NPCI" |
clLanguage | String | Language code, e.g. "en_US" |
clColor | String | Brand colour hex, e.g. "#CD1C5F" |
clBackgroundColor | String | Background hex, e.g. "#FFFFFF" |
payRequest | CLCredentialsPayRequestModel? | Required for pay/collect/mandate flows |
credsAllowed | [CredsAllowed] | From selected FetchAccountModel.credsAllowed |
isUPILite | Bool | true for all UPI Lite flows |
isAuthOffline | Bool | UPI Lite only |
enableUserAuth | Bool | UPI Lite only |
getDeviceDetails | Bool | UPI Lite only |
txnTimestamp | String | UPI Lite only – use getUPILiteTimestamp() |
accountRef | String? | UPI Lite – account.accRefNumber |
payerLiteAccNumber | String? | UPI Lite – account.lrnNumber |
CLCredentialsPayRequestModel – Parameter Reference
| Parameter | Description |
|---|---|
payerBankName | Selected account bank name |
circleTransactionId | Circle pay approve only |
transactionId | Transaction ID (from payModel or generated) |
txnAmount | Amount formatted as String(format: "%.2f", amount) |
payeeName | Payee name from verifyVPA response |
payerAddr | User's primary VPA ID |
payeeAddr | Payee VPA ID |
note | Description; use "Pay Description" if empty |
Set UPI PIN
When to show this option: Check mbeba in the account list response (Section 10.1). If mbeba == "N", the account has no UPI PIN — show Set UPI PIN. If mbeba == "Y", the PIN is already set — show Change UPI PIN (Section 14.2) instead.
Which format to use: Read mobRegFormat from the bank list response (Section 11.1). It tells you which set-PIN method the bank supports. Map it to the CL Library's selectedFormat:
mobRegFormat | selectedFormat | Method |
|---|---|---|
"FORMAT1" | .formate1 | Set via Debit Card details |
"FORMAT2" | .formate2 | Set via ATM PIN |
"FORMAT3" | .formate3 | Set via Aadhaar OTP |
The full Set UPI PIN flow has 5 steps:
Step 1 – Generate OTP
Call generateOTP first. This sends an OTP to the user's registered number, which the CL Library will later use for confirmation.
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .generateOTP(m2pParams: OTPAPIRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
format: selectedBank.mobRegFormat ?? "FORMAT1", // from bank list response
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
vpa: primaryVpaId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard response?.statusCode == "200" else { return }
// OTP sent successfully.
// For FORMAT1 and FORMAT2: proceed directly to Step 3 (getListKeys).
// For FORMAT3 (Aadhaar): do Step 2 first.
}Step 2 – Aadhaar OTP (FORMAT3 only)
If the user's bank uses FORMAT3, call aadhaarOTP and verify that an Aadhaar is linked to the account before proceeding.
// Only for FORMAT3:
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .aadhaarOTP(m2pParams: FetchAccountApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let accounts = try? JSONDecoder().decode(FetchAccountResponse.self, from: data)
else { return }
// Find the selected account in the response and check its `aeba` field
let aeba = accounts.result?.first(where: { $0.accRefNumber == selectedAccount.accRefNumber })?.aeba
guard aeba == "Y" else {
// Show error: "We can't find Aadhaar linked to your bank account. Please contact your bank."
return
}
// Aadhaar is linked — proceed to Step 3 (getListKeys)
}Step 3 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1",
txnType: "GET_TOKEN",
challenge: storedChallenge,
type: "CHALLENGE",
subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 4 – Open CL Library
let selectedFormat: UPIFormate
switch selectedBank.mobRegFormat {
case "FORMAT2": selectedFormat = .formate2
case "FORMAT3": selectedFormat = .formate3
default: selectedFormat = .formate1
}
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: selectedFormat,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.setMpin.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: nil,
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
if code == "2" {
// CL Library is waiting for OTP confirmation. We already triggered the OTP
// in Step 1 — just signal "done" back to unblock the library.
clCallback?(["status": "0"])
return
}
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 5 – Call Set UPI PIN API
let request = VerifyPINRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
format: selectedBank.mobRegFormat ?? "FORMAT1", // must match selectedFormat
txnId: clResult.transactionID,
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
credDetails: clResult.credDetails?.setPINCred
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .setUPIPIN(m2pParams: request),
m2pHeaders: headers,
m2pMethod: .post
) { response in
// On success: refresh the account list — mbeba will now be "Y" for this account
}
}
}CL Library code == "2" explained: When the CL Library returns code == "2", it is pausing to wait for an OTP to be entered. Because .generateOTP was already called in Step 1, the OTP has been sent. You must respond immediately with clCallback?(["status": "0"]) to tell the library "OTP confirmed — continue." If you don't call this callback, the library will time out.
Change UPI PIN
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .changeMpin,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.changeMpin.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: nil,
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call Change UPI PIN API
let request = ChangePINRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
txnId: clResult.transactionID,
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
creds: clResult.credDetails?.changePINCred?.creds,
newCreds: clResult.credDetails?.changePINCred?.newCreds
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .changeUPIPIN(m2pParams: request),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle VerifyPINResponseModel */ }
}
}Check Balance (with CL Library)
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .checkBanlance,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.reqBalEnq.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: nil,
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call Check Balance API
let request = BankBalanceRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
txnId: clResult.transactionID,
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
creds: clResult.credDetails?.checkBalanceCred
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkBalance(m2pParams: request),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle BankBalanceResponseModel */ }
}
}See Section 15 for checking balance without CL Library.
Pay (P2P / P2M)
Call verifyVPA first (Section 18) to populate payModel. Then:
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Build pay request and open CL Library
// Use transactionId from payModel if present, else generate a new one
let txnId = payModel.transactionId.map { $0.isEmpty ? nil : $0 } ??
M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let payRequest = CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "",
transactionId: txnId,
txnAmount: String(format: "%.2f", Double(payModel.amount ?? "0") ?? 0),
payeeName: payModel.payeeName,
payerAddr: primaryVpaId,
payeeAddr: payModel.payeeId,
note: (payModel.description ?? "").isEmpty ? "Pay Description" : payModel.description
)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .pay,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.pay.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: payRequest,
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call Make Payment API
payModel.paymentType = .scanAndPay // .payUPIID | .payAgain | .payCollectRequest
payModel.transactionId = clResult.transactionID
payModel.clLibraryAuthDetail = clResult
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 /* Handle UPIPaymentReqResponse */ }
}
}Collect (Approve Incoming Request)
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library (same structure as Pay, credType: CredType.collect)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .pay,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.collect.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: payRequest, // populated the same as Pay
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call Collect Auth API
let request = CollectAuthModelRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
txnId: clResult.transactionID,
action: CollectAuthAction.APPROVE.rawValue, // or DECLINE
payerRef: PayeePayerRef(accRef: selectedAccount.accRefNumber),
creds: clResult.credDetails?.collectCred
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .moneyRequestAction(m2pParams: request),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CollectAuthModel */ }
}
}Mandate Action / Create Mandate
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let txnId = payModel.transactionReferenceId ?? M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let amount = String(format: "%.2f", Double(payModel.amount ?? "0") ?? 0)
let desc = (payModel.description ?? "").isEmpty ? "Create Mandate" : payModel.description ?? "Create Mandate"
let payRequest = CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "",
transactionId: txnId,
txnAmount: amount,
payeeName: "User Name",
payerAddr: primaryVpaId,
payeeAddr: payModel.payeeId ?? "",
note: desc
)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .pay,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.mandate.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: payRequest,
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3a – For mandate ACTION (approve/pause/revoke):
let mandateRequest = MandateAPIRequest(
deviceInfo: buildDeviceInfo(),
profileId: profileId,
channelCode: "YOUR_CHANNEL_CODE",
action: MandateStatus.approve.rawValue, // APPROVE | PAUSE | UNPAUSE | REVOKE
txnRefId: payModel.transactionReferenceId,
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
txnId: clResult.transactionID,
credDetails: MandateCredDetail(cred: clResult.credDetails?.mandateCred)
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .mandateAction(m2pParams: mandateRequest),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle MandatesActionResponse */ }
// Step 3b – For CREATE MANDATE:
payModel.transactionId = clResult.transactionID
payModel.clLibraryAuthDetail = clResult
let params = UpiPaymentRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE", profileId: profileId, seqNo: "1", payModel: payModel
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .createmandate(m2pParams: params),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CreateMandateModel */ }
}
}UPI Lite – Binding
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let timestamp = M2PUPISDKHL.shared.getUPILiteTimestamp()
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let payRequest = CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "",
transactionId: txnId,
txnAmount: "0.00",
payeeName: "User Name",
payerAddr: primaryVpaId,
payeeAddr: primaryVpaId,
note: "Pay Description"
)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .binding,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.binding.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: payRequest,
credsAllowed: selectedAccount.credsAllowed ?? [],
isUPILite: true,
isAuthOffline: false,
enableUserAuth: false,
getDeviceDetails: false,
txnTimestamp: timestamp,
accountRef: selectedAccount.accRefNumber,
payerLiteAccNumber: selectedAccount.lrnNumber
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call UPI Lite List Key API
payModel.paymentType = .liteBinding
payModel.selectedAccountRef = selectedAccount.accRefNumber
payModel.transactionId = clResult.transactionID
payModel.clLibraryAuthDetail = clResult
let params = UPILiteListKeyRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, seqNo: "1", payModel: payModel
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .upiLiteListKey(m2pParams: params),
m2pHeaders: headers, m2pMethod: .post
) { [weak self] response in
guard let self = self,
let data = response?.result?.data(using: .utf8),
let keyResponse = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let liteXmlPayload = keyResponse.result?.xmlResponse, !liteXmlPayload.isEmpty
else { return }
// Step 4 – Register UPI Lite on the device using the lite XML payload
// liteXmlPayload is the "xmlResponse" field from the upiLiteListKey response —
// it is different from the npciToken used above; do not mix them.
M2PUPISDKHL.shared.isRegisterUPILite(
mobileNumber: "91\(userMobileNumber)",
accountRef: selectedAccount.accRefNumber ?? "",
xmlPayload: liteXmlPayload
) { isRegistered in
if isRegistered {
// UPI Lite is now bound on this device.
// Optionally read the current device-local Lite balance:
let balance = M2PUPISDKHL.shared.getLiteBalance(
mobileNumber: "91\(userMobileNumber)",
accountRef: selectedAccount.accRefNumber ?? ""
)
// Proceed to Top Up / Load Money flow (Section 14.8)
} else {
// Show error: "Unable to Register UPI Lite. Please try after sometime."
}
}
}
}
}UPI Lite – Top Up (Load Money)
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let timestamp = M2PUPISDKHL.shared.getUPILiteTimestamp()
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let payRequest = CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "",
transactionId: txnId,
txnAmount: String(format: "%.2f", Double(selectedAmount) ?? 0),
payeeName: "User Name",
payerAddr: primaryVpaId,
payeeAddr: primaryVpaId,
note: "Pay Description"
)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .liteTopUP,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.pay.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
clColor: "#CD1C5F",
clBackgroundColor: "#FFFFFF",
payRequest: payRequest,
credsAllowed: selectedAccount.credsAllowed ?? [],
isUPILite: true,
isAuthOffline: false,
enableUserAuth: false,
getDeviceDetails: false,
txnTimestamp: timestamp,
accountRef: selectedAccount.accRefNumber,
payerLiteAccNumber: selectedAccount.lrnNumber
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call Make Payment API
let purpose = (selectedAccount.liteEnabled == true)
? InitiationMode.liteTopUp.rawValue
: InitiationMode.liteEnableTopUp.rawValue
payModel.purpose = purpose
payModel.paymentType = .liteTopUP
payModel.selectedAccountRef = selectedAccount.accRefNumber
payModel.transactionId = clResult.transactionID
payModel.initiationMode = InitiationMode.defaultString.rawValue
payModel.amount = selectedAmount
payModel.payType = InitiationMode.vpa.rawValue
payModel.transactionType = InitiationMode.pay.rawValue
payModel.payeeName = "User Name"
payModel.payeeType = "PERSON"
payModel.payeeCode = "0000"
payModel.liteAccountReferenceNumber = selectedAccount.lrnNumber
payModel.clLibraryAuthDetail = clResult
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 /* Handle UPIPaymentReqResponse */ }
}
}UPI Lite – Pay
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let timestamp = M2PUPISDKHL.shared.getUPILiteTimestamp()
let txnId = payModel.transactionId.map { $0.isEmpty ? nil : $0 } ??
M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .litePay,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.pay.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
payRequest: CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "", transactionId: txnId,
txnAmount: String(format: "%.2f", Double(selectedAmount) ?? 0),
payeeName: payModel.payeeName, payerAddr: primaryVpaId,
payeeAddr: payModel.payeeId,
note: (payModel.description ?? "").isEmpty ? "Pay Description" : payModel.description
),
credsAllowed: selectedAccount.credsAllowed ?? [],
isUPILite: true,
isAuthOffline: false,
enableUserAuth: false,
getDeviceDetails: false,
txnTimestamp: timestamp,
accountRef: selectedAccount.accRefNumber,
payerLiteAccNumber: selectedAccount.lrnNumber
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call Make Payment API
payModel.paymentType = .litePay
payModel.purpose = InitiationMode.litePay.rawValue
payModel.selectedAccountRef = selectedAccount.accRefNumber
payModel.transactionId = clResult.transactionID
payModel.initiationMode = payModel.initiationMode // from verifyVPA response
payModel.amount = selectedAmount
payModel.payType = InitiationMode.vpa.rawValue
payModel.transactionType = InitiationMode.pay.rawValue
payModel.liteAccountReferenceNumber = selectedAccount.lrnNumber
payModel.clLibraryAuthDetail = clResult
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 /* Handle UPIPaymentReqResponse */ }
}
}UPI Lite – Disable
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let timestamp = M2PUPISDKHL.shared.getUPILiteTimestamp()
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .litedisable,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.pay.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
payRequest: CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "", transactionId: txnId,
txnAmount: "UPI LITE WHOLE BALANCE",
payeeName: "User Name", payerAddr: primaryVpaId,
payeeAddr: primaryVpaId, note: "Pay Description"
),
credsAllowed: selectedAccount.credsAllowed ?? [],
isUPILite: true,
isAuthOffline: false,
enableUserAuth: false,
getDeviceDetails: false,
txnTimestamp: timestamp,
accountRef: selectedAccount.accRefNumber,
payerLiteAccNumber: selectedAccount.lrnNumber
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call Make Payment API
payModel.purpose = InitiationMode.liteDisable.rawValue
payModel.paymentType = .litedisable
payModel.selectedAccountRef = selectedAccount.accRefNumber
payModel.transactionId = clResult.transactionID
payModel.initiationMode = InitiationMode.defaultString.rawValue
payModel.amount = "UPI LITE WHOLE BALANCE"
payModel.payType = InitiationMode.vpa.rawValue
payModel.transactionType = InitiationMode.pay.rawValue
payModel.payeeName = "User Name"
payModel.payeeType = "PERSON"
payModel.payeeCode = "0000"
payModel.liteAccountReferenceNumber = selectedAccount.lrnNumber
payModel.clLibraryAuthDetail = clResult
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 /* Handle UPIPaymentReqResponse */ }
}
}UPI Lite – Transfer Out
Same CL request as Disable (.litedisable). Call .getListKeys first (same pattern as 14.10), then after CL Library success callback set the purpose to transfer out:
// After getListKeys + openCLLibrary success callback:
payModel.purpose = "LITE_TRANSFER_OUT"
payModel.paymentType = .litedisable // or .liteTopUP
payModel.selectedAccountRef = selectedAccount.accRefNumber
payModel.transactionId = clResult.transactionID
payModel.amount = selectedAmount
payModel.payeeId = "USER_PRIMARY_VPA_ID"
payModel.liteAccountReferenceNumber = selectedAccount.lrnNumber
payModel.clLibraryAuthDetail = clResult
// ... (set payeeName, payeeType, payeeCode, accountType as needed)Circle Full Delegate
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let amount = String(format: "%.2f", Double(selectedAmount) ?? 0)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .delegatePay,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.mandate.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
payRequest: CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "", transactionId: txnId,
txnAmount: amount,
payeeName: verifyVPAResult.maskName, // from verifyVPA
payerAddr: primaryVpaId,
payeeAddr: verifyVPAResult.addr, // from verifyVPA
note: "Delegate Description"
),
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Register UPI Circle delegate
var addDelegateRequest = AddDelegateRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", profileId: profileId,
type: "ADD", subType: UPICircleTypeEnum.full.rawValue,
purpose: InitiationMode.reqDelgate.rawValue,
roleType: "AUTHORIZER",
delegateMobileNumber: delegateMobileNumber,
txnId: clResult.transactionID ?? "",
payerRef: PayerRefData(accRef: selectedAccount.accRefNumber),
payeeRef: [PayeeRefData(addrType: "VPA", id: verifyVPAResult.addr, name: verifyVPAResult.maskName)]
)
addDelegateRequest.endDate = endDateString // ddmmyyyy format
addDelegateRequest.startDate = M2PUPISDKHL.shared.getStringFromTheDate(date: Date(), formate: .ddmmyyyy)
addDelegateRequest.initiatedBy = "PRIMARY"
addDelegateRequest.credDetails = MandateCredDetail(cred: clResult.credDetails?.fullDelegateCred)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .registerUPICircle(m2pParams: addDelegateRequest),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle AddDelegateModel */ }
}
}Circle Pay Approve
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let txnId = payModel.transactionId ??
M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .pay,
paymentType: .circlePayApprove,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.pay.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
payRequest: CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "",
circleTransactionId: payModel.circleTransactionId,
transactionId: txnId,
txnAmount: String(format: "%.2f", Double(selectedAmount) ?? 0),
payeeName: payModel.payeeName,
payerAddr: primaryVpaId,
payeeAddr: payModel.payeeId,
note: (payModel.description ?? "").isEmpty ? "Pay Description" : payModel.description
),
credsAllowed: selectedAccount.credsAllowed ?? [],
accountRef: selectedAccount.accRefNumber
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Approve / Reject Circle Payment
payModel.paymentType = .circlePayApprove // or .circlePayReject
payModel.initiationMode = InitiationMode.defaultString.rawValue
payModel.purpose = InitiationMode.reqDelgate.rawValue
payModel.clLibraryAuthDetail = clResult
let params = UpiPaymentRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE", profileId: profileId, seqNo: "1", payModel: payModel
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .approveRejectCirclePayment(m2pParams: params),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle UPIPaymentReqResponse */ }
}
}International QR Activation
// Step 1 – Refresh NPCI token
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getListKeys(m2pParams: MerchantKeysRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", txnType: "GET_TOKEN", challenge: storedChallenge,
type: "CHALLENGE", subType: "INITIAL"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let decoded = try? JSONDecoder().decode(MerchantKeysResponseModel.self, from: data),
let npciToken = decoded.result?.key?.key?.first?.keyValue, !npciToken.isEmpty
else { return }
// Step 2 – Open CL Library
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let clRequestModel = CLCredentialsRequestModel(
selectedFormat: .checkBanlance,
maskedAccnumber: selectedAccount.maskedAccnumber,
xmlResponse: npciToken,
credType: CredType.reqBalEnq.rawValue,
npciToken: npciToken,
primaryVpaId: primaryVpaId,
appId: M2PUPISDKHL.shared.getAppId(),
mobileNumber: userMobileNumber,
topVC: self,
clKeyCode: "NPCI",
clLanguage: "en_US",
payRequest: CLCredentialsPayRequestModel(
payerBankName: selectedAccount.name ?? "", transactionId: txnId,
payeeName: payModel.payeeName, payerAddr: primaryVpaId,
payeeAddr: payModel.payeeId,
note: (payModel.description ?? "").isEmpty ? "Pay Description" : payModel.description
),
credsAllowed: selectedAccount.credsAllowed ?? []
)
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
guard (code == "1" || code == "3"), let clResult = data else { return }
// Step 3 – Call International QR Action API
let startDate = M2PUPISDKHL.shared.getStringFromTheDate(date: Date(), formate: .yyyyMMdd)
let nextThreeMonths = M2PUPISDKHL.shared.getfurtueDateFromCurrentDate(month: 3, day: 1)
let endDate = M2PUPISDKHL.shared.getStringFromTheDate(date: nextThreeMonths ?? Date(), formate: .yyyyMMdd)
let params = InternationalQRRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
txnId: clResult.transactionID ?? "",
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
action: InternationalQREnum.activation.rawValue,
endDate: endDate,
startDate: startDate,
txnType: InitiationMode.international.rawValue,
creds: clResult.credDetails?.checkBalanceCred
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .internationalQRAction(m2pParams: params),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle InternationalQRResponseModel */ }
}
}CL Library Callback Handling
The openCLLibrary completion handler returns a code value:
| Code | Meaning | Action |
|---|---|---|
"1" | Success – credential blocks received | Proceed with the corresponding SDK API call |
"3" | Success (alternate success code) | Same as code "1" |
"2" | OTP required | Call clCallback?(["status": "0"]) to confirm OTP received |
"M2P-002" | credsAllowed is empty | Show error; ensure account credsAllowed is passed correctly |
| Other | Error | Display error to user |
M2PUPISDKHL.shared.openCLLibrary(requestModel: clRequestModel) { code, error, data, clCallback in
if code == "1" || code == "3" {
// Success – use data (ClAuthorizationResultModel) to make your API call
let clResult = data
// clResult?.transactionID
// clResult?.credDetails?.setPINCred – for set PIN
// clResult?.credDetails?.changePINCred – for change PIN
// clResult?.credDetails?.checkBalanceCred – for balance check
// clResult?.credDetails?.payCred – for pay
// clResult?.credDetails?.collectCred – for collect
// clResult?.credDetails?.mandateCred – for mandate
// clResult?.credDetails?.fullDelegateCred – for circle delegate
} else if code == "2" {
// OTP trigger – notify CL Library after receiving OTP
clCallback?(["status": "0"]) // "0" = success, "-1" = failure
} else if code == "M2P-002" {
// credsAllowed array was empty – pass the correct account's credsAllowed
} else {
// Error
print("CL Library error:", error ?? "Unknown error")
}
}Balance Check (without CL Library)
let request = BankBalanceRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
payerRef: PayerRef(accRef: selectedAccount.accRefNumber)
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkBalance(m2pParams: request),
m2pHeaders: headers,
m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let model = try? JSONDecoder().decode(BankBalanceResponseModel.self, from: data)
else { return }
let balance = model.result?.balance ?? "--"
}OTP Generation & Aadhaar OTP
Note: generateOTP and aadhaarOTP are used as Step 1 and Step 2 of the Set UPI PIN flow documented in Section 14.1. If you are implementing Set UPI PIN, refer to that section for the full sequence (OTP → Aadhaar OTP if FORMAT3 → getListKeys → CL Library → setUPIPIN). The snippets below show the standalone API shapes for reference.
Generate OTP
// format: from selectedBank.mobRegFormat — "FORMAT1" | "FORMAT2" | "FORMAT3"
// payerRef: selected account reference number
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .generateOTP(m2pParams: OTPAPIRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
format: "FORMAT1",
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
vpa: primaryVpaId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle OTPResponseModel — proceed to CL Library on success */ }Aadhaar OTP (FORMAT3 only)
// Use only when mobRegFormat is "FORMAT3".
// After success, check aeba == "Y" in the returned account to confirm Aadhaar linkage.
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .aadhaarOTP(m2pParams: FetchAccountApiRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle FetchAccountResponse — validate aeba before continuing */ }QR Code – Scan & Pay
Parse a Scanned QR String
let payModel = M2PUPISDKHL.shared.validateQRData(scannedText: rawQRString)
if payModel.isQRExpired == true {
// Show "QR has expired"
} else if payModel.isInvalidQR == true {
// Show "Invalid QR"
} else if payModel.qrCodeType == QRType.internationalQR.rawValue {
// Show "International QR not supported" or handle via Section 30
} else if payModel.qrCodeType == QRType.mandateQR.rawValue {
guard payModel.isValidStartDate == true, payModel.isValidEndDate == true else {
// Show "Mandate QR expired"
return
}
// Proceed with create mandate (Section 24.1 + Section 14.6)
} else if let upiId = payModel.scannedUpiId, !upiId.isEmpty {
// Validate UPI ID first (Section 18)
} else {
// Show "Invalid QR"
}Validate QR (against server)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .validateQr(m2pParams: ValidateQRRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
// pass scanned QR data fields
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle ValidateQRModel */ }Verify a Signed QR (optional merchant security)
M2PUPISDKHL.shared.verifySignedQR(
originalText: payModel.originalText ?? "",
publicKey: payModel.merchantKey ?? ""
) { isValid in
if isValid {
// Proceed to payment
} else {
// Show "Invalid QR signature"
}
}Generate a QR Image
let qrConfig = ConstructQR(
upiId: "yourvpa@m2p",
payeeName: "Store Name",
amount: "500.00",
description: "Order #1234",
txnRef: M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
)
if let qrImage = M2PUPISDKHL.shared.constructQR(constructQR: qrConfig) {
imageView.image = qrImage
}Verify VPA (Validate Payee)
Always call before making any payment. Populates UpiTransferPayModel with payee details.
let params = RecentTransactionsRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1",
profileId: profileId,
type: payModel.payType, // InitiationMode.vpa.rawValue or .account.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 }
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
// Now open CL Library for payment (Section 14.4) or proceed directly
}Make Payment (P2P / P2M)
Direct payment without CL Library (pre-authenticated credentials):
payModel.amount = "100.00"
payModel.description = "Payment"
payModel.paymentType = .scanAndPay
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 ?? ""
// Check status after payment (Section 23)
}For CL Library-authenticated payments, follow Section 14.4 which calls this internally.
Send Money Request (Collect)
payModel.transactionType = "collect"
payModel.amount = "500.00"
payModel.description = "Requesting payment"
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .sendMoneyRequest(m2pParams: UpiPaymentRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE", profileId: profileId,
seqNo: "1", expiryDate: "2026-04-30T23:59:59", payModel: payModel
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle UPIPaymentReqResponse */ }Money Request Actions
List Collect Requests
// isSender = true → requests you SENT
// isSender = false → requests you RECEIVED
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .moneyRequestList(isSender: false, m2pParams: RecentPaymentListApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle RecentPaymentListResponse */ }Approve / Decline (authenticated via CL Library)
See Section 14.5. For raw approval without CL Library:
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .moneyRequestAction(m2pParams: CollectAuthModelRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, seqNo: "1",
txnId: incomingTxnId, action: "APPROVE"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CollectAuthModel */ }Transaction History
// offset and pageNo used for pagination
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .userTransactionList(offset: 0, pageNo: 1, m2pParams: TransactionHistoryApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
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; increment pageNo for next page
}Recent Transactions
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .recentTransactionList(m2pParams: RecentTransactionsRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
seqNo: "1", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle RecentTransactionsUPIList */ }Check Transaction Status
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkTransactionStatus(m2pParams: CheckStatusRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, seqNo: "1",
orgRrn: originalRrn, extTxnId: txnId, txnType: "PAYMENT", subType: "PAY"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard response?.statusCode == "200",
let data = response?.result?.data(using: .utf8),
let model = try? JSONDecoder().decode(CheckStatusResModel.self, from: data)
else { return }
// model.result?.status → "SUCCESS" | "FAILURE" | "PENDING"
}Mandate Management
Create Mandate (see Section 14.6 for CL-authenticated creation)
Fetch Mandate List
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getMandateList(offset: 0, pageNo: 1, m2pParams: MyMandatesRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle MyMandatesFetch */ }Mandate Actions
// action: APPROVE | REVOKE | PAUSE | UNPAUSE | MODIFY_APPROVE
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .mandateAction(m2pParams: MandateAPIRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, action: "REVOKE", txnRefId: mandateTxnRefId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle MandatesActionResponse */ }Mandate Status Grouping Helper
let pending = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .pending)
let live = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .live)
let completed = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .completed)
let modify = M2PUPISDKHL.shared.getMandateStatus(mandateStatusTitle: .modify)Dispute Management
List Disputes
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .disputeList(offset: 0, pageNo: 1, m2pParams: DisputeListApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle DisputeListResponseModel */ }Raise Dispute
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .raiseDispute(m2pParams: CreateDisputeApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, txnId: disputedTxnId, reason: "GOODS_NOT_RECEIVED"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CreateDisputeResponseModel */ }Beneficiary Management
View Beneficiaries
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .viewBeneficiary(m2pParams: ViewBeneficiaryRequestModel(entityId: profileId)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let list = try? JSONDecoder().decode(ViewBeneficiaryResponse.self, from: data)
else { return }
// list.result is [ViewBeneficiaryResult]
}Add Beneficiary
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .addBeneficiary(m2pParams: AddBeneficiaryRequest(
entityId: profileId, accountType: "SAVINGS",
beneficiaryAccountNo: "1234567890",
beneficiaryId: UUID().uuidString,
beneficiaryName: "John Doe",
ifsc: "HDFC0001234",
transferMode: ["IMPS", "NEFT"],
nickName: "John"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle ValidateUPIModel */ }Block / Unblock User
// action: "BLOCK" | "UNBLOCK"
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .blockUnblockUser(m2pParams: CollectAuthModelRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, seqNo: "1", action: "BLOCK", vpaId: targetVpaId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CollectAuthModel */ }List Blocked Users
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getBlockedUserList(m2pParams: BlockedUserApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle BlockedUserResponseModel */ }UPI Circle (Delegate)
Register (Add Delegate) – Section 14.12 covers authentication via CL Library
Get Circle List
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .getUPICircleList(m2pParams: DelegateDetailsRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle DelegateDetailsModel */ }Circle Action (Approve / Update / Upgrade / Revoke)
// actionEnum: .approve | .update | .upgrade | .revoke
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .upiCircleAction(m2pParams: DelegateActionRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, action: "APPROVE",
roleType: "DELEGATE",
delegateRefNo: delegateRefNo,
txnId: txnId
), actionEnum: .approve),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle DelegateActionModel */ }Request Circle Payment (Delegate sends request)
payModel.paymentType = .circlePayRequest
payModel.initiationMode = InitiationMode.defaultString.rawValue
payModel.purpose = InitiationMode.reqDelgate.rawValue
payModel.selectedAccountRef = "AUTHORIZER_ACCOUNT_REF"
payModel.circleType = .full // or .partial
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .requestCirclePayment(m2pParams: UpiPaymentRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
businessType: "YOUR_BUSINESS_TYPE", profileId: profileId, seqNo: "1", payModel: payModel
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle CirclePayResponseModel */ }Approve / Reject Circle Payment – Section 14.13
Circle Delegate Request List
// isSender = true → requests you sent; false → requests you received
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .circleDelegateRequestList(isSender: false, offset: 0, pageNo: 1,
m2pParams: RecentPaymentListApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)
),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle RecentPaymentListResponse */ }UPI Lite – Device-level Utilities
These are synchronous device-local calls that do not hit the network.
Is UPI Lite Supported
let isSupported = M2PUPISDKHL.shared.isUpiLiteSupported()Is UPI Lite Bound for Account
let boundResponse = M2PUPISDKHL.shared.isUpiLiteBoundForMobileNumber(
mobileNumber: "91\(userMobileNumber)",
accountRef: selectedAccount.accRefNumber ?? ""
)
switch boundResponse?.status {
case UpiLiteBound.bound.rawValue:
if boundResponse?.syncRequired == "true" {
// Sync required — the device state is out of sync with the server.
// Call checkTransactionStatus to re-sync before proceeding to Load Money.
// Use lrnNumber and accRefNumber from the account where liteEnabled == true.
let liteAccount = profileAccounts.first(where: { $0.liteEnabled == true })
let extTxnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .checkTransactionStatus(m2pParams: CheckStatusRequestModel(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
extTxnId: extTxnId,
lrn: liteAccount?.lrnNumber,
accountRefNumber: liteAccount?.accRefNumber
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
// Sync complete — proceed to Load Money (Section 14.8)
}
} else {
// Directly proceed to Load Money (Section 14.8)
}
case UpiLiteBound.noBind.rawValue:
// Proceed to Binding flow (Section 14.7)
default:
break
}Register UPI Lite (after binding key list)
M2PUPISDKHL.shared.isRegisterUPILite(
mobileNumber: "91\(userMobileNumber)",
accountRef: selectedAccount.accRefNumber ?? "",
xmlPayload: npciToken // keyValue from getListKeys (same npciToken)
) { isRegistered in
if isRegistered {
let balance = M2PUPISDKHL.shared.getLiteBalance(
mobileNumber: "91\(userMobileNumber)",
accountRef: selectedAccount.accRefNumber ?? ""
)
// Continue with Load Money flow
}
}Get UPI Lite Balance (device-local)
let liteBalance = M2PUPISDKHL.shared.getLiteBalance(
mobileNumber: "91\(userMobileNumber)",
accountRef: selectedAccount.accRefNumber ?? ""
)Unbind UPI Lite
let isUnbound = M2PUPISDKHL.shared.isUnBoundUPILite(
mobileNumber: "91\(userMobileNumber)",
accountRef: selectedAccount.accRefNumber ?? ""
)Get UPI Lite Timestamp (for CL Library requests)
let timestamp = M2PUPISDKHL.shared.getUPILiteTimestamp()
// Use as txnTimestamp in CLCredentialsRequestModel for all UPI Lite flowsInternational QR Management
// isActivation = true → activate international QR for this account
// isActivation = false → deactivate
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
let threeMonths = M2PUPISDKHL.shared.getfurtueDateFromCurrentDate(month: 3, day: 1)
let startDate = isActivation
? M2PUPISDKHL.shared.getStringFromTheDate(date: Date(), formate: .yyyyMMdd)
: selectedAccount.international?.valStart ?? ""
let endDate = isActivation
? M2PUPISDKHL.shared.getStringFromTheDate(date: threeMonths ?? Date(), formate: .yyyyMMdd)
: selectedAccount.international?.valEnd ?? ""
let params = InternationalQRRequest(
deviceInfo: buildDeviceInfo(),
channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId,
seqNo: "1",
txnId: txnId,
payerRef: PayerRef(accRef: selectedAccount.accRefNumber),
action: isActivation ? InternationalQREnum.activation.rawValue : InternationalQREnum.deactivation.rawValue,
endDate: endDate,
startDate: startDate,
txnType: InitiationMode.international.rawValue
// creds: set from CL Library response (Section 14.14) for activation
)
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .internationalQRAction(m2pParams: params),
m2pHeaders: headers,
m2pMethod: .post
) { response in /* Handle InternationalQRResponseModel */ }UPI Version Check (UPI Lite Eligibility)
Call this before showing the UPI Lite enable option. It checks whether the selected bank account supports UPI Lite on the device.
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .upiVersionCheck(m2pParams: UPIEnableVersionRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
guard let data = response?.result?.data(using: .utf8),
let result = try? JSONDecoder().decode(UPIEnableVersionResponse.self, from: data)
else { return }
// Check if the selected account's bank IFSC supports UPI Lite.
// The response is a list — find an entry where:
// • no == "2.98" AND
// • description == "LITE_VERSION"
// for the IFSC of the account the user wants to enable UPI Lite on.
let isLiteSupported = result.result?.contains {
$0.no == "2.98" && $0.description == "LITE_VERSION"
} ?? false
if isLiteSupported {
// Bank supports UPI Lite — allow the user to proceed with Binding (Section 14.7)
// Note: If multiple banks are eligible, show a bank selection screen first.
// Once UPI Lite is enabled, the user cannot switch the linked bank.
} else {
// Show error: "Your bank doesn't support UPI Lite."
}
}Multiple eligible banks: If more than one of the user's accounts has a LITE_VERSION-supporting bank, present a bank selection screen before enabling UPI Lite. The selection is permanent — after binding, the user cannot change the UPI Lite bank without disabling and re-enabling.
Validate QR
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .validateQr(m2pParams: ValidateQRRequestParams(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle ValidateQRModel */ }Circle Check Status
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .circleCheckStatus(m2pParams: CircleCheckStatusRequestModel(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE",
profileId: profileId, seqNo: "1"
)),
m2pHeaders: headers, m2pMethod: .post
) { response in /* Handle UPIPaymentReqResponse */ }Profile Deregistration
Permanently removes the user's UPI profile. Irreversible.
M2PUPISDKHL.shared.getM2PUpiService(
m2pFlowType: .deRegisterUserProfile(m2pParams: VPADeregisterApiRequest(
deviceInfo: buildDeviceInfo(), channelCode: "YOUR_CHANNEL_CODE", profileId: profileId
)),
m2pHeaders: headers, m2pMethod: .post
) { response in
// Clear local session data and profileId on success
}Utility Functions
SDK & Device Info
let sdkVersion = M2PUPISDKHL.shared.getSDKVersion()
let appId = M2PUPISDKHL.shared.getAppId()
let osDetails = M2PUPISDKHL.shared.getOSDetails() // "ios 17.2"
let deviceId = M2PUPISDKHL.shared.getDeviceId() // Vendor UUID, no dashes
let ipAddress = M2PUPISDKHL.shared.getIPAddress()Generate Transaction ID
// ipCode comes from GetProfileResponse.result.ipCode
let txnId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
// Format: <ipCode><UUID without dashes>Date Utilities
// Future date
let futureDate = M2PUPISDKHL.shared.getfurtueDateFromCurrentDate(month: 3, day: 1)
// Date → String
let dateStr = M2PUPISDKHL.shared.getStringFromTheDate(date: futureDate, formate: .ddMMyyyy)
// String → Date
let date = M2PUPISDKHL.shared.getDateFromTheString(dateString: "31/12/2026", formate: .ddMMyyyy)
// Check expiry
let isValid = M2PUPISDKHL.shared.isValidationWithCurrentDate(
expireDateString: "31/12/2026", dateFormat: .ddMMyyyy
)Stop API Timer
M2PUPISDKHL.shared.stopApiTimer()
// Call when dismissing a payment screen mid-flightError Handling Reference
public struct SDKResponseModel {
public let statusCode: String? // HTTP status or SDK error code
public let result: String? // JSON string (decode per flow)
public let error: String? // Human-readable description
}SDK Internal Error Codes
| Code | Trigger | Message |
|---|---|---|
M2P-000 | SSL pinning failed | SSL Pinning Failed |
M2P-001 | Jailbroken / rooted device | Widget cannot be invoked on this device |
M2P-002 | credsAllowed empty in CL Library | credAllowed is Empty |
M2P-003 | No internet | No Internet connection |
M2P-004 | Malformed URL | URL Not Valid |
M2P-005 | Request validation failed | Validation Failed |
M2P-006 | VPN active | Please turn VPN off to proceed |
M2P-007 | App not from App Store | App might not be from App Store |
M2P-500 | Server error | Internal Server Error |
Recommended Handler
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-002": showAlert("Configuration Error", 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 success response
}Best Practices
-
Key exchange first – run before every session. Re-run if the app was backgrounded for an extended period.
-
CL Library setup once per session –
setupCLLibrary()(Section 13) only needs to run once. Call it after key exchange completes. The registration stays valid for the session. -
Always pass correct
credsAllowed– passaccount.credsAllowedfrom the selectedFetchAccountModel. EmptycredsAllowedreturns errorM2P-002. -
Use
getTransactionID(ipCode:)for every transaction – never reuse a transaction ID. TheipCodecomes from theGetProfileResponse. -
Handle payment PENDING state – a payment response of
PENDINGmeans the transaction is in flight. Always callcheckTransactionStatus(Section 23) before showing a failure. -
UPI Lite: always check bound status first via
isUpiLiteBoundForMobileNumber(Section 29.2) before deciding whether to show Binding or Top Up flow. -
Pagination – increment
pageNoby 1 on each load-more. Stop fetching when the result count is less than your page size. -
Stop timer on dismiss – call
stopApiTimer()when a payment or CL Library screen is dismissed mid-flight. -
Production security –
enableRootedDevice: trueis mandatory in release builds. Never disable SSL pinning in production. -
Never log credentials – do not log
npciToken,credDetails, private keys, or full payment responses to the console in production.
UpiTransferPayModel – Field Reference
UpiTransferPayModel (also referred to as payModel) carries all the metadata for a payment or money request. Most fields have sensible defaults, but several must be set correctly depending on the scenario. Below is the complete field guide.
Default values (apply to all flows unless otherwise noted)
| Field | Default | When to change |
|---|---|---|
purpose | "DEFAULT" | Override with value from QR scan for Scan & Pay. For UPI Lite first-time load use "LITE_ENABLE_TOPUP"; subsequent loads use "LITE_TOPUP". |
transactionType | "PAY" | Set to "COLLECT" when sending or paying a collect request. |
payType | "VPA" | Set to "ACCOUNT" when paying via bank account number (IFSC transfer). |
initiationMode | "DEFAULT" | Override with the value from the QR scan model for Scan & Pay. |
paymentType | .default | See the table below. |
paymentType values
| Scenario | paymentType |
|---|---|
| Normal P2P / P2M payment | .default |
| Scan & Pay | .scanAndPay — set automatically by validateQRData(scannedText:) |
| Pay an incoming collect request | .payCollectRequest |
| Send a collect (money request) | .sendCollectRequest |
| Approve a mandate | .approveMandate |
| Create a mandate | .createMandate |
| Pause a mandate | .pauseMandate |
| Unpause a mandate | .unpauseMandate |
| Circle Pay — delegate approves | .circlePayApprove |
| Circle Pay — delegate rejects | .circlePayReject |
Payee fields — populated from verifyVPA response
payModel.payeeName = verifyVPAResult.maskName // result.maskName
payModel.payeeType = verifyVPAResult.type // result.type
payModel.payeeId = verifyVPAResult.addr // result.addr (the UPI ID)
payModel.payeeCode = verifyVPAResult.code // result.code
payModel.accountType = verifyVPAResult.accType // result.accType
payModel.ifscCode = verifyVPAResult.ifsc // result.ifsc
payModel.cmid = verifyVPAResult.cmid // result.cmid
payModel.isVerifiedMerchant = verifyVPAResult.merchant?.verifiedMerchantAmount & description
payModel.amount = userEnteredAmount // or amount from QR scan model
payModel.description = userEnteredNote // or description from QR scan model
// isAmountEditable (default true)
// Set false when QR provides a fixed amount the user cannot change
payModel.isAmountEditable = false
// isDescriptionEditable (default true)
// Set false when QR provides a fixed description
payModel.isDescriptionEditable = false
// minimumAmount — only from Scan & Pay model
// Enforce: userAmount >= minimumAmount AND userAmount <= scannedAmount
payModel.minimumAmount = scanModel.minimumAmountTransaction ID
// Generate a fresh txnId for every transaction — never reuse a previous one.
// ipCode comes from GetProfileResponse.result.ipCode
payModel.transactionId = M2PUPISDKHL.shared.getTransactionID(ipCode: ipCode)
// For pay-a-collect-request flows, set this to the custRef from the received request:
payModel.transactionReferenceId = incomingRequest.custRefSelected account
payModel.selectedAccount = selectedAccount.accRefNumber
payModel.liteAccountReferenceNumber = selectedAccount.lrnNumber // UPI Lite flows onlyMandate QR special handling
if payModel.qrCodeType == QRType.mandateQR.rawValue {
guard payModel.isValidStartDate == true, payModel.isValidEndDate == true else {
// QR mandate dates are invalid — show error and stop
return
}
// Proceed with Create Mandate flow (Section 14.6)
}Merchant transaction restrictions
You must enforce these rules for unverified offline merchants to comply with NPCI guidelines:
let isOfflineMerchant = payModel.merchantType == "OFFLINE"
let isUnverified = payModel.isVerifiedMerchant == false
// Rule 1: Block INTENT / SECURE_INTENT initiationMode for unverified offline merchants
if isUnverified && isOfflineMerchant {
let mode = payModel.initiationMode ?? ""
if mode == "04" || mode == "4" || mode == "05" || mode == "5" {
showAlert("Payment not allowed for this merchant type.")
return
}
}
// Rule 2: Cap payments at ₹2,000 for unverified offline merchants
if isUnverified && isOfflineMerchant {
let amount = Double(payModel.amount ?? "0") ?? 0
if amount > 2000 {
showAlert("Maximum payment limit is ₹2,000 for this merchant.")
return
}
}QR expired / invalid checks: Always evaluate payModel.isQRExpired == true and payModel.isInvalidQR == true immediately after parsing a QR string. Stop the flow and show an appropriate error for either condition.
Last updated: April 2, 2026 | SDK Version: 1.0.0
PPI iOS SDK Integration
M2P Headless UPI SDK for iOS — complete technical integration guide for PPI partners covering onboarding, payments, mandates, disputes, and security.
Account Fetch Request POST
Initiates an account fetch request to retrieve bank account details from NPCI. This is an asynchronous API that sends a request to NPCI and returns a callbackRef. Use accountFetchRes with the callbackRef to get the actual account details. Used during onboarding to discover the users bank accounts.
