# EkaiDocumentCapture: integration guide

SDK version **1.2.0** · built 2026-10-12T18:06:49Z · iOS only · everything runs on device, no network calls.

This file is written for **coding agents first and humans second**. It is the same file whoever reads it: a person gets the product overview at https://idxstudioz.com/ekai-sdk, an agent reads this. Follow the sections in order. Section 0 is not optional: it lists what only a human can decide.

What the SDK does: scans a driver's licence / ID card (front and back) or a passport, parses the PDF417 (AAMVA) barcode and the MRZ, crops the document and the portrait, runs active liveness on a selfie, and compares the selfie to the ID portrait on device.

---

## 0. Before you integrate: decisions for a human

An agent must **ask** about every row marked *Ask* and must not invent an answer. If the human is unavailable, use the *Safe default* and record the assumption in the final report.

| # | Decision | Why it matters | Safe default |
|---|---|---|---|
| 1 | **Which flows are needed?** Scan only, scan + liveness, or scan + liveness + face match | Each flow adds code, UI and privacy obligations. Face match is the one with legal and accuracy caveats (rows 4, 5) | Scan only. *Ask* before adding liveness or face match |
| 2 | **Minimum iOS version** | The SDK needs iOS 15+. Face match needs iOS 16+ (on iOS 15 `EkaiFaceMatcher` returns `.notEvaluated`). An App Clip that must stay under the size limit needs iOS 16 | Keep the app's current minimum; do not raise it silently. *Ask* if raising it is the only way to get face match |
| 3 | **Is the host an App Clip?** | Size budget: 10 MB (iOS 15 clips) or 15 MB (iOS 16+) for the *whole* clip. The SDK is about 8.4 MB on device (7.0 MB of that is the face model) | Treat as a full app. *Ask* if the target name contains `Clip` |
| 4 | **Face-match legal sign-off** | The bundled face model (SFace) was trained on public research face datasets. Their terms must be checked by counsel before face match ships to production | Do not enable face match in a production build. A development or test build is fine |
| 5 | **Face-match thresholds** | `matchSimilarity` 0.363 is the SFace authors' value; `noMatchSimilarity` 0.25 comes from a tiny internal sample. **Provisional.** Recalibrate on a larger ID-vs-selfie set with lookalike impostors before relying on it | Surface `.uncertain` to the user or to manual review; never auto-approve on `.match` alone |
| 6 | **Liveness expectations** | Active liveness only (a blink and a head turn in random order). It stops photos and simple replays. It does **not** stop video replays or masks and is **not** a certified PAD (presentation attack detection) | Describe it to the product owner as "basic liveness". *Ask* before it is called anti-spoofing in any UI or contract |
| 7 | **Camera permission copy** | `NSCameraUsageDescription` is shown to end users and reviewed by App Review | *Ask* for wording. Placeholder: "We use the camera to scan your ID and take a selfie." |
| 8 | **Where do captured images and parsed data go?** | The SDK never sends anything off device. Your app does. Images contain government ID data; that is regulated personal data | Keep in memory only, do not write to disk, logs, analytics or crash reports. *Ask* before adding any upload |
| 9 | **Commercial terms and licence key** | This build does not enforce a licence check. Commercial terms are agreed with IDX Studios, outside this file | Do not ship to production without confirmation from the human that terms are agreed |
| 10 | **Which countries / documents must work?** | Barcode parsing is AAMVA (US/Canada licences). MRZ covers passports and ICAO IDs. Other layouts may scan but return no parsed `data` | *Ask* for the document list and test each one on a device |

---

## 1. Facts the agent must know

- **Package**: `EkaiDocumentCapture.xcframework`, module `EkaiDocumentCapture`, slices `ios-arm64` and `ios-arm64_x86_64-simulator`. Built Release with library evolution (`.swiftinterface` included), so it loads under newer Xcode versions.
- **Language**: Swift API, also usable from Objective-C for the `@objc` parts. UIKit view controller for scanning; wrap it for SwiftUI (see `reference-demo/DocumentScannerView.swift`).
- **No camera in the simulator.** The scanner and liveness cannot be run there. Build for the simulator to check it compiles; verify behaviour on a physical iPhone. Vision's document and face models also fail in the simulator.
- **No network.** Do not add network permissions on the SDK's behalf. If you see the SDK "needing" the internet, something is wrong.
- **Thread rules**: `EkaiCaptureResult.validity` runs OCR and `EkaiFaceMatcher.match` runs inference: call both off the main thread.
- **Auto mode can call `didCapture` more than once.** Ignore repeats (guard with a flag).
- **Names are spelled as shipped.** The delegate protocol is `EkiaDocumentScanControllerDelegete` (sic). Do not "fix" the spelling in your code or the build breaks.
- **Not yet verified on a physical device** (state this in your report, do not claim otherwise): tracker thresholds and auto-capture feel, liveness turn direction, passport auto-detect on real passports.
- Open passport spreads may be outlined as a whole two-page spread; the SDK keeps only the MRZ page for the returned `image`, `headshotData` and MRZ decode.
- `EkaiDocumentScanOptions.showReviewScreen` exists but is **not wired up**. Do not rely on it; build your own review screen.

---

## 2. Preflight (run these, do not skip)

```bash
# 2.1 The SDK on this machine (agents with a local install):
cat ~/.ekai-sdk/latest.json                     # version, sha256, minimum iOS
# 2.2 Verify the zip you were given is the one that was released:
shasum -a 256 EkaiDocumentCapture-*.zip         # must equal "sha256" in release.json
# 2.3 The host project:
xcodebuild -version                             # Xcode 26+ was used to build; older may fail to load the interface
xcodebuild -list                                # find the app (and App Clip) targets and schemes
grep -rn "IPHONEOS_DEPLOYMENT_TARGET" *.xcodeproj/project.pbxproj | sort -u
grep -rn "EkaiDocumentCapture" . --include=project.pbxproj --include=Package.swift --include=Podfile | head
# last line: if anything matches, the SDK is already integrated: this is an upgrade, read CHANGELOG.md first
```

If the project has no iOS app target, stop and tell the human: this SDK is iOS only.

---

## 3. Install

Pick **one** route. Route A is the easiest for agents because it never edits `project.pbxproj` by hand.

### Route A: local Swift package (recommended)
1. Copy the whole release folder (`EkaiDocumentCapture.xcframework` **and** `Package.swift`) into the project, e.g. `Vendor/EkaiDocumentCapture/`. Commit it, so other machines and CI can build.
2. Xcode: *File → Add Package Dependencies… → Add Local…* → choose that folder → add the `EkaiDocumentCapture` product to the app target (and the App Clip target if there is one).
3. Xcode embeds and signs it automatically.

### Route B: drag the xcframework
Add `EkaiDocumentCapture.xcframework` to the target: *General → Frameworks, Libraries, and Embedded Content → Embed & Sign*. For an App Clip, embed it in the clip target. This is a manual Xcode step; an agent that cannot drive Xcode should use Route A.

Then:
- Add `NSCameraUsageDescription` to the target's Info.plist (copy from decision 7).
- Build for an iOS Simulator destination to confirm `import EkaiDocumentCapture` compiles.

---

## 4. Integrate

### 4.1 Scan a document

```swift
import EkaiDocumentCapture

let options = EkaiDocumentScanOptions()
options.scanType = .Auto            // .Auto | .ID1FrontOnly | .ID1BackOnly | .PassPort
options.captureMode = .Auto         // .Auto | .Manual | .Idle
options.enableFlashCapture = .None  // default; flash adds glare
let vc = EkaiDocumentScanController()
vc.scanOptions = options
vc.delegate = self                  // EkiaDocumentScanControllerDelegete (sic)
vc.callbackQueue = .main
vc.modalPresentationStyle = .fullScreen   // the Cancel button only shows when full-screen
vc.dismissAfterCapture = false      // true = auto-dismiss when presented modally
present(vc, animated: true)
```

Delegate methods: `documentScanController(_:didCapture: EkaiCaptureResult)`, `documentScanController(_:didFinishWithError:)`, optional `scanWasCancelled()`.

`EkaiCaptureResult.capturedDocuments` is `[EkaiDocument]`; helpers `.front`, `.frontFlash`, `.back`. Each `EkaiDocument` has `image`, `headshotData`, `type` (`.id1Front` / `.id1Back` / `.passport` / `.unknown`), `data: EkaiDocumentData?` (parsed AAMVA or MRZ fields) and `captureAnalysis`.

**Standard flow** (as in the reference demo): scan with `.Auto`, then branch on `document.type`:
- `.passport` → done, there is no back.
- `.id1Front` → set `scanOptions.scanType = .ID1BackOnly` on the same controller and scan the back (changing options on a live controller is supported).
- `.id1Back` → ask the user for the front.

`.PassPort` is the manual fallback when Auto does not recognise a passport. Back scans capture after the same barcode payload decodes continuously for 0.7 s.

**Validity** (front-vs-barcode cross-check): build an `EkaiCaptureResult`, set `capturedDocuments = [front, back]` (passport: `[front]`) and read `.validity` **off the main thread**.

**Expiry**: `expirationDate` is literal. An OCI booklet's placeholder expiry reads as 1986. Check `neverExpires` and `mrzExpiryIsPlaceholder` before telling anyone a document is expired. Full rules: `DOCUMENT_PROFILES.md`.

### 4.2 Liveness and selfie (only if decision 1 includes it)

```swift
let session = EkaiLivenessSession()
session.delegate = self   // EkaiLivenessSessionDelegate: state/message updates, didPassWithSelfie(UIImage)
// Front camera, AVCaptureVideoDataOutput, connection.videoOrientation = .portrait, isVideoMirrored = false
session.process(pixelBuffer: buffer, orientation: .up)   // about 10 fps is enough
```
A blink and a head turn, random order. On success the delegate receives the best frontal frame. `yawSignForUserRight` is -1 for unmirrored frames; if the turn directions are reversed on a device, set it to 1. Reference: `reference-demo/SelfieLivenessView.swift`.

### 4.3 Face match (only if decision 1 includes it and decision 4 allows it)

```swift
let matcher = EkaiFaceMatcher()
let result = matcher.match(document: frontDocument, selfie: selfieImage)   // off the main thread
result.decision     // .match / .uncertain / .noMatch / .notEvaluated
result.similarity   // cosine, -1...1
result.score        // 0...1
result.notes        // e.g. "Low-quality ID portrait"
```
Pipeline: Vision face detection and landmarks, 5-point alignment to 112×112, SFace embedding (128-d), cosine similarity. It never returns `.match` when input quality is poor. `.notEvaluated` means it could not decide (iOS 15, no face found); treat it as "needs another route", not as a failure of the person. To use your own licensed model, pass an `EkaiFaceComparing` implementation: `EkaiFaceMatcher(comparator:)`.

### 4.4 Reference code

`reference-demo/` is a working SwiftUI app flow (scan → back → selfie/liveness → result). Copy structure from it rather than designing the flow from scratch.

---

## 5. Definition of done

An agent may report the integration finished only when **every** line is true, and must list any that are not.

- [ ] Section 0 answered by a human, or defaults used and listed in the report.
- [ ] The framework is inside the repo (not referenced from `~/.ekai-sdk` or Downloads) and the project builds from a clean checkout.
- [ ] `import EkaiDocumentCapture` compiles for an iOS Simulator destination, and for `generic/platform=iOS`.
- [ ] `NSCameraUsageDescription` present in every target that scans (app and App Clip).
- [ ] The scan controller is presented full-screen and handles `didCapture` (once), `didFinishWithError` and `scanWasCancelled`.
- [ ] `validity` and `match` are called off the main thread.
- [ ] Nothing writes ID images or parsed fields to disk, logs, analytics or crash reports. `recordDebugFrames` and `debugHUD` are **off** in release builds.
- [ ] No hand-edited SDK code. If the SDK needs a change, report it to IDX Studios instead.
- [ ] If App Clip: thinned size measured with Xcode's App Thinning Size Report and below the limit.
- [ ] The report says plainly that scanning, liveness direction and passport detection were **not** verified on a physical device unless someone actually did.

---

## 6. Do not

- Do not unzip over an existing integration without reading `CHANGELOG.md` ("Changed" lists behaviour changes).
- Do not rename `EkiaDocumentScanControllerDelegete`.
- Do not assume `expirationDate` in the past means expired (section 4.1).
- Do not set `recordDebugFrames = true` outside local debugging: it saves ID images to the Documents directory.
- Do not run the scanner in the simulator and report "it works": the camera is not there.
- Do not auto-approve a person on `.match` alone while thresholds are provisional (decision 5).

## 7. Troubleshooting

| Symptom | Likely cause |
|---|---|
| `No such module 'EkaiDocumentCapture'` | Package/framework not added to *this* target (App Clip needs its own), or Xcode older than 26 cannot read the interface |
| App crashes on launch with `dyld: Library not loaded` | Framework added but not *Embed & Sign* (Route B) |
| Black screen, no camera | Running in the simulator, or `NSCameraUsageDescription` missing |
| No Cancel button | Controller not presented `.fullScreen` |
| `didCapture` fires twice | Auto mode; guard against repeats |
| Face match always `.notEvaluated` | Device below iOS 16, or no face found in the ID portrait or selfie |
| Liveness asks for the wrong turn direction | Flip `yawSignForUserRight` to 1 |
| Expired-looking passport that should never expire | OCI placeholder expiry: use `neverExpires` |

## 8. Third-party and licensing

`THIRD_PARTY_NOTICES.md` and `SFace-LICENSE-Apache-2.0.txt` ship with the SDK. If your app has an acknowledgements screen, include them. Bundled: SFace face model (Apache-2.0, see decision 4), ZXingObjC (Apache-2.0), RNCryptor (MIT).

## 9. Upgrading

1. Read `CHANGELOG.md` for every version between yours and the new one.
2. Replace the folder you committed in step 3 with the new release folder.
3. Re-run section 5.
4. Check that nothing you relied on moved from "Changed" in the changelog (for 1.2.0: flash default, `targetDPI`, distance window, headshot crop).

Support: reply to the email that carried your download link, or write to the address on https://idxstudioz.com.
