---
title: "ID Capture"
url: "https://developer.incode.com/sdk-reference/module-id-scan/"
section: "sdk-reference"
group: "iOS SDK / iOS Individual Modules"
version: "v1.1"
status: "live"
---
# ID Capture

The ID Capture module captures the front and back of a government-issued ID, with auto-capture and quality checks, and produces clean images for processing. It is the entry point for most identity-capture flows.

For an overview of this module and how it works, see [ID Capture](/features-and-modules/id-capture/).

How you use this module depends on your integration pattern. When the app defines the steps in code, you add the module to an `IncdOnboardingFlowConfiguration` as shown below; when the flow is defined in Dashboard, you reference it by session token and let the back end drive the steps. See [Integration Approaches](/sdk-reference/ios-flow-configuration/).

**Availability:** All variants.

## Add ID Capture

1. Add the module with `addIdScan()`.
   ```swift
   flowConfig.addIdScan(
       idType: nil,                              // .id or .passport; nil shows the chooser
       scanStep: .both,                          // .front / .back / .both
       idCategory: .primary,                     // .primary or .secondary
       showTutorials: true,
       showRetakeScreenForManualCapture: nil,
       showRetakeScreenForAutoCapture: nil,
       autocaptureUxMode: nil,                   // .countdown or .holdStill(duration:)
       showIDOverlay: nil,
       streamFrames: nil,
       initialOrientation: nil,
       enableAudioStream: nil,
       showIdTypeChooser: nil,
       digitalIdsUpload: false,
       motion: false                             // collect motion data + run local motion-behavior calc
   )
   ```
2. Add [ID OCR/Process](/sdk-reference/module-id-info/) after ID Capture. ID Capture captures the document images, and processing then runs validation and OCR on what was captured. When `scanStep == .both`, processing runs automatically afterward.

## Configuration Options

Configure the module with `addIdScan()`.

| Option                             | Type                 | Description                                                                                                                                              |
| ---------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `idType`                           | `IdType?`            | Sets the type of document: `.id` or `.passport`. `nil` lets the user select.                                                                             |
| `scanStep`                         | `ScanStep`           | Selects which sides are captured: `.front`, `.back`, or `.both`. Default: `.both`.                                                                       |
| `idCategory`                       | `IDCategory`         | `.primary` or `.secondary`.                                                                                                                              |
| `showTutorials`                    | `Bool?`              | Shows the tutorial screen that explains how to scan an ID.                                                                                               |
| `showIdTypeChooser`                | `Bool?`              | Shows or hides the document type selector.                                                                                                               |
| `showRetakeScreenForAutoCapture`   | `Bool?`              | Controls whether the review/retake screen appears after auto-capture.                                                                                    |
| `showRetakeScreenForManualCapture` | `Bool?`              | Controls whether the review/retake screen appears after manual capture.                                                                                  |
| `autocaptureUxMode`                | `AutocaptureUxMode?` | Specifies auto-capture behavior: `.countdown` or `.holdStill(duration:)`.                                                                                |
| `showIDOverlay`                    | `Bool?`              | Shows a visual overlay/frame guide on the camera preview to help the user align the ID within the capture area. Does not impact UI v2. Default: `false`. |
| `streamFrames`                     | `Bool?`              | Turns on frame streaming during capture. Only applies for the Video Conference (`-vc`) variant.                                                          |
| `initialOrientation`               | `IDOrientation?`     | Sets the orientation of the on-screen ID template: `.vertical` or `.horizontal`. Does not impact device rotation.                                        |
| `enableAudioStream`                | `Bool?`              | Enables audio capture alongside the video/image stream during scanning. Only applies if `streamFrames` is `true`.                                        |
| `digitalIdsUpload`                 | `Bool?`              | Allows uploading an existing digital ID (PDF).                                                                                                           |
| `motion`                           | `Bool`               | Enables motion data collection and local motion-behavior calculation for the capture. Default: `false`.                                                  |

ID Capture ships both a v1 (legacy) and a v2 (smart-capture) screen. The one that runs is not controlled by an `addIdScan` parameter; the SDK selects it automatically based on a remote UX experiment and, for the v2 US smart-capture back-scan path, the active flow's Workflow configuration in Dashboard. Neither condition is directly settable by the integrating app.

There is also a non-UI variant for headless capture: `addIdScanNonUI(scanStep:idCategory:streamFrames:enableAudioStream:)`.

The overload taking `showRetakeScreen`, `showAutoCaptureRetakeScreen`, and `enableRotationOnRetakeScreen` is deprecated. Use the renamed parameters shown above for `showRetakeScreen` (`showRetakeScreenForManualCapture`) and `showAutoCaptureRetakeScreen` (`showRetakeScreenForAutoCapture`). `enableRotationOnRetakeScreen` has no replacement; it was removed instead of renamed, and is retained in the deprecated overload only as an unused parameter.

## Result

ID Capture reports through the `IncdOnboardingDelegate` callbacks, with one pair per captured side plus the lifecycle and per-attempt variants:

```swift
func onIdFrontStarted(_ controller: any IDCaptureController)
func onIdFrontCompleted(_ result: IdScanResult)
func onIdFrontAttemptCompleted(_ result: IdScanResult)
func onIdBackStarted(_ controller: any IDCaptureController)
func onIdBackCompleted(_ result: IdScanResult)
func onIdBackAttemptCompleted(_ result: IdScanResult)
```

`onIdFrontCompleted(_:)` and `onIdBackCompleted(_:)` each deliver an `IdScanResult` for that side. Check `error` first: a non-`nil` value indicates a fatal, pre-upload failure (for example, the scan never reached the server) and takes precedence over the other fields. Otherwise, treat `scanStatus` as the authoritative validation outcome for the capture. `failReason` provides supplementary detail and is not derived from `scanStatus`. Both are independently reported from the same upload response.

`IdScanResult` fields:

- `image`: The captured ID or passport image.
- `base64Image`: The Base64-encoded string of the captured image.
- `encryptedBase64Image`: The encrypted Base64-encoded image, when encryption is enabled.
- `croppedFace`: The face photo cropped from the ID; `nil` when no face can be extracted.
- `chosenIdType`: The document type the user chose (`IdType`).
- `classifiedIdType`: The document type returned by the server.
- `idCategory`: Whether the document is a primary or secondary identification document (`IDCategory`: `.primary` or `.secondary`).
- `scanStatus`: The scan outcome; an `IncdIdScanStatus`: `unknown`, `ok`, `errorClassification`, `errorGlare`, `errorSharpness`, `errorReadability`, `errorInCapture`, `errorUnacceptableID`, or `wrongSide`.
- `failReason`: The reason the scan failed; `nil` when the scan succeeds.
- `issueName`: The legal name on the document.
- `issueYear`: The year the document was issued.
- `countryCode`: The country code of the document's issuing country.
- `allAttemptsExhausted`: `true` if all retry attempts are used up. This reflects the current side, front or back, independently.
- `metadata`: The proprietary capture metadata required by the SDK; populated only in [Capture-Only mode](/sdk-reference/ios-capture-only-sdk/).
- `idealCaptureEnvironmentTestResult`: The result of the ideal-capture-environment test.
- `deviceStats`: A `DeviceStats` snapshot of the device state when the result was produced. Its `motionStatus` (`MotionStatus`) reports device motion during capture. `MotionStatus` is a `CustomStringConvertible` enum (not a string-backed/`RawRepresentable` enum) with three cases; its `description`, and the value serialized to the wire, is the uppercase string shown in parentheses:
  - `.unclear` → `"UNCLEAR"`: Motion could not be determined; this is the fallback used when no motion result is available.
  - `.pass` → `"PASS"`: Device motion was within acceptable limits.
  - `.fail` → `"FAIL"`: Excessive device motion was detected.
- `error`: An `IncdIdScanError` when the capture failed; otherwise, `nil`.

Errors also surface through `IncdOnboardingDelegate.onError(_:)`. There is no ID-Capture-specific delegate error callback beyond the per-result `error` field.

### Real-Time Capture Feedback

`IDFrameAnalysisError` gains a new case:

```swift
case preparingCamera  // new
```

`preparingCamera` is reported through `IDCaptureFeedback.error` while the ID camera is getting ready, before the preview is shown. It is not an error: the capture continues normally once the camera is ready, and the SDK's own screens show the existing "Adjusting camera" copy for it, just like `settingUpStreaming`. If you `switch` exhaustively over `IDFrameAnalysisError`, add a handler for this case.

```swift
func idCaptureController(
    _ controller: any IDCaptureController,
    didReceive feedback: IDCaptureFeedback
) {
    if feedback.error == .preparingCamera {
        // The SDK is preparing the camera; no recovery action is required.
    }
}
```

## Custom-UI Capture with IDCaptureController

For headless capture where your app draws its own camera UI, add the module with `addIdScanNonUI(scanStep:idCategory:streamFrames:enableAudioStream:)` and drive capture yourself through an `IDCaptureController`. The SDK hands you the controller in `onIdFrontStarted(_:)` and `onIdBackStarted(_:)`; store it, set its `delegate`, and start it against a `CALayer` that hosts the camera preview.

```swift
func onIdFrontStarted(_ controller: any IDCaptureController) {
    self.idCaptureController = controller
    controller.delegate = self
    controller.start(
        in: cameraContainerView.layer,
        guidelineFrame: nil,   // nil uses the controller's internal default
        idType: nil,           // nil defaults to .id
        idOrientation: nil,    // nil defaults to .horizontal
        idSide: nil            // nil defaults to .front
    )
}
```

`IDCaptureController` is an `ObservableObject`, so its published state can back a SwiftUI view directly.

### State

- `isManualCapture` (`Bool`): Whether capture is currently in manual mode.
- `canUploadIDScan` (`Bool`): Whether a captured scan is ready to upload.
- `remainingUploadAttempts` (`Int`): Upload attempts left for the current side.
- `recommendedGuideline` (`CGRect?`): The controller's recommended guideline frame, if computed.
- `isRunning` (`Bool`): Whether the capture session is active.
- `currentIdType` (`IdType`): The document type currently being captured.
- `currentIdSide` (`IDSide`, get/set): The side being captured, `.front` or `.back`.
- `currentIdOrientation` (`IDOrientation`): The detected orientation, `.vertical` or `.horizontal`.
- `currentFeedback` (`IDCaptureFeedback?`): The latest real-time feedback.
- `currentGuidelineFrame` (`CGRect`): The guideline frame currently in effect.

### Lifecycle

```swift
func start(in layer: CALayer, guidelineFrame: CGRect?, idType: IdType?, idOrientation: IDOrientation?, idSide: IDSide?)
func pauseAnalyzing()
func continueAnalyzing()
func stop()
func cancel()
func finish()
```

- `start(in: guidelineFrame: idType: idOrientation: idSide:)` attaches the camera preview to the layer in aspect-fill mode and begins analyzing frames, stopping when an ideal frame is captured. 
- `pauseAnalyzing()` and `continueAnalyzing()` pause and resume analysis while keeping the camera feed running. 
- `stop()` ends the session and detaches the preview. 
- `cancel()` returns an empty result for the module and proceeds to the next one when possible. 
- `finish()` completes the ID Scan module and advances to the next one if available; call it once you are ready to move on.

### Manual Capture

```swift
func switchToManualCapture()
func switchToAutomaticCapture()
func triggerManualFrameCapture()
```

Call `switchToManualCapture()` to leave auto-capture, then `triggerManualFrameCapture()` to capture the current frame on demand. Call `switchToAutomaticCapture()` to return to auto-capture.

Unlike `switchToManualCapture()`, which takes effect immediately, `switchToAutomaticCapture()` only updates the mode: frame analysis. The auto-capture fallback timers restart when capture next resumes analysis. Pair it with a `continueAnalyzing()` call — calling it on a live capture without a following resume leaves analysis paused with no timer armed.

### Guideline Frame

The guideline frame defines the expected position and size of the ID within the preview and feeds the position, size, and visibility checks. It is expressed in the preview layer's coordinate space, not the camera frame.

```swift
func updateGuidelineFrame(_ frame: CGRect)
```

Push a new frame whenever your overlay moves or resizes, using the same preview space passed to `start(...)`. The controller also reports a recommended frame through `idCaptureControllerDidChangeIdAttributes(...)`; you can apply it or supply your own.

### Digital ID and Barcode

When you accept an existing digital document instead of a live scan, hand the data to the controller:

```swift
func setDigitalID(pdfDocumentData: Data)
func setBarcode(barcodeData: String)
```

### Upload

```swift
func startUpload()                                                  // results via delegate
func upload(onProgressUpdate: ((Float) -> Void)?) async -> IDUploadResult
```

`startUpload()` reports results and progress through the delegate. `upload(onProgressUpdate:)` returns the result directly and reports progress through the optional closure. `IDUploadResult` is `Result<Void, IDUploadError>`.

```swift
Task {
    let result = await controller.upload { progress in
        updateProgressBar(progress)
    }
    switch result {
    case .success:
        handleUploadSuccess()
    case .failure(let error):
        handleUploadError(error)
    }
}
```

### IDCaptureControllerDelegate

```swift
func idCaptureControllerDidStart(_ controller: any IDCaptureController)
func idCaptureControllerDidDetectID(_ controller: any IDCaptureController) async
func idCaptureController(_ controller: any IDCaptureController, didReceive feedback: IDCaptureFeedback)
func idCaptureControllerDidClearFeedback(_ controller: any IDCaptureController)
func idCaptureControllerDidChangeIdAttributes(_ controller: any IDCaptureController, orientation: IDOrientation, idType: IdType, recommendedGuidelineRect: CGRect?)
func idCaptureController(_ controller: any IDCaptureController, willCapturePhoto frame: UIImage, idCrop: UIImage) async
func idCaptureController(_ controller: any IDCaptureController, didCapturePhoto frame: UIImage, idCrop: UIImage) async
func idCaptureControllerDidSwitchToManualCapture(_ controller: any IDCaptureController)
func idCaptureControllerDidSwitchToAutomaticCapture(_ controller: any IDCaptureController)
func idCaptureController(_ controller: any IDCaptureController, captureAttemptInFlight: Bool)
func idCaptureController(_ controller: any IDCaptureController, didBecomeReadyToProceedWithId side: IDSide)
func idCaptureControllerDidCompleteIdScan(_ controller: any IDCaptureController)
func idCaptureController(_ controller: any IDCaptureController, didCaptureDigitalID: Data) async
func idCaptureController(_ controller: any IDCaptureController, didCaptureBarcode: String) async

// Uploading
func idCaptureControllerDidStartUploading(_ controller: any IDCaptureController)
func idCaptureController(_ controller: any IDCaptureController, didUpdateUploadProgress progress: Float)
func idCaptureController(_ controller: any IDCaptureController, didFinishUploading status: IDUploadResult, canRetry: Bool)
```

`idCaptureControllerDidClearFeedback(_:)`, `idCaptureControllerDidSwitchToAutomaticCapture(_:)` and `idCaptureController(_:captureAttemptInFlight:)` have default no-op implementations, so existing conformers do not need to add them. `idCaptureController(_:captureAttemptInFlight:)` reports `true` when a capture attempt is claimed and `false` when it is released, so UI that starts or leaves a capture can follow it. Real-time feedback arrives as an `IDCaptureFeedback`, whose `error` is an `IDFrameAnalysisError`; see [Real-Time Capture Feedback](#real-time-capture-feedback) for the case set and handling.

## Errors

`IncdIdScanError` cases:

- `error`
- `cameraSetupFailed`
- `permissionsDenied`
- `fakePermissionsDenied`
- `skipped`
- `streamAudioPermissionsDenied`