iOS SDK for KINEGRAM eMRTD verification using the v2 WebSocket protocol.
┌───────────────┐ Results ┌─────────────────┐
│ DocVal Server │────────────────▶│ Your Server │
└───────────────┘ └─────────────────┘
▲
│ WebSocket v2
▼
┏━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ eMRTD Connector ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━┛
▲
│ NFC
▼
┌──────────────┐
│ │
│ PASSPORT │
│ │
│ ID CARD │
│ │
│ │
│ (eMRTD) │
│ │
└──────────────┘
The eMRTD Connector enables the Document Validation Server (DocVal) to read and verify eMRTD documents through a secure WebSocket connection.
V2 solves the iOS 20-second NFC timeout issue by moving most APDU exchanges to the device. Instead of relaying every APDU through the server (causing latency to accumulate), V2 performs bulk reading locally and uses the server only for security-critical operations.
Upgrading from V1? See the Migration Guide for detailed instructions.
validate(with:)
Add the following to your Package.swift:
dependencies: [
.package(url: "https://github.com/OVD-Kinegram-AG/emrtd-connector-sdk-ios", from: "2.10.2")
]The package includes the KinegramEmrtd.xcframework binary dependency automatically.
Add the following to your Podfile:
pod 'KinegramEmrtdConnector', '~> 2.10.2'Then run pod install.
Note: CocoaPods support is provided for compatibility with existing projects. However, we recommend using Swift Package Manager as CocoaPods is only in maintenance mode since September 2024 (official announcement).
import KinegramEmrtdConnector
// Initialize connector
let connector = EmrtdConnector(
serverURL: URL(string: "wss://server.example.com/ws2/validate")!,
validationId: UUID().uuidString,
clientId: "YOUR-CLIENT-ID"
)
// Validate with MRZ
let mrzKey = MRZKey(
documentNumber: "P1234567",
birthDateyyMMdd: "900101",
expiryDateyyMMdd: "250101"
)
let result = try await connector.validate(with: mrzKey)
if result.isValid {
print("Document holder: \(result.mrzInfo?.primaryIdentifier ?? "")")
}That's it! The SDK handles connection, validation, and disconnection automatically.
If you need more control, you can still use the explicit connection approach:
// Connect first
try await connector.connect()
// Then validate
let result = try await connector.startValidation(accessKey: mrzKey)
// Disconnect when done
await connector.disconnect()For passports that require CAN (Card Access Number):
let canKey = CANKey(can: "123456") // 6-digit CAN
let result = try await connector.validate(with: canKey)Some identity documents require PACE (Password Authenticated Connection Establishment) polling to be detected. This includes French ID cards (FRA ID) and Omani ID cards (OMN ID).
// Enable PACE polling for PACE-enabled documents (requires iOS 16+)
let canKey = CANKey(can: "123456")
let result = try await connector.validate(with: canKey, usePACEPolling: true)Important:
PACEPollingNotAvailable error will be thrown if you try to use PACE polling on iOS 15 or earlierIf you don’t want to decide usePACEPolling yourself, you can provide the document type and issuing country and let the SDK decide. This currently enables PACE polling for known ID cards that require it (e.g., FRA ID, OMN ID) and keeps it disabled for standard passports.
// Auto-select PACE polling based on document info
// .idCard with country FRA enables PACE polling automatically
let canKey = CANKey(can: "123456")
// Option A: Specify document type explicitly
let result = try await connector.validate(
with: canKey,
documentType: .idCard,
issuingCountry: "FRA" // ISO 3166-1 alpha-3
)
// Option B: Derive document type from MRZ document code prefix (e.g., "ID", "I<", "P<", "PM")
let docType = DocumentKind.fromMRZDocumentCode("ID")
let result2 = try await connector.validate(
with: canKey,
documentType: docType,
issuingCountry: "FRA"
)Notes:
If your server requires custom headers (e.g., for authentication), you can optionally provide them. These headers will be forwarded by DocVal to your result server:
let headers = [
"Authorization": "Bearer your-token",
"X-Custom-Header": "value"
]
let connector = EmrtdConnector(
serverURL: URL(string: "wss://server.example.com/ws2/validate")!,
validationId: UUID().uuidString,
clientId: "YOUR-CLIENT-ID",
httpHeaders: headers // Optional parameter
)Note: This is only for specific use cases where your result server requires additional authentication or metadata.
If you don't need to receive the validation result back from the server, you can use the receiveResult parameter:
let connector = EmrtdConnector(
serverURL: URL(string: "wss://server.example.com/ws2/validate")!,
validationId: UUID().uuidString,
clientId: "YOUR-CLIENT-ID",
receiveResult: false
)
// The validate call will complete after sending data to server
try await connector.validate(with: mrzKey)When receiveResult is false, the server won't send back the validation result, reducing latency and bandwidth usage.
Full API documentation is available at: https://ovd-kinegram-ag.github.io/emrtd-connector-sdk-ios/documentation/kinegramemrtdconnector
Check out the Example directory for a complete SwiftUI app demonstrating:
do {
let result = try await connector.validate(with: mrzKey)
} catch EmrtdConnectorError.nfcNotAvailable(let reason) {
print("NFC not available: \(reason)")
} catch EmrtdConnectorError.connectionTimeout {
print("Timeout - hold passport steady")
} catch EmrtdConnectorError.incompleteRead(let missingFiles, let reason) {
print("Missing files: \(missingFiles.joined(separator: ", "))")
print("Reason: \(reason)")
} catch EmrtdReaderError.accessControlFailed {
print("Wrong MRZ/CAN")
} catch {
print("Error: \(error)")
}// Set delegate for progress updates
connector.delegate = self
// Implement delegate method
func connector(_ connector: EmrtdConnector, didUpdateNFCStatus status: NFCProgressStatus) async {
print(status.alertMessage)
// Shows: "Reading Document Data\n▮▮▮▮▯▯▯"
}The SDK provides a delegate callback to confirm when the server has successfully posted results (Close Code 1000):
// This is called when server successfully posts to result server
func connectorDidSuccessfullyPostToServer(_ connector: EmrtdConnector) async {
print("Server successfully posted results")
// You can now proceed knowing the server processed the data
}The SDK provides English status messages by default. To localize the NFC dialog messages for your users:
// Configure localization before validation
connector.nfcStatusLocalization = { status in
// Return your localized message based on the status
switch status.step {
case .waitingForPassport:
return NSLocalizedString("nfc.waitingForPassport", comment: "")
case .readingDG1:
return NSLocalizedString("nfc.readingDocumentData", comment: "")
// ... handle other cases
default:
return status.alertMessage // Fall back to English
}
}
// The NFC dialog will now show your localized messages
let result = try await connector.validate(with: accessKey)This needed entitlement is added automatically by Xcode when enabling the Near Field Communication Tag Reading capability in the target Signing & Capabilities.
After enabling the capability the *.entitlements file needs to contain
the TAG (Application specific tag, including ISO 7816 Tags) and PACE (Needed for PACE polling support (some ID cards)) format:
...
<dict>
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>PACE</string>
<string>TAG</string>
</array>
</dict>
...The app needs to define the list of AIDs it can connect to, in the Info.plist file.
The AID is a way of uniquely identifying an application on a ISO 7816 tag.
eMRTDS use the AIDs A0000002471001 and A0000002472001.
Your Info.plist entry should look like this:
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>A0000002471001</string>
<string>A0000002472001</string>
</array>NFCReaderUsageDescription key: <key>NFCReaderUsageDescription</key>
<string>This app uses NFC to verify passports</string>Debug logging is automatically enabled in DEBUG builds. Log messages will appear in the console during development.