Appearance
Usage Guide
Quick Start
swift
import KinegramEmrtdConnector
// Unique transaction ID, usually from your server
let validationId = UUID().uuidString
// Initialize connector
let connector = EmrtdConnector(
serverURL: URL(string: "wss://docval.kurzdigital.com/ws2/validate")!,
validationId: validationId,
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 ?? "")")
}The SDK handles connection, validation, and disconnection automatically.
Connector Configuration
When you create an EmrtdConnector, two values are required for every session: validationId and clientId. They control how a scan is tracked and where its result is delivered, so it is important to understand the difference.
swift
let connector = EmrtdConnector(
serverURL: URL(string: "wss://docval.kurzdigital.com/ws2/validate")!,
validationId: validationId, // identifies this single scan
clientId: "YOUR-CLIENT-ID" // identifies your account/tenant
)validationId
The validationId identifies one individual scan. The DocVal Service uses it to map the validation result back to the user or session that triggered the scan, and it is the key you use to correlate the result you receive on your result server with the original request.
- Recommended: one value per scan. We recommend generating a fresh
validationIdfor each scan so every result can be told apart individually. - Use it to correlate results. When the DocVal Service posts the validation result to your result server, it includes the same
validationId. Store it on your side (e.g. next to the user or session record) so you can match the incoming result to the right request. - Where it comes from. Typically your backend generates the
validationId, hands it to the app for the scan, and remembers it so it can recognise the result later. For simple cases you can also generate it on the device.
Format: any non-empty string. The exact format is up to you — the DocVal Server does not require a specific one. We recommend using a UUID (version 4) as described below, since it is guaranteed to be unique and saves you from having to ensure uniqueness yourself:
swift
// Recommended: generate a UUID per scan
let validationId = UUID().uuidString
// e.g. "cf847daa-a548-4509-bdeb-4edd51aee448"clientId
The clientId is your API access key. It identifies your account (tenant) on the DocVal Service and is used to determine which result server the validation result is sent to. While the validationId changes with every scan, the clientId is a fixed value for your integration.
- Identifies your tenant. All scans belonging to your integration use the same
clientId. It tells the DocVal Service who is calling. - Routes the result. Based on the
clientId, the DocVal Service knows the configured result server endpoint and posts the validation result there.
Format: a non-empty string assigned to you (for example "example_client"). Replace the "YOUR-CLIENT-ID" placeholder in the examples with the value you were given.
Advanced Usage (Manual Connection)
If you need more control, you can use the explicit connection approach:
swift
// Connect first
try await connector.connect()
// Then validate
let result = try await connector.startValidation(accessKey: mrzKey)
// Disconnect when done
await connector.disconnect()Reading Selected Data Groups
By default, a validation reads DG1 (MRZ), DG2 (face image), and the optional data groups the document carries. If you only need some of them, pass a DataGroupSet — reading less is faster, because DG2 alone accounts for most of the chip reading time:
swift
// MRZ and face image only
let result = try await connector.validate(
with: mrzKey,
filesToRead: .minimalKYC
)startValidation accepts the same parameter in the manual connection flow. Besides the predefined selections you can combine data groups freely, e.g. [.dg1, .dg11].
| Selection | Data Groups | Use Case |
|---|---|---|
.minimal | DG1 | MRZ data alone, no biometrics |
.minimalKYC | DG1, DG2 | MRZ plus face image |
.standard | DG1, DG2, DG7, DG11, DG12 | Same as a validation without a selection |
.all | DG1 – DG16 | All sixteen data groups |
How a selection behaves:
- The SOD is always read, and the DocVal Service verifies the data groups it receives — a reduced selection shows up as fewer entries in
data_groups_checkedof the validation result. - Selected data groups the document does not contain are skipped.
- DG1 and DG2 must read successfully while they are part of the selection, otherwise the validation fails.
- The selection never affects the authenticity checks: DG14 (Chip Authentication) and DG15 (Active Authentication) are read whenever the document carries them.
See Relevant Data Groups for what each data group contains.
Using With CAN
For documents that support PACE with a Card Access Number (a 6-digit number printed on the document):
swift
let canKey = CANKey(can: "123456") // 6-digit CAN
let result = try await connector.validate(with: canKey)INFO
CAN authentication only works with documents that support PACE (Password Authenticated Connection Establishment). It cannot be used with BAC (Basic Access Control).
Reading PACE-Enabled Documents
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).
swift
// 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:
- PACE polling is only available on iOS 16 and later
- PACE polling cannot detect standard passports, use it only when you know the document requires it
- A
PACEPollingNotAvailableerror will be thrown if you try to use PACE polling on iOS 15 or earlier
Automatic PACE Selection (by Document Info)
If 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.
swift
// Auto-select PACE polling based on document info
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
let docType = DocumentKind.fromMRZDocumentCode("ID")
let result2 = try await connector.validate(
with: canKey,
documentType: docType,
issuingCountry: "FRA"
)Notes:
- You still need the PACE entitlement in your app when PACE might be used (see Installation).
- For unknown countries or for passports, the SDK defaults to no PACE polling (you can still use the manual flag if needed).
Custom HTTP Headers
If your server requires custom headers (e.g., for authentication), you can provide them in the EmrtdConnector initializer. These headers are sent in the WebSocket handshake and forwarded by the DocVal Service to your result server:
swift
let headers = [
"Authorization": "Bearer your-token",
"X-Custom-Header": "value"
]
let connector = EmrtdConnector(
serverURL: URL(string: "wss://docval.kurzdigital.com/ws2/validate")!,
validationId: UUID().uuidString,
clientId: "YOUR-CLIENT-ID",
httpHeaders: headers
)Log Context
The DocVal Service can add your own fields to every log line of a session, from the WebSocket handshake to the close of the connection. You can use this to correlate the logs of the DocVal Service with your own logs:
swift
let connector = EmrtdConnector(
serverURL: URL(string: "wss://docval.kurzdigital.com/ws2/validate")!,
validationId: UUID().uuidString,
clientId: "YOUR-CLIENT-ID",
baggage: [
"example_id": "1a2b3c4d5e6f",
"example_key": "12345"
]
)The SDK sends the entries as a W3C Baggage header with the WebSocket handshake and encodes the values for you.
The following rules apply:
- A key must be a token of RFC 7230. This allows letters, digits and the characters
!#$%&'*+-.^_`|~. - A value must not be empty and must have 128 characters or less.
- The header has 8192 characters at most, the limit of the specification. An entry that does not fit is dropped.
- An entry that does not follow these rules is dropped. The other entries are sent. Each dropped entry goes to the
monitoringDelegateand to the debug log. - The entries replace a
baggageheader fromhttpHeaders.
The operator of the DocVal Service has to put the keys on the allow-list of the server. The server ignores all other keys. See Log Context from the Baggage Header for the server side.
Fire-and-Forget Mode
If you don't need to receive the validation result back from the server, you can use the receiveResult parameter:
swift
let connector = EmrtdConnector(
serverURL: URL(string: "wss://docval.kurzdigital.com/ws2/validate")!,
validationId: UUID().uuidString,
clientId: "YOUR-CLIENT-ID",
receiveResult: false
)
// The validate call will complete after sending data to the 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.
Diagnostics
Enable diagnostic data collection on the DocVal Service to help troubleshoot eMRTD reading problems:
swift
let connector = EmrtdConnector(
serverURL: URL(string: "wss://docval.kurzdigital.com/ws2/validate")!,
validationId: UUID().uuidString,
clientId: "YOUR-CLIENT-ID",
enableDiagnostics: true
)WARNING
When diagnostics are enabled, the DocVal Service collects additional debugging information that may contain personal data. Make sure to obtain user consent if required.
See Diagnostic Session for more information.
Error Handling
The SDK uses Swift's native error handling. Errors are thrown from the validate call and can be caught by type:
swift
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)")
}Progress Updates
swift
// 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▮▮▮▮▯▯▯"
}Session Close Callback
The SDK provides a callback that fires when the WebSocket session closes, regardless of success or failure. This is the recommended way to handle session completion:
swift
func connector(_ connector: EmrtdConnector, didClose info: CloseInfo) async {
if info.postConfirmed {
// Server confirmed successful post to result server (Close Code 1000)
print("Results posted successfully")
} else {
// Either an error occurred or connection was lost
// Check didFailWithError for error details, or verify with your backend
print("Post not confirmed")
}
}CloseInfo Properties
| Property | Type | Description |
|---|---|---|
postConfirmed | Bool | true if server confirmed successful post (code 1000) |
trigger | CloseTrigger | What caused the session to close |
code | Int? | WebSocket close code (1000 = success) |
reason | String? | Close reason from server |
CloseTrigger Values
| Trigger | Description |
|---|---|
.server | Server sent explicit CLOSE message |
.connectionLost | Connection dropped unexpectedly |
.timeout | Timeout waiting for server CLOSE |
.client | SDK called disconnect() |
Migration from connectorDidSuccessfullyPostToServer
The old connectorDidSuccessfullyPostToServer callback is deprecated. It only fires on success (Code 1000), leaving you without information on failures or connection drops.
Before (deprecated):
swift
func connectorDidSuccessfullyPostToServer(_ connector: EmrtdConnector) async {
print("Posted successfully")
}After (recommended):
swift
func connector(_ connector: EmrtdConnector, didClose info: CloseInfo) async {
if info.postConfirmed {
print("Posted successfully")
}
}Localization
The SDK provides English status messages by default. To localize the NFC dialog messages:
swift
// Configure localization before validation
connector.nfcStatusLocalization = { status in
switch status.step {
case .waitingForPassport:
return NSLocalizedString("nfc.waitingForPassport", comment: "")
case .readingDG1:
return NSLocalizedString("nfc.readingDocumentData", comment: "")
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)Debug Logging
Debug logging is automatically enabled in DEBUG builds. Log messages will appear in the console during development.
