Customer documentation · Integration overview · Questions: contactus@diimeai.com

NonaShield SDK Integration Guide

For NonaShield SaaS customers. Your app talks to NonaShield at https://api.diimeai.com — use exactly this address on every platform.

What you need from NonaShield

ItemUsed in
SDK key (shown below as YOUR_SDK_KEY)Step "Start the SDK" on every platform
Android: android-sdk-release.aarAndroid steps
iOS: PayShieldKMP.xcframeworkiOS steps
React Native: the NonaShield React Native packageReact Native steps

The integration is the same four calls on every platform: start → register the device → connect your login → (optional) check before a payment or any other API. No amount or other payment data is ever passed.

Check the SDK file before you use it

Every Android SDK file comes with a checksum (.sha256) and a signature (.asc). Check both before you add the file to your app. The signing key is published at https://diimeai.com/docs/sdk-signing-key.asc; its fingerprint is 8471 0E04 E6EC 5C20 F07A 1058 6C3A 631C 09DE 8C34. Compare it with what gpg shows before you trust it.

curl -O https://diimeai.com/docs/sdk-signing-key.asc
gpg --import sdk-signing-key.asc
gpg --fingerprint 84710E04E6EC5C20F07A10586C3A631C09DE8C34
sha256sum -c <the SDK file>.sha256
gpg --verify <the SDK file>.asc <the SDK file>

If either check fails, do not use the file and write to contactus@diimeai.com.

Your SDK key and your subscription

continues for a further 30 days.

sign-ins are refused with the error code SUBSCRIPTION_LAPSED. Your app is not otherwise affected. Pay the invoice or write to contactus@diimeai.com and access returns.

"SUBSCRIPTION_LAPSED" or "TENANT_KEY_INVALID", and null when all is well. A failed enrollment starts its reason with the same code. Use it to alert your own team, for example in your logs or monitoring. There is nothing for your users to do, and the SDK never blocks your app because of it: it stops calling NonaShield, keeps its events queued, and picks up again by itself once the key is accepted.

one. Put the new key in tenantId in your next app release. The old key keeps working until it expires, so there is no gap.


Android (Kotlin)

Requirements: minSdk 23+, compileSdk 34+, Kotlin Gradle plugin 2.1+, Java 17.

Step 1 — Add the SDK file. Copy android-sdk-release.aar into app/libs/.

Step 2 — Add it to Gradle. In app/build.gradle, inside dependencies { }, then click Sync Now:

implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'com.google.android.play:integrity:1.3.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
implementation 'androidx.appcompat:appcompat:1.7.0'

Step 3 — Allow internet. In AndroidManifest.xml, above <application:

<uses-permission android:name="android.permission.INTERNET" />

Step 4 — Start the SDK once, in your Application class onCreate() (register the class with android:name=".MyApplication" on <application> if you do not have one):

import android.util.Log
import com.payshield.sdk.PayShieldConfig
import com.payshield.sdk.PayShieldSDK
import com.payshield.sdk.SdkEnvironment
import com.payshield.sdk.enrollment.EnrollmentCallback
import com.payshield.sdk.enrollment.EnrollmentResult
import com.payshield.sdk.policy.PolicyDecision

PayShieldSDK.initialize(
    context = applicationContext,
    config  = PayShieldConfig(
        backendUrl        = "https://api.diimeai.com",
        tenantId          = "YOUR_SDK_KEY",
        autoBlockSeverity = null,
        environment       = SdkEnvironment.DEVELOPMENT,   // PRODUCTION for your release build
    ),
)

Step 5 — Register the device, right after Step 4:

PayShieldSDK.enroll(callback = object : EnrollmentCallback {
    override fun onResult(result: EnrollmentResult) {
        when (result) {
            is EnrollmentResult.Success -> Log.i("NonaShield", "Enrollment OK")
            is EnrollmentResult.Failure -> Log.e("NonaShield", "Enrollment failed: ${result.reason}")
        }
    }
})

Step 6 — Connect your login. Where your own login succeeds:

PayShieldSDK.onUserLogin(userId = loggedInUserId)

Security findings on the device are reported from the moment enrollment succeeds, before anyone logs in; this call links the device to the user. To have NonaShield also check each login, see Protect any API of your app below and use the action name LOGIN.

Step 7 — (Optional) Check before a payment. Call it from a coroutine, on every payment, just before you move any money. It waits for NonaShield's answer for this payment: a few seconds normally, or up to about a minute when your security team has to review a risky payment. The payment goes ahead only on ALLOW.

val check = try {
    PayShieldSDK.evaluatePaymentCheckpoint()
} catch (e: Exception) {
    null                                   // no answer: treat it as "not approved", never as success
}

when {
    check == null                              -> showRetry()           // could not verify; nothing was approved
    check.decision == PolicyDecision.ALLOW     -> continuePayment()
    check.decision == PolicyDecision.STEP_UP   -> startYourExtraCheck() // your OTP / biometric, then continue
    else                                       -> showBlocked(check.reason)
}

Protect any API of your app (the Critical API section)

NonaShield has no fixed list of critical APIs. You decide which of your APIs to protect, and the action name you pass to the check is that API's name. Every protected API follows the same flow. Call the check from a coroutine, just before you call your own API:

val check = try {
    PayShieldSDK.evaluateApiCheckpoint(action = "KYC")     // before your KYC-submit API
} catch (e: Exception) {
    null                                                   // no answer: not approved
}
// the same for any API: action = "OTP", "ADD_BENEFICIARY", "PAYMENT", or any name of your own
// for login: after your own login succeeds and you have called PayShieldSDK.onUserLogin(userId), and before
// you open the app, call it with the username the user typed:
//     PayShieldSDK.evaluateApiCheckpoint(action = "LOGIN", attemptedUserId = typedUsername)
// enter the app only on ALLOW; on DENY, sign the user out again

Handle check exactly as in Step 7 and the next section. Your API is called only on ALLOW. evaluatePaymentCheckpoint() is the same call with the action PAYMENT, plus the payment-only options below.

The flow is the same for every API. If your account enforces on the device, the SDK blocks a call from a device with a critical issue, on the device. If it does not, a risky call is held for up to 60 seconds for your security team to allow, step up or block, and check carries that decision (a call nobody decides in time is denied). This wait happens only inside evaluateApiCheckpoint and evaluatePaymentCheckpoint, so call them only where you can wait. For an API where you only want a record and never a wait, use evaluateAtCheckpoint(action = "..."): it reports straight away, never makes your call wait, and the call still appears in the dashboard.

Every call appears in one Critical API section of your NonaShield dashboard, with the action you passed in the Action column, the user, the device and what was decided (allowed, step-up, blocked, or held for review). That one section is how you see that your devices, your app and your APIs are all protected. An API whose action you never pass is not checked and does not appear. You do not have to protect every API: protect the ones you choose.

Rules for the action name: one stable name per API (the same name every time), capital letters, digits and _, up to 64 characters. SESSION is used by the SDK itself and is not shown.

What stays specific to payments: the transaction amount bucket and txCommitment (the payment step-up challenge), the new-device cooling period, and the location-consent rule below. All other APIs use the flow above without them.

Handle every outcome

Your app must handle all three decisions, for a payment and for any other API you protect (read "payment" below as "API call"). Nothing about the call (amount, recipient, note, message) is ever sent to NonaShield.

DecisionWhat your app does
ALLOWContinue the payment. Show success when your own payment call succeeds.
STEP_UPDo not continue yet. Run your own extra authentication (OTP, biometric or PIN). Continue only if it succeeds; otherwise stop. The extra check is yours to build; NonaShield only tells you it is needed.
DENYStop. Do not call your payment API. Show a message that fits check.reason (table below).

check.reason for a DENY:

check.reasonMeaningSuggested message
soc_blockedYour security team reviewed the payment and blocked it"This payment was blocked by security review. No money was moved."
soc_review_timeoutThe payment was held for review and was not approved in time"This payment was not approved in time. Please try again." Offer a retry.
verification_unavailableNonaShield could not be reached, so nothing was approved"We could not verify this payment. Check your connection and try again." Offer a retry.
critical_issue_enforcedA critical security issue was found on this device and your account enforces on the device"This payment was blocked because of a security issue on this device." Do not offer a retry.
on_device_persistent_blockThe SDK has blocked this deviceShow your blocked screen. Do not offer a retry.
backend_force_blockYour security team blocked this deviceShow your blocked screen. Do not offer a retry.
API_RISK_BLOCKNonaShield found a critical issue on this device or account and your account enforces on the device"This was blocked for security reasons." Do not offer a retry.
anything elseThe NonaShield decision for this callStop the call and show a general "blocked for security reasons" message.

check.reason for a STEP_UP:

check.reasonMeaning
soc_step_upYour security team reviewed the held payment and asked for extra authentication
consent_requiredThe customer declined location (see below), so a payment that would be approved needs extra authentication
anything elseNonaShield's own risk decision asked for extra authentication

When the customer declines location

NonaShield uses location only to check that a payment is plausible for the customer (for example, not made from two places too far apart to travel between). Your app owns the consent screen and its wording. Tell the SDK the answer:

PayShieldSDK.setLocationConsent(granted = false)   // the customer declined
PayShieldSDK.setLocationConsent(granted = true)    // the customer agrees, or changes their mind

Call it at start-up with the customer's saved choice, and again whenever they change it. The SDK keeps the answer in memory only.

What you tell the SDKWhat happens
falseThe SDK reads no location. A payment that would be approved returns STEP_UP with reason consent_required, so your app runs its own OTP or biometric. A payment that is blocked or held stays so; a refusal never softens a decision.
true, or you never call itUnchanged. The SDK reads a recent location fix only when your own app already holds the location permission. The SDK never asks for location permission itself.

The network address of a request is still seen by NonaShield's servers, as with any internet request.

What your screen must do while it waits. A held payment can take up to about a minute, so:

Your account setting: enforce on the device

One setting on your NonaShield account decides what happens when a payment is risky. NonaShield switches it for you (and your security team sees it on the dashboard).

SettingA serious issue on the device (HIGH or CRITICAL, for example root, hooking, a VPN or a proxy) at payment time
Enforce on device: ONThe SDK blocks on the device whenever a HIGH or CRITICAL threat is present. A threat that can clear (a VPN, a proxy, developer options, screen sharing, open Wi-Fi and similar) blocks only while it lasts; evidence of compromise (root, jailbreak, hooking, tampering) blocks until your security team clears it. A payment returns DENY with critical_issue_enforced.
Enforce on device: OFFNothing blocks on the device. A payment made while a HIGH or CRITICAL threat is present is held and sent to your security team, who choose Allow, Step-up or Block. You receive ALLOW, STEP_UP or DENY (soc_blocked), or DENY (soc_review_timeout) after about a minute with no decision.

The same setting applies at registration and login: with it ON the SDK blocks a device with a critical issue; with it OFF the issue is reported to your security team. Registration is never held. A login or any other API is held for your security team only when you protect it with evaluateApiCheckpoint as shown above; a call you send with evaluateAtCheckpoint is recorded on the dashboard and never waits. (A build started with SdkEnvironment.DEVELOPMENT never blocks at registration or login on its own, so use a PRODUCTION build to see that behaviour. Payments and APIs you protect with evaluateApiCheckpoint are checked the same way in every build.)

Your app handles the outcome the same way in both cases, so you do not need to know which setting is active.

Platform note. Waiting for a security-team review (evaluatePaymentCheckpoint and evaluateApiCheckpoint) and the location consent call are available in the Android SDK. On iOS and React Native the payment check returns at once and does not wait for a review, and there is no evaluateApiCheckpoint yet; handle ALLOW, STEP_UP and DENY there exactly as described above.

Protect sensitive fields and the screen (Android)

Screen. You do nothing. The SDK keeps FLAG_SECURE on every screen of your app, including while the app is in the background, so screenshots, screen recordings, screen sharing and the Recent Apps thumbnail all show a blank screen. Do not clear the flag yourself in onPause.

Sensitive fields. Use SecureEditText instead of EditText for passwords, PINs, OTPs and card numbers:

<com.payshield.sdk.integration.SecureEditText
    android:id="@+id/pin"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="numberPassword" />

It masks the text (so an accessibility service reads it redacted), asks the keyboard not to learn from or suggest from what is typed, turns off autofill, copy, cut and the selection menu, and blocks paste unless you set allowPaste = true. For a field that must stay readable, such as an amount, set masked = false; the other protections stay on. Call wipe() when the user leaves the screen or after submitting.

A keyboard app can ignore the "do not learn" request. The SDK reports an untrusted keyboard separately, and it does not provide its own on-screen keyboard.


iOS (Swift)

Requirements: iOS 14+, Xcode 15+.

Step 1 — Add the SDK files. From the PayShieldSDK-iOS zip NonaShield gives you, drag the three frameworks PayShieldKMP.xcframework, TalsecBridge.xcframework and TalsecRuntime.xcframework into your Xcode project and set each to Embed & Sign under your target's Frameworks, Libraries, and Embedded Content. (TalsecBridge and TalsecRuntime are the open-source freeRASP runtime the SDK uses; their MIT licence is in the zip's THIRD_PARTY_LICENSES folder.)

Step 2 — Enable App Attest. Target → Signing & Capabilities → + Capability → App Attest.

Step 3 — Capture touches. In AppDelegate.swift, create your window with the SDK so it can capture behaviour:

import PayShieldKMP

window = PayShieldSDK.shared.makeRootWindow()

Step 4 — Start the SDK once, in application(_:didFinishLaunchingWithOptions:):

let config = PayShieldConfig(
    backendUrl: "https://api.diimeai.com",
    autoBlockSeverity: nil,
    tenantId: "YOUR_SDK_KEY",
    environment: .development,             // .production for your release build
    backendCertPins: []
)
PayShieldSDK.shared.initialize(config: config)

Step 5 — Register the device (off the main thread — it waits for the network):

DispatchQueue.global(qos: .userInitiated).async {
    if let result = try? PayShieldSDK.shared.enroll() {
        print(result is EnrollmentResult.Success ? "Enrollment OK" : "Enrollment failed")
    }
}

Step 6 — Connect your login. Where your own login succeeds:

try? PayShieldSDK.shared.onUserLogin(userId: loggedInUserId)

Step 7 — (Optional) Check before a payment:

if let result = try? PayShieldSDK.shared.evaluateAtCheckpoint(
        action: "PAYMENT", onDevicePolicy: false, transactionValueTier: .unknown) {
    if result.decision == PolicyDecision.allow { /* continue */ }
    else if result.decision == PolicyDecision.stepUp { /* your extra check */ }
    else { /* stop the payment */ }
}

Protect the screen and sensitive fields (iOS)

Screen. The SDK covers your window with a black view when the app resigns active (so the app-switcher snapshot shows the cover, not a balance or an OTP) and while the screen is being recorded or mirrored. You do nothing. iOS does not allow an app to block or blank a screenshot taken with the side buttons; the SDK reports it after it is taken (SCREENSHOT). It cannot be prevented, and we do not claim it can.

Sensitive fields. Set isSecureTextEntry = true on password, PIN, OTP and card fields. iOS then masks the text and does not allow third-party keyboards in that field.

Third-party keyboards in the rest of the app. iOS lets only your app decide this. In your AppDelegate, refuse keyboard extensions app-wide:

func application(_ application: UIApplication,
                 shouldAllowExtensionPointIdentifier id: UIApplication.ExtensionPointIdentifier) -> Bool {
    return id != .keyboard
}

The SDK cannot do this for you, and iOS gives no API to identify which keyboard is active.


React Native

Requirements: the Android and iOS requirements above.

Step 1 — Install the package NonaShield gives you, then install the iOS pods:

npm install <NonaShield React Native package>
cd ios && pod install

Step 2 — Android: nothing else to add. iOS: enable App Attest (iOS Step 2) and create the window with PayShieldSDK.shared.makeRootWindow() in AppDelegate.swift (iOS Step 3).

Step 3 — Start the SDK once, at app start:

import { PayShieldSdk } from '@payshield/react-native-sdk';

await PayShieldSdk.initialize({
  backendUrl: 'https://api.diimeai.com',
  tenantId: 'YOUR_SDK_KEY',
  autoBlockSeverity: null,
  environment: 'DEVELOPMENT',            // 'PRODUCTION' for your release build
});

Step 4 — Register the device, right after Step 3:

const enrollment = await PayShieldSdk.enroll();
console.log(enrollment.success ? 'Enrollment OK' : `Enrollment failed: ${enrollment.reason}`);

Step 5 — Connect your login. Where your own login succeeds:

PayShieldSdk.onUserLogin(loggedInUserId);

Step 6 — (Optional) Check before a payment:

const check = await PayShieldSdk.evaluateAtCheckpoint('PAYMENT');
if (check.decision === 'ALLOW') { /* continue */ }
else if (check.decision === 'STEP_UP') { /* your extra check */ }
else { /* stop the payment */ }

Flutter and Cordova

Both are thin wrappers over the same native SDK, with the same calls as React Native: start, register the device, connect your login, and the optional check before a payment (plus isBlocked and getBlockDetails).

onUserLogin(...), evaluateAtCheckpoint(...). See flutter-sdk/README.md.

See cordova-sdk/README.md.

On both, autoBlockSeverity / autoBlock is required and has no default, exactly as in the native SDKs. NonaShield will tell you which value to use. These two wrappers are new; run them on a device in your own app before release.


Harden your app build (optional, Android)

A separate Gradle plugin, com.payshield.hardening, hardens your own app's compiled code — it does not touch the SDK. Apply it in your app module, not in a library module that only wraps the SDK.

What it does:

CapabilityWhat it does
Renaming dictionaryGenerates a per-build list of identifier names and wires it into R8 (-obfuscationdictionary) so renamed classes/methods change build to build, not just once
String encryptionEncrypts string literals in your compiled classes (AES-256-GCM, per-build key) and replaces each with a call to a generated decrypt method
Metadata strippingRemoves source-file name, line numbers and local-variable names from your release build; optionally strips annotations you explicitly list

What it does not do: control-flow obfuscation, code that restructures itself at runtime, or renaming native (.so) libraries. These need a commercial obfuscator; we do not bundle one, and say so rather than claim it.

Setup — project-level build.gradle:

plugins {
    id 'com.payshield.hardening' version 'x.y.z' apply false   // version from your NonaShield contact
}

App module's build.gradle:

plugins {
    id 'com.android.application'
    id 'com.payshield.hardening'
}

payshieldHardening {
    // All optional — shown with their defaults.
    enabled = true
    stringEncryption = true
    metadataStripping = true
    minStringLength = 5
    dictionarySeed = System.currentTimeMillis()   // fix this to a stable value for reproducible builds
    dictionarySize = 2000
    stripAnnotationDescriptors = []                // e.g. ["Lcom/yourapp/internal/DebugOnly;"]
    excludedPackagePrefixes = []                   // e.g. ["com/yourapp/generated/"]
}

String encryption and metadata stripping only take effect on a build type with minifyEnabled true — they run against R8's output, after shrinking, so R8 has already decided what your code needs before either transform touches it. Build your release APK/bundle as normal; nothing else changes about your build process.


Check that it works

Run the app on a real phone. You should see Enrollment OK in your log. Log in once, then tell your NonaShield contact — they will confirm your device appears on your NonaShield dashboard.

Security findings on the device (a rogue build, hooking, tampering) are reported as soon as enrollment succeeds, before anyone logs in. They are not held back until onUserLogin; the device is linked to a user at the first login.

Rules

If something goes wrong

What you seeWhat to do
Unresolved reference / Cannot access class (Android)Use the latest SDK file from NonaShield, then Build → Clean Project and Rebuild Project
Module was compiled with an incompatible version of KotlinUpgrade the Kotlin Gradle plugin to 2.1 or newer
NoClassDefFoundError naming okhttp3, androidx.security or com.google.android.playA library from Android Step 2 is missing
Enrollment failedCheck internet access and that YOUR_SDK_KEY is correct; send us the log line
Enrollment failed starting SUBSCRIPTION_LAPSED, TRIAL_EXPIRED or TENANT_KEY_INVALIDAccess has ended or the key is wrong. Pay the open invoice or write to contactus@diimeai.com. PayShieldSDK.getAccessIssue() shows the same code
Enrollment OK but nothing on the dashboardThe user ID in "Connect your login" has a disallowed character

Questions: contactus@diimeai.com