m2pfintech
SDK IntegrationPPI SDK

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

  1. Introduction
  2. Prerequisites
  3. Installation
  4. SDK Initialization
  5. Device Info Helper
  6. Security Setup – Key Exchange & Encryption
  7. SIM Binding
  8. User Registration & Profile
  9. VPA (UPI ID) Management
  10. Account Management
  11. UPI Number (Numeric ID) Management
  12. Balance Check
  13. QR Code – Scan & Pay
  14. Verify VPA (Validate Payee)
  15. Make Payment (P2P / P2M)
  16. Send Money Request (Collect)
  17. Money Request Actions
  18. Transaction History
  19. Check Transaction Status
  20. Mandate Management
  21. Dispute Management
  22. Beneficiary Management
  23. Block / Unblock User
  24. Profile Deregistration
  25. Utility Functions
  26. Error Handling Reference
  27. 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

CategoryFeatures
OnboardingSIM binding, profile registration, VPA creation, account linking
PaymentsP2P, P2M, QR Scan & Pay, Collect, Mandates
ManagementBalance check, transaction history, status check, disputes
SecurityJailbreak & root detection, SSL pinning, end-to-end encryption, VPN check
UtilitiesQR generation & parsing, date helpers, device info, IP address

Prerequisites

RequirementMinimum Version
Xcode15.0
iOS Deployment Target17.0
Swift5.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

  1. Obtain M2PUPIHLSDK.framework from M2P (or build it from source by opening M2PUPIHLSDK.xcodeproj and building the M2PUPIHLSDK scheme).

  2. Drag M2PUPIHLSDK.framework into your Xcode project navigator.

    • Check "Copy items if needed".
  3. In your app target → GeneralFrameworks, Libraries, and Embedded Content, set M2PUPIHLSDK.framework to Embed & Sign.

  4. Add the following Swift Package dependencies to your project via File → Add Package Dependencies:

    PackageURLVersion Rule
    CryptoSwifthttps://github.com/krzyzanowskim/CryptoSwiftExact – 1.5.1
    SwCrypthttps://github.com/soyersoyer/SwCryptUp to Next Major – 5.0.0
    IOSSecuritySuitehttps://github.com/securing/IOSSecuritySuite.gitUp to Next Major – 2.1.0
  5. 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`

ParameterDescription
baseUrlFull base URL including trailing slash
endPointAPI endpoint prefix (can be empty "")
sslPublicKeyBase64-encoded server SSL public key for certificate pinning
enableRootedDevicetrue = 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

FieldMax length
appId75
mobile12
deviceId45
location40
geoCode15
ipAddress45
telecom15

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; returns GetProfileResponse.
  • true – calls register only; returns GetProfileIDResponseModels.

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

CodeTriggerMessage
M2P-000SSL pinning mismatchSSL Pinning Failed
M2P-001Jailbroken / rooted deviceWidget cannot be invoked on this device
M2P-003No internetNo Internet connection
M2P-004Malformed URLURL Not Valid
M2P-005Request validation failedValidation Failed
M2P-006VPN detectedPlease turn VPN off to proceed
M2P-007App not from App StoreApp might not be from App Store
M2P-500Server errorOops! Internal Server Error
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

  1. 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.

  2. Production security — always set enableRootedDevice: true in production builds. Never disable SSL pinning in a release build.

  3. Reuse CommonDeviceInfo — build it once per session. Avoid creating it inside loops.

  4. Unique transaction IDs — always use getTransactionID(ipCode:) to generate seqNo / txnId values. Never reuse an ID.

  5. Handle pending payment states — a payment can return PENDING. Always call checkTransactionStatus on timeout before showing a failure to the user.

  6. Pagination — use offset and pageNo consistently for transaction history, mandate lists, and dispute lists. Load the next page only when the user scrolls to the bottom.

  7. Stop timer on view dismiss — call stopApiTimer() when your payment screen is dismissed mid-flight to prevent stale callbacks.

  8. 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 enableRootedDevice is true.

  9. Validate before calling — verify that profileId is non-empty before any profile-scoped call. Missing required fields return M2P-005.

  10. 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

On this page

Table of Contents
Introduction
Prerequisites
Required Info.plist Permissions
Installation
Steps
SDK Initialization
Device Info Helper
Security Setup – Key Exchange & Encryption
Step 1 – Generate a local key pair
Step 2 – Exchange public keys with the server
SIM Binding
Initiate SIM Binding Request
Check SIM Binding Status
User Registration & Profile
Register Profile
Get Profile
Update User Profile
VPA (UPI ID) Management
Check VPA Availability
Create VPA
Set Primary VPA
Delete VPA
Account Management
Fetch Account List
Add Account
Set Primary Account
Remove Account
Manage User Account List
UPI Number (Numeric ID) Management
Check UPI Number Availability
Create UPI Number
UPI Number Action (Modify / Remove)
Balance Check
QR Code – Scan & Pay
Parse a Scanned QR String
Verify a Signed QR (Optional Security Step)
Generate a QR Code Image
Verify VPA (Validate Payee)
Make Payment (P2P / P2M)
Send Money Request (Collect)
Money Request Actions
List Incoming / Outgoing Collect Requests
Approve or Decline a Collect Request
Transaction History
Recent Transaction List
Check Transaction Status
Mandate Management
Create Mandate
Fetch Mandate List
Mandate Actions
Mandate Status Grouping Helper
Dispute Management
List Disputes
Raise a Dispute
Beneficiary Management
View Beneficiaries
Add Beneficiary
Block / Unblock User
Fetch Blocked User List
Profile Deregistration
Utility Functions
SDK & Device Information
Generate a Unique Transaction ID
Date Utilities
Stop API Timer
Error Handling Reference
SDK Internal Error Codes
Recommended Error Handling Pattern
Best Practices