Sumsub — Mobile SDK integration (iOS & Android)
Embed Sumsub KYC into an existing native app via the IdensicMobileSDK framework
(main class SNSMobileSDK), from dependency install to presenting / launching the
verification flow — on iOS (Swift) or Android (Kotlin).
This is the trunk: read the always-on sections below (intake, asking-vs-doing),
detect the platform, then use the decision table to open
only the branch files that match the platform, the project, and the user's answers.
Intake — ask these three questions first, in one message
Before touching any project file, ask all three together:
Launch point. "From which screen should the Sumsub flow open? Please share the
class name or describe the screen." — iOS: a ViewController or SwiftUI view;
Android: an Activity, Fragment, or Composable.
Access token source. "Does your app already have a way to fetch a Sumsub
access token from your backend? (If yes, share the function name, endpoint URL,
or service class. If not, I'll add a placeholder you can fill in when your
backend is ready.)"
Optional modules. "The base SDK covers standard verification flows and is always installed. On top of it, do
you need any of these
add-ons?
- NFC (passport / eMRTD chip reading) — iOS:
MRTDReader; Android: the nfc module – read the chip on
biometric passports
- VideoIdent — live video call with a moderator
- EID — German eID card reading
- Fisherman / Device Intelligence — fraud signals (iOS: optional
module; Android: bundled in the base SDK since 1.43.0 — nothing to add)
Skip them if you're not sure — they can be added later."
Modules are purely additive. In a multi-select, list only the modules —
an empty selection already means "base only"; don't add "base only" as a
co-selectable peer.
Do not ask about: the App Token or the secret key — those are server concerns
that never touch the app.
Asking vs doing — keep approvals meaningful
- Read-only is free — scan / grep / read
Info.plist, AndroidManifest.xml & build files without asking.
- Batch mutations into one approval, not one per line ("I'll add these 3 Info.plist
keys [list] + this call site [diff] — apply?").
- Ask explicitly only for: editing existing app code, project-mutating commands
(
pod install, a Gradle sync), anything irreversible, and decisions you couldn't settle in intake.
Detect the platform (do this before opening any branch file)
Decide whether this is an iOS or Android project — the install steps, the
permission model, and the generated language all differ. Read-only.
# iOS markers
find . -maxdepth 3 \( -name '*.xcodeproj' -o -name '*.xcworkspace' -o -name 'Podfile' -o -name 'Package.swift' \) 2>/dev/null
# Android markers
find . -maxdepth 3 \( -name 'build.gradle' -o -name 'build.gradle.kts' -o -name 'settings.gradle*' -o -name 'AndroidManifest.xml' \) 2>/dev/null
- iOS only (
.xcodeproj / Package.swift / Podfile, no Gradle) → iOS track.
- Android only (
build.gradle[.kts] / AndroidManifest.xml, no Xcode) → Android track.
- Both present (a monorepo, or a React-Native / Flutter shell with native folders) →
report both and ask which platform to integrate. One platform per run. (React
Native / Flutter wrappers themselves are out of scope — this skill targets the native
SDKs; the Step-1 scan bails on a detected wrapper.)
- Neither → ask the user to point at the app's source root.
Confirm the detected platform if there was any ambiguity, then follow the matching track.
How to navigate this skill
After intake, open ONLY the rows that match the detected platform, the project (from
Step 1), and the user's intake answers. Each branch file is self-contained and ends with
a Next pointer. A letter (2A/2B, 6A/6B) marks a branch — pick one; plain numbers run
in sequence.
iOS track
| Step |
Condition |
Read |
| 1. Scan |
always (first, after intake) |
ios/stages/1-scan.md |
| 2A. Install — CocoaPods |
CocoaPods chosen (per Step 1) |
ios/stages/2a-install-cocoapods.md |
| 2B. Install — SPM |
SPM chosen (per Step 1; the default) |
ios/stages/2b-install-spm.md |
| 3. Permissions & capabilities |
always |
ios/stages/3-permissions.md |
| 4. Integration file |
always |
ios/stages/4-integration-code.md |
| 5. Wire into target |
always (right after creating the file) |
ios/stages/5-wire-target.md |
| 6A. Launch — UIKit |
host is a UIKit ViewController |
ios/stages/6a-launch-uikit.md |
| 6B. Launch — SwiftUI |
host is a SwiftUI view |
ios/stages/6b-launch-swiftui.md |
Android track
| Step |
Condition |
Read |
| 1. Scan |
always (first, after intake) |
android/stages/1-scan.md |
| 2. Install — Dependencies |
always |
android/stages/2-install-dependencies.md |
| 3. Integration file |
always (Gradle auto-compiles — no wire-into-target step) |
android/stages/3-integration-code.md |
| 4. Launch |
always |
android/stages/4-launch.md |
Fallback (last resort only). This skill and its branch files are the primary
source — don't reach for external docs by default. Only if you hit a genuine blocker
they don't resolve, consult the Sumsub docs:
iOS ·
Android.
When the tree is walked, return here for Guardrails and Handoff.
Guardrails — what this skill must never do
- Detect the platform first — never emit Swift into an Android project or Kotlin into an iOS one; if both are
present, ask which to integrate.
- Never edit existing app code beyond the single targeted call site the user approved in intake.
- (iOS) Let the SDK present itself — in SwiftUI too. Always launch via
SumsubVerification.start() (
sdk.present() / sdk.present(from:)). Never strong-hold the SDK or its mainVC across the flow, and never
bridge sdk.mainVC into a SwiftUI .fullScreenCover / .sheet. mainVC strong-retains the SDK and the SDK fires
onDidDismiss from that controller's dealloc; holding it yourself delays teardown and causes "opens every other
time" + a crash on re-open. The SDK owns presentation and dismissal — one owner, clean re-open.
- (Android) Never add camera/mic permissions or runtime prompts — the SDK declares and requests them itself; adding
your own can double-prompt or conflict.
- (Android) The token-refresh handler is synchronous —
onTokenExpired() must return the token on a background
thread; bridge the suspend token call with runBlocking, not the iOS async/callback pattern.
- Never run a project-mutating command (
pod install, a Gradle sync/build) without explicit user permission in the
current message.
- Never ask for or reference the App Token or secret key — tokens are minted server-side.
- Never store or reference the App Token or secret key in any app file.
- Never silently overwrite an existing Info.plist / AndroidManifest value — always show the current value and ask
first.
- Never apply theme changes in this skill — styling belongs in
sumsub-theme-msdk.
- Never gate access on in-app callbacks — the authoritative verdict comes from the backend (webhook + applicant
GET), not the SDK callbacks.
Handoff
After all changes are complete, summarise clearly:
- Files created / modified — list each with a one-line description.
- Stub that needs filling in — if the token fetch is a placeholder, say so explicitly and describe what the user
must implement. (On Android it's the single
SumsubTokenProvider.fetchAccessToken suspend function — the launcher
reuses it for mid-session refresh.)
- Build step — iOS: "Run
pod install, then open the .xcworkspace and build" or "Build and run — SPM packages
resolve automatically"; Android: "Sync Gradle, then build & run".
- How to test — explain: get a sandbox access token from your backend (or temporarily hardcode one), trigger
verification from the launch point (iOS:
SumsubVerification.start(…); Android: the ViewModel action whose effect
makes the screen call SumsubLauncher.present(…)), and verify the Sumsub flow appears. (NFC modules need a
physical device — not the iOS simulator / Android emulator.)
- Source of truth — remind the user: the final verification result comes from the backend webhook + applicant read,
not from the SDK callbacks.
- Next steps — point to
sumsub-theme-msdk for styling (iOS & Android).
1---2name: sumsub-integrate-msdk3description: Integrate the Sumsub Mobile SDK (IdensicMobileSDK / SNSMobileSDK) into a native iOS or Android app — even when the user doesn't name the SDK (e.g. "add Sumsub/KYC to my iPhone app", "add KYC to my Android app", "show the verification screen", "launch Sumsub from a view controller / activity"). Detects the platform from the project (iOS Xcode/Swift vs Android Gradle/Kotlin) and follows the matching track. Covers install, permissions, init, token refresh, and presenting / launching the flow. SKIP for web (`sumsub-integrate-websdk`), backend / API-only token signing (`sumsub-api-generic`), theming (`sumsub-theme-msdk`), and React Native / Flutter (not supported — native iOS & Android only).4---56# Sumsub — Mobile SDK integration (iOS & Android)78Embed Sumsub KYC into an existing native app via the **`IdensicMobileSDK`** framework9(main class **`SNSMobileSDK`**), from dependency install to presenting / launching the10verification flow — on **iOS** (Swift) or **Android** (Kotlin).1112This is the **trunk**: read the always-on sections below (intake, asking-vs-doing),13detect the platform, then use the [decision table](#how-to-navigate-this-skill) to open14**only** the branch files that match the platform, the project, and the user's answers.1516## Intake — ask these three questions first, in one message1718Before touching any project file, ask all three together:19201. **Launch point.** "From which screen should the Sumsub flow open? Please share the21 class name or describe the screen." — *iOS:* a ViewController or SwiftUI view;22 *Android:* an Activity, Fragment, or Composable.23242. **Access token source.** "Does your app already have a way to fetch a Sumsub25 access token from your backend? (If yes, share the function name, endpoint URL,26 or service class. If not, I'll add a placeholder you can fill in when your27 backend is ready.)"28293. **Optional modules.** "The base SDK covers standard verification flows and is **always installed**. On top of it, do30 you need any of these31 add-ons?32 - **NFC** (passport / eMRTD chip reading) — *iOS:* `MRTDReader`; *Android:* the `nfc` module – read the chip on33 biometric passports34 - **VideoIdent** — live video call with a moderator35 - **EID** — German eID card reading36 - **Fisherman / Device Intelligence** — fraud signals *(iOS: optional37 module; **Android: bundled in the base SDK since 1.43.0** — nothing to add)*38 Skip them if you're not sure — they can be added later."3940 Modules are **purely additive**. In a multi-select, list **only the modules** —41 an empty selection already means "base only"; don't add "base only" as a42 co-selectable peer.4344Do **not** ask about: the App Token or the secret key — those are server concerns45that never touch the app.4647## Asking vs doing — keep approvals meaningful4849- **Read-only is free** — scan / grep / read `Info.plist`, `AndroidManifest.xml` & build files without asking.50- **Batch mutations into one approval**, not one per line ("I'll add these 3 Info.plist51 keys [list] + this call site [diff] — apply?").52- **Ask explicitly only for:** editing existing app code, project-mutating commands53 (`pod install`, a Gradle sync), anything irreversible, and decisions you couldn't settle in intake.5455## Detect the platform (do this before opening any branch file)5657Decide whether this is an **iOS** or **Android** project — the install steps, the58permission model, and the generated language all differ. Read-only.5960```bash61# iOS markers62find . -maxdepth 3 \( -name '*.xcodeproj' -o -name '*.xcworkspace' -o -name 'Podfile' -o -name 'Package.swift' \) 2>/dev/null63# Android markers64find . -maxdepth 3 \( -name 'build.gradle' -o -name 'build.gradle.kts' -o -name 'settings.gradle*' -o -name 'AndroidManifest.xml' \) 2>/dev/null65```6667- **iOS only** (`.xcodeproj` / `Package.swift` / `Podfile`, no Gradle) → **iOS track**.68- **Android only** (`build.gradle[.kts]` / `AndroidManifest.xml`, no Xcode) → **Android track**.69- **Both present** (a monorepo, or a React-Native / Flutter shell with native folders) →70 report both and **ask which platform to integrate.** One platform per run. (React71 Native / Flutter wrappers themselves are out of scope — this skill targets the native72 SDKs; the Step-1 scan bails on a detected wrapper.)73- **Neither** → ask the user to point at the app's source root.7475Confirm the detected platform if there was any ambiguity, then follow the matching track.7677## How to navigate this skill7879After intake, open ONLY the rows that match the **detected platform**, the project (from80Step 1), and the user's intake answers. Each branch file is self-contained and ends with81a **Next** pointer. A letter (2A/2B, 6A/6B) marks a branch — pick one; plain numbers run82in sequence.8384### iOS track8586| Step | Condition | Read |87|-------------------------------|----------------------------------------|----------------------------------------------------------------------------|88| 1. Scan | always (first, after intake) | [`ios/stages/1-scan.md`](ios/stages/1-scan.md) |89| 2A. Install — CocoaPods | CocoaPods chosen (per Step 1) | [`ios/stages/2a-install-cocoapods.md`](ios/stages/2a-install-cocoapods.md) |90| 2B. Install — SPM | SPM chosen (per Step 1; the default) | [`ios/stages/2b-install-spm.md`](ios/stages/2b-install-spm.md) |91| 3. Permissions & capabilities | always | [`ios/stages/3-permissions.md`](ios/stages/3-permissions.md) |92| 4. Integration file | always | [`ios/stages/4-integration-code.md`](ios/stages/4-integration-code.md) |93| 5. Wire into target | always (right after creating the file) | [`ios/stages/5-wire-target.md`](ios/stages/5-wire-target.md) |94| 6A. Launch — UIKit | host is a UIKit ViewController | [`ios/stages/6a-launch-uikit.md`](ios/stages/6a-launch-uikit.md) |95| 6B. Launch — SwiftUI | host is a SwiftUI view | [`ios/stages/6b-launch-swiftui.md`](ios/stages/6b-launch-swiftui.md) |9697### Android track9899| Step | Condition | Read |100|---------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------|101| 1. Scan | always (first, after intake) | [`android/stages/1-scan.md`](android/stages/1-scan.md) |102| 2. Install — Dependencies | always | [`android/stages/2-install-dependencies.md`](android/stages/2-install-dependencies.md) |103| 3. Integration file | always (Gradle auto-compiles — no wire-into-target step) | [`android/stages/3-integration-code.md`](android/stages/3-integration-code.md) |104| 4. Launch | always | [`android/stages/4-launch.md`](android/stages/4-launch.md) |105106> **Fallback (last resort only).** This skill and its branch files are the primary107> source — don't reach for external docs by default. Only if you hit a genuine blocker108> they don't resolve, consult the Sumsub docs:109> [iOS](https://docs.sumsub.com/docs/get-started-ios) ·110> [Android](https://docs.sumsub.com/docs/get-started-android).111112When the tree is walked, return here for **Guardrails** and **Handoff**.113114## Guardrails — what this skill must never do115116- **Detect the platform first** — never emit Swift into an Android project or Kotlin into an iOS one; if both are117 present, ask which to integrate.118- **Never edit existing app code** beyond the single targeted call site the user approved in intake.119- **(iOS) Let the SDK present itself — in SwiftUI too.** Always launch via `SumsubVerification.start()` (120 `sdk.present()` / `sdk.present(from:)`). **Never** strong-hold the SDK or its `mainVC` across the flow, and **never**121 bridge `sdk.mainVC` into a SwiftUI `.fullScreenCover` / `.sheet`. `mainVC` strong-retains the SDK and the SDK fires122 `onDidDismiss` from that controller's `dealloc`; holding it yourself delays teardown and causes "opens every other123 time" + a crash on re-open. The SDK owns presentation **and** dismissal — one owner, clean re-open.124- **(Android) Never add camera/mic permissions or runtime prompts** — the SDK declares and requests them itself; adding125 your own can double-prompt or conflict.126- **(Android) The token-refresh handler is synchronous** — `onTokenExpired()` must return the token on a background127 thread; bridge the suspend token call with `runBlocking`, not the iOS async/callback pattern.128- **Never run a project-mutating command** (`pod install`, a Gradle sync/build) without explicit user permission in the129 current message.130- **Never ask for or reference the App Token or secret key** — tokens are minted server-side.131- **Never store or reference the App Token or secret key** in any app file.132- **Never silently overwrite** an existing Info.plist / AndroidManifest value — always show the current value and ask133 first.134- **Never apply theme changes** in this skill — styling belongs in `sumsub-theme-msdk`.135- **Never gate access on in-app callbacks** — the authoritative verdict comes from the backend (webhook + applicant136 GET), not the SDK callbacks.137138## Handoff139140After all changes are complete, summarise clearly:1411421. **Files created / modified** — list each with a one-line description.1432. **Stub that needs filling in** — if the token fetch is a placeholder, say so explicitly and describe what the user144 must implement. (On Android it's the single `SumsubTokenProvider.fetchAccessToken` suspend function — the launcher145 reuses it for mid-session refresh.)1463. **Build step** — iOS: "Run `pod install`, then open the `.xcworkspace` and build" or "Build and run — SPM packages147 resolve automatically"; Android: "Sync Gradle, then build & run".1484. **How to test** — explain: get a sandbox access token from your backend (or temporarily hardcode one), trigger149 verification from the launch point (iOS: `SumsubVerification.start(…)`; Android: the ViewModel action whose effect150 makes the screen call `SumsubLauncher.present(…)`), and verify the Sumsub flow appears. (NFC modules need a151 physical device — not the iOS simulator / Android emulator.)1525. **Source of truth** — remind the user: the final verification result comes from the backend webhook + applicant read,153 not from the SDK callbacks.1546. **Next steps** — point to `sumsub-theme-msdk` for styling (iOS & Android).