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
| Item | Used in |
|---|---|
SDK key (shown below as YOUR_SDK_KEY) | Step "Start the SDK" on every platform |
Android: android-sdk-release.aar | Android steps |
iOS: PayShieldKMP.xcframework | iOS steps |
| React Native: the NonaShield React Native package | React 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
- The SDK key is specific to your company and lasts 12 months. Keep it private and use it exactly as sent.
- Billing is monthly, by invoice. Your access runs while your invoices are paid. After each payment, access
continues for a further 30 days.
- If a payment is late, the SDK keeps working for a 7-day grace period. After that, new device enrollments and device
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.
- Your app can see why. If access has ended,
PayShieldSDK.getAccessIssue()returns"TRIAL_EXPIRED",
"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.
- Renewing the key: we send you a new key about 60 days before the 12 months end, with a new SDK file if there is
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.
| Decision | What your app does |
|---|---|
ALLOW | Continue the payment. Show success when your own payment call succeeds. |
STEP_UP | Do 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. |
DENY | Stop. Do not call your payment API. Show a message that fits check.reason (table below). |
check.reason for a DENY:
check.reason | Meaning | Suggested message |
|---|---|---|
soc_blocked | Your security team reviewed the payment and blocked it | "This payment was blocked by security review. No money was moved." |
soc_review_timeout | The 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_unavailable | NonaShield 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_enforced | A 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_block | The SDK has blocked this device | Show your blocked screen. Do not offer a retry. |
backend_force_block | Your security team blocked this device | Show your blocked screen. Do not offer a retry. |
API_RISK_BLOCK | NonaShield 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 else | The NonaShield decision for this call | Stop the call and show a general "blocked for security reasons" message. |
check.reason for a STEP_UP:
check.reason | Meaning |
|---|---|
soc_step_up | Your security team reviewed the held payment and asked for extra authentication |
consent_required | The customer declined location (see below), so a payment that would be approved needs extra authentication |
| anything else | NonaShield'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 SDK | What happens |
|---|---|
false | The 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 it | Unchanged. 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:
- Show a "checking this payment" state and disable the pay button so it cannot be tapped twice.
- Call it from a coroutine, never on the main thread, and only one call at a time per payment.
- Do not time it out yourself. The SDK gives up on its own and returns
soc_review_timeout. - If the app is closed during the wait, treat the payment as not made and start again from the pay button.
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).
| Setting | A serious issue on the device (HIGH or CRITICAL, for example root, hooking, a VPN or a proxy) at payment time |
|---|---|
| Enforce on device: ON | The 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: OFF | Nothing 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).
- Flutter: add the
payshield_sdkpackage fromflutter-sdk/and callPayShieldSdk.initialize(...),enroll(),
onUserLogin(...), evaluateAtCheckpoint(...). See flutter-sdk/README.md.
- Cordova:
cordova plugin add ./cordova-sdk, then callPayShieldSdk.initialize(...)afterdeviceready.
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:
| Capability | What it does |
|---|---|
| Renaming dictionary | Generates 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 encryption | Encrypts string literals in your compiled classes (AES-256-GCM, per-build key) and replaces each with a call to a generated decrypt method |
| Metadata stripping | Removes 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
YOUR_SDK_KEYis the key NonaShield sent you, exactly as sent.loggedInUserIdis your own internal user ID: letters, digits and_ - . @only. Not a name, email or phone number.- Start the SDK once only. Do not call any other SDK setup function.
- Keep
autoBlockSeverityatnullunless NonaShield has agreed a different setting with you. The tenant setting on the dashboard decides whether the SDK blocks. - Optional:
deviceLabel(for example a branch or terminal name) andcustomDeviceId(your own asset id, unique per tenant) inPayShieldConfig, 128 characters each. They show next to the device in the NonaShield portal. Leave them out if you do not need them. - Every threat event the SDK records carries a reference id. When a block happens,
BlockDetails.eventIdholds it (it can benulluntil the event is recorded;PayShieldSDK.getBlockDetails()returns it once it is). Quote it to the security team, or look it up from the device withGET /api/v1/device/events/{eventId}, which answers for that device's own events only.
If something goes wrong
| What you see | What 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 Kotlin | Upgrade the Kotlin Gradle plugin to 2.1 or newer |
NoClassDefFoundError naming okhttp3, androidx.security or com.google.android.play | A library from Android Step 2 is missing |
Enrollment failed | Check internet access and that YOUR_SDK_KEY is correct; send us the log line |
Enrollment failed starting SUBSCRIPTION_LAPSED, TRIAL_EXPIRED or TENANT_KEY_INVALID | Access 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 dashboard | The user ID in "Connect your login" has a disallowed character |
Questions: contactus@diimeai.com