iOS SDK
Drop biometric enrolment and recognition into a native iOS app. Ships an embedded UI for face / voice / palm / WinkPin capture, plus a clean async API for the orchestration around it.
This page reflects the integration WinkKey ships in production. Code samples are pulled from winkkey_app/ios/Runner/ and pre-bake the workarounds for SDK 2.1.1's known sharp edges (see Gotchas).
Install
The iOS SDK ships through Swift Package Manager from Wink's Azure DevOps host. There is no CocoaPods spec — SPM is the supported path.
1. Add the package
If you use XcodeGen, declare it in project.yml:
packages:
WinkIdentity:
url: https://dev.azure.com/payfave/wink-payfave-public/_git/wink_mobile_sdk_ios
exactVersion: 2.1.1
targets:
Runner:
dependencies:
- package: WinkIdentity
product: WinkIdentitySDK
Or in Xcode directly: File → Add Package Dependencies…, paste the URL above, pin to 2.1.1, add WinkIdentitySDK to your app target.
2. Add the -ObjC linker flag
The SDK pulls in google_mlkit_face_detection, which uses Objective-C categories. Without this flag the linker strips the category methods and the app crashes on first capture screen with doesNotRecognizeSelector.
OTHER_LDFLAGS = -ObjC -all_load
3. Bundle the merchant signing key
Wink issues you a private PEM when you provision a merchant. Drop it into your app target as wink_private_key.pem (exact name — the SDK looks it up by resource name during configure).
Add it to .gitignore immediately. It is a per-merchant secret, not a per-user one.
Configure
Configure the SDK once at app launch — typically inside your App.init on SwiftUI or application(_:didFinishLaunchingWithOptions:) on UIKit. The SDK keeps a singleton, so a single configure call is enough for the lifetime of the process.
import SwiftUI
import WinkIdentitySDK
@main
struct WinkKeyApp: App {
init() {
configureWinkSDK()
}
var body: some Scene { … }
private func configureWinkSDK() {
let config = WinkConfiguration(
keyId: "23c96f21-…", // from your merchant dashboard
merchantId: "mycompa660",
livenessEnabled: true,
faceConfidenceThreshold: 0.7,
mfaOrder: ["voice", "palm"],
retriesAllowed: 3,
presentExtProfile: false,
enablePalm: true,
enableVoice: true,
environment: "stage", // not "staging" — see Gotcha #1
signingKeyResource: "wink_private_key",
autoPreWarm: true
)
do {
try WinkSDKAdapter.shared.configure(config)
} catch {
print("[Wink] configure failed: \(error)")
}
}
}
Stage is the only environment. Wink grants external developers keys against the stage tier; production hosts are provisioned per-merchant when you go live. Hardcoding "stage" is the right default for any developer build.
Enrol a user
enroll presents the SDK's capture flow modally and walks the user through whichever modalities your merchant config requires. It returns a WinkLoginSnapshot containing the new user's winkTag, access token, and any captured profile fields.
func enroll(
firstName: String?,
lastName: String?,
email: String?
) async throws -> WinkLoginSnapshot {
guard let vc = rootVC() else {
throw WinkSDKServiceError.noPresenter
}
// requiredEnrollment="min" lets Wink pick the modality set per
// merchant config. "face" alone hangs on stage — see Gotcha #2.
let req = WinkEnrollRequest(
requiredEnrollment: "min",
firstName: firstName,
lastName: lastName,
email: email
)
return try await withCheckedThrowingContinuation { c in
WinkSDKAdapter.shared.enroll(request: req, from: vc) { result in
c.resume(returning: snapshot(from: result))
}
}
}
Authenticate
authenticate runs an existing user through whichever factor(s) your merchant requires. The captured face is matched against your enrolled population on the Wink server; on success you get a fresh access token plus the user's identity claims.
func authenticate() async throws -> WinkLoginSnapshot {
guard let vc = rootVC() else {
throw WinkSDKServiceError.noPresenter
}
return try await withCheckedThrowingContinuation { c in
WinkSDKAdapter.shared.authenticate(from: vc) { result in
c.resume(returning: snapshot(from: result))
}
}
}
The returned snapshot carries winkTag, accessToken, name and email if the user has them, and a responseType of "Green" (clean match) or "Yellow" (match but a second factor is required). On Yellow, present the prompt indicated by secondFactorAuthOptions.
Gotchas
Five things WinkKey paid for in integration time. Front-load these and the rest of the SDK is friendly.
Environment is "stage", not "staging"
The public docs say staging; the SDK rejects it. There is no client-side validation — you'll see a generic "User biometric authentication failed" at first capture. Hardcode "stage".
Use requiredEnrollment: "min", not "face"
Despite docs listing "face" as valid, the enrol flow hangs after two network calls on stage when you pass it. "min" means "minimum modality set per merchant config" and is what every working integration uses.
-ObjC -all_load is mandatory
The SDK's transitive dep google_mlkit_face_detection uses Obj-C categories. Without this linker flag the app crashes the first time it hits a capture screen with doesNotRecognizeSelector. Set it on the app target's Build Settings.
Don't host the SDK inside a Flutter app
The SDK ships an internal Flutter engine (App.xcframework, Flutter.xcframework) which collides at build time with a host app's own engine. Native UIKit / SwiftUI works; mixed Flutter does not.
The PEM is required, but failure is silent until first auth
If wink_private_key.pem is missing from the bundle, configure still returns success — and then enrolment / auth fails later with a generic message. Verify the resource is in the bundle right after configure during development.
Reporting bugs: the WinkKey team is the upstream channel for SDK feedback while the public tracker is being stood up. File issues at winkkey/wink-ios-feedback.md with reproduction steps; the Wink mobile team triages weekly.