---
title: "Capture-Only End-to-End Integration"
url: "https://developer.incode.com/sdk-reference/ios-capture-only-integration/"
section: "sdk-reference"
group: "iOS SDK / iOS Getting Started / Capture-Only Mode"
version: "v1.1"
status: "live"
---
# Capture-Only End-to-End Integration

[Deepsight](/features-and-modules/deepsight/) is Incode's liveness technology. During capture, the device collects extra signals: depth data, a short selfie video, and motion data. The back end uses these signals to find spoofing attempts.

This page shows how to use Deepsight with Capture-Only Mode. In Capture-Only Mode, the iOS SDK captures the images and signals but does not send them to Incode; your integration sends them with the Omni API. Use this mode when your app needs to control when and how captures are sent to your own back end. This page gives the SDK configuration, the order of the API calls, and a sample request for each call, for two flows:

- **Basic flow**: An ID Capture module and a Selfie module. The user scans an ID and takes a selfie with Deepsight.
- **Video Selfie flow**: The basic flow plus a Video Selfie module. The user records a video that can hold a second ID scan, a selfie, and voice consent. The back end compares this recording with earlier captures.

You can also [run these flows without Deepsight](#flows-without-deepsight).

***

## Prerequisites

Before you start, ensure you have:

- The iOS SDK set to `.captureOnly`. See [Capture-Only Mode](/sdk-reference/ios-capture-only-sdk/).
- iOS SDK 5.45.0 or later. Earlier versions do not have `DeepsightConfiguration` or the `motion` parameter of `addIdScan(...)`.
- A Dashboard flow (`configurationId`) with:
    - Deepsight turned on in its Settings tab.
    - The Deepsight settings of the Face Capture module configured to match the SDK `modality`.
    - Face Capture and ID Capture modules with Motion set to match the SDK `motion`.
- An [API key](/get-started-with-incode/getting-credentials/) for your environment.

:::warning
Set the Deepsight and Motion options in the SDK to the same values as the flow configuration in Dashboard. The two control different things: Dashboard tells the back end which signals to expect, and the SDK configuration decides which signals the device collects. If they differ, the back end rejects the call with `Invalid capture metadata`. The next section gives the two settings.
:::

The examples use `YOUR_API_URL` and `YOUR_API_KEY`, the same names as the [installation](/sdk-reference/setup-ios/) examples. Replace them with the API URL and the API key of your environment.

:::warning
Make these calls from your back end. The API key gives full access to your environment. Do not put the API key in the app, and do not call the Omni API from the device. The app sends the captures to your back end, and your back end makes the calls on this page.
:::

Every call to `YOUR_API_URL` after `omni/start` needs these headers. An upload to a pre-signed URL does not use them.

| Header | Value |
| --- | --- |
| `x-api-key` | `YOUR_API_KEY`, the API key of your environment. |
| `api-version` | `1.0` |
| `X-Incode-Hardware-Id` | The `token` value that `omni/start` returned. |
| `Content-Type` | `application/json` |

***

## SDK Configuration for Deepsight

Deepsight needs two changes in the SDK code, one for the [ID Capture](/sdk-reference/module-id-scan/) module and one for the [Selfie](/sdk-reference/module-selfie/) module.

### ID Capture

The `motion` parameter of `addIdScan(...)` is available in iOS SDK 5.45.0 and newer. It writes the motion flag into the capture metadata. The default value is `false`.

Set the same value as the Motion option of the ID Capture module in Dashboard. If the two differ, the back end rejects `omni/add/front-id/v2` with `Invalid capture metadata`.

```swift
let flowConfig = IncdOnboardingFlowConfiguration()
flowConfig.addIdScan(
    idType: .id,
    showIdTypeChooser: false,
    motion: true
)
```

### Selfie

The `addSelfieScan(deepsight:)` overload is available in iOS SDK 5.45.0 and newer. It replaces `addSelfieScan(requireDepthData:videoLivenessRecording:)`, which is deprecated. See the [Migration Guide](/release-notes/migration_guide/).

`DeepsightConfiguration` has three independent fields:

| Field | Default | Description |
| --- | --- | --- |
| `enabled` | `true` | Turns Deepsight on or off. When `false`, the capture behaves as a plain single frame and the SDK ignores the modality. |
| `modality` | `.singleFrameWithDepth` | The capture mode. It takes effect only when `enabled` is `true`. |
| `motion` | `false` | Turns motion collection on or off. This field is independent of the other two. |

The three modality values are:

| Modality | Depth Data | Video Recording |
| --- | --- | --- |
| `.singleFrame` | No | No |
| `.singleFrameWithDepth` | Yes | No |
| `.singleFrameWithDepthAndVideo` | Yes | Yes |

```swift
flowConfig.addSelfieScan(
    deepsight: DeepsightConfiguration(
        enabled: true,
        modality: .singleFrameWithDepthAndVideo,
        motion: true
    )
)
```

### Result Fields

Read these fields from the capture results and send them to the API:

| Callback | Result | Field | Send It To |
| --- | --- | --- | --- |
| `onIdFrontCompleted(_:)` | `IdScanResult` | `base64Image`, `metadata` | `omni/add/front-id/v2` |
| `onIdBackCompleted(_:)` | `IdScanResult` | `base64Image`, `metadata` | `omni/add/back-id/v2` |
| `onSelfieScanCompleted(_:)` | `SelfieScanResult` | `frameWithDepth`, `metadata` | `omni/add/face/third-party` |
| `onSelfieScanCompleted(_:)` | `SelfieScanResult` | `presignedVideoFileURL` | `omni/recordings/import` |
| `onVideoSelfieCompleted(_:)` | `VideoSelfieResult` | `videoData` | The URL from `omni/generateVideoSelfieUrl` |
| `onVideoSelfieCompleted(_:)` | `VideoSelfieResult` | `idFront` | `omni/videoselfie/compare-id/v2`, `omni/videoselfie/compare-ocr/v2` |
| `onVideoSelfieCompleted(_:)` | `VideoSelfieResult` | `idBack` | `omni/videoselfie/compare-back-id/v2`, `omni/videoselfie/compare-back-ocr/v2` |
| `onVideoSelfieCompleted(_:)` | `VideoSelfieResult` | `passport` | `omni/videoselfie/compare-id/v2`, `omni/videoselfie/compare-ocr/v2` |

`SelfieScanResult.frameWithDepth` holds the frame with depth data. A device with no TrueDepth camera gives `nil`. In that case, send `SelfieScanResult.selfieBase64`, which the same callback returns and which holds the frame without depth data.

`SelfieScanResult.presignedVideoFileURL` is set in `.captureOnly` only, and only when the modality is `.singleFrameWithDepthAndVideo`.

`VideoSelfieResult` gives each image as a `UIImage` and each recording as `Data`. Encode each image as base64 before you send it.

***

## Basic Flow

Use this flow for Deepsight with an ID Capture module and a Selfie module.

Call the endpoints in this order. A tag shows which capture module a step belongs to. Make a tagged step only when that module is in the flow. A step with no tag always applies.

1. `POST /omni/start`
2. `POST /omni/add/device-fingerprint`
3. **ID Capture, front side**: `POST /omni/add/front-id/v2?onlyFront=false`
:::warning
The `onlyFront` query parameter tells the back end whether a back image follows.

  - To scan both sides, use `addIdScan(scanStep: .both)`, send `onlyFront=false`, and make step 4.
  - To scan the front only, use `addIdScan(scanStep: .front)`, send `onlyFront=true`, and omit step 4.

  `.both` is the default.
:::
4. **ID Capture, back side**: `POST /omni/add/back-id/v2`
5. **ID Capture**: `POST /omni/process/id`
6. **Selfie with Deepsight video**: `POST /omni/recordings/import`
7. **Selfie**: `POST /omni/add/face/third-party?imageType=selfie&recordingId=RECORDING_ID`
8. **Selfie**: `POST /omni/process/face?imageType=selfie`
9. `POST /omni/finish-status`

:::info
Send the device fingerprint with `omni/add/device-fingerprint` directly after `omni/start`, before any capture. The SDK does the same in `.standard` mode: it sends the device fingerprint as part of the session setup, before the first module starts. Send it once for each session.
:::

### Which Steps Apply

Each tagged step needs its module in the flow and its data in the result.

| Steps | Make Them When | Flow Configuration | Result Field |
| --- | --- | --- | --- |
| 3 | The flow scans the front of a document | `addIdScan(...)` | `IdScanResult.base64Image` from `onIdFrontCompleted(_:)` |
| 4 | The flow scans the back of a document | `addIdScan(scanStep: .both)`, the default | `IdScanResult.base64Image` from `onIdBackCompleted(_:)` |
| 5 | The flow scans a document | `addIdScan(...)` | None; this call carries no image |
| 6 | The selfie records a video for Deepsight | `addSelfieScan(deepsight:)` with `.singleFrameWithDepthAndVideo` | `SelfieScanResult.presignedVideoFileURL` |
| 7, 8 | The flow takes a selfie | `addSelfieScan(...)` | `SelfieScanResult.frameWithDepth` |

:::warning
A `nil` field is not always a skipped step. Your configuration decides which calls to make. The result field tells you what to send.

If you did not enable a step, its field is `nil` and you omit its calls. That is the normal case.

If you did enable a step and its field is `nil`, the capture did not complete. Do not treat this as a skipped step. Record the fault and examine it. The session continues without that check, and no error tells you so.

The SDK gives you the reason for Deepsight video. When `presignedVideoFileURL` is `nil` but the face capture completed, read `SelfieScanResult.videoRecordingError`. It reports `insufficientStorageForVideoRecording` or `videoRecordingFailed`.
:::

### Step 1: Start the Session

`omni/start` creates the interview. The response gives the `token` value for the `X-Incode-Hardware-Id` header of every later call.

```bash
curl --location 'YOUR_API_URL/omni/start' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
    "configurationId": "YOUR_FLOW_ID"
}'
```

### Step 2: Add the Device Fingerprint

Read the fingerprint from the SDK with `IncdOnboardingManager.shared.getDeviceFingerprint()`. `DeviceFingerprint` is `Encodable`. Encode it as JSON and send it without changes.

```swift
let fingerprint = IncdOnboardingManager.shared.getDeviceFingerprint()
let fingerprintJson = String(data: try JSONEncoder().encode(fingerprint), encoding: .utf8)
```

```bash
curl --location 'YOUR_API_URL/omni/add/device-fingerprint' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data "$FINGERPRINT_JSON"
```

The body has this shape. The values change with the device and the SDK version, so do not copy them.

```json
{
    "sdkVersion": "5.45.0",
    "hash": "1234-1234-1234",
    "ip": "10.11.11.11",
    "osVersion": "18.1.1",
    "deviceModel": "iPhone16,2",
    "data": "{\"crossApp\":true}",
    "deviceType": "IOS",
    "algorithmName": "identifierForVendor",
    "algorithmVersion": "1.0.0"
}
```

### Step 3: Add the Front of the ID

Send `base64Image` and `metadata` from the `IdScanResult` that `onIdFrontCompleted(_:)` returns.

```bash
curl --location 'YOUR_API_URL/omni/add/front-id/v2?onlyFront=false' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "ID_FRONT_BASE64_IMAGE",
    "metadata": "ID_FRONT_METADATA"
}'
```

### Step 4: Add the Back of the ID

Send `base64Image` and `metadata` from the `IdScanResult` that `onIdBackCompleted(_:)` returns.

```bash
curl --location 'YOUR_API_URL/omni/add/back-id/v2' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "ID_BACK_BASE64_IMAGE",
    "metadata": "ID_BACK_METADATA"
}'
```

### Step 5: Process the ID

```bash
curl --location 'YOUR_API_URL/omni/process/id' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{}'
```

### Step 6: Send the Deepsight Video

`SelfieScanResult.presignedVideoFileURL` is a file URL, not the video itself. Read that file as plain text. Then send the text in the `video` field without a change. The contents are already base64. Do not encode them again.

If `presignedVideoFileURL` is `nil`, there is no recording to send. Omit this step, and send the selfie without a `recordingId`.

```bash
curl --location 'YOUR_API_URL/omni/recordings/import' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "type": "selfie",
    "video": "CONTENTS_OF_PRESIGNED_VIDEO_FILE"
}'
```

The response gives a `recordingId`. Keep this value for the next call.

### Step 7: Send the Selfie Image

Send `frameWithDepth` and `metadata` from the `SelfieScanResult` that `onSelfieScanCompleted(_:)` returns. Add the `recordingId` from the previous step as a query parameter.

```bash
curl --location 'YOUR_API_URL/omni/add/face/third-party?imageType=selfie&recordingId=RECORDING_ID' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "FRAME_WITH_DEPTH",
    "metadata": "SELFIE_METADATA"
}'
```

### Step 8: Process the Selfie Image

This call runs the face checks on the selfie from step 7. The request body is empty.

```bash
curl --location 'YOUR_API_URL/omni/process/face?imageType=selfie' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{}'
```

### Step 9: Finish the Session

This call closes the session. Make it after all the capture and process calls. The request body is empty.

```bash
curl --location 'YOUR_API_URL/omni/finish-status' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{}'
```

***

## Video Selfie Flow

Use this flow for Deepsight with an [ID Capture](/sdk-reference/module-id-scan/) module, a [Selfie](/sdk-reference/module-selfie/) module, and a [Video Selfie](/sdk-reference/module-video-selfie/) module.

Call the endpoints in this order. A tag shows which capture module a step belongs to. Make a tagged step only when that module is in the flow. A step with no tag always applies.

1. `POST /omni/start`
2. `POST /omni/add/device-fingerprint`
3. **ID Capture, front side**: `POST /omni/add/front-id/v2?onlyFront=false`
4. **ID Capture, back side**: `POST /omni/add/back-id/v2`
5. **ID Capture**: `POST /omni/process/id`
6. **Selfie with Deepsight video**: `POST /omni/recordings/import`
7. **Selfie**: `POST /omni/add/face/third-party?imageType=selfie&recordingId=RECORDING_ID`
8. **Selfie**: `POST /omni/process/face?imageType=selfie`
9. **Video Selfie, document front side**: `POST /omni/videoselfie/compare-id/v2`
10. **Video Selfie, document front side**: `POST /omni/videoselfie/compare-ocr/v2`
11. **Video Selfie, document back side**: `POST /omni/videoselfie/compare-back-id/v2`
12. **Video Selfie, document back side**: `POST /omni/videoselfie/compare-back-ocr/v2`
13. **Video Selfie, taking a selfie**: `POST /omni/add/face/third-party?imageType=videoSelfie`
14. **Video Selfie, taking a selfie**: `POST /omni/process/face?imageType=videoSelfie`
15. **Video Selfie, audio recording, speech check**: `POST /omni/add/speech/v2`
16. **Video Selfie, consent, face check**: `POST /omni/videoselfie/voiceconsent/add/face`
17. `GET /omni/generateVideoSelfieUrl`
18. `PUT` the Video Selfie recording to the returned URL
19. `POST /omni/finish-status`

The questions and the consent phrase come from a call you make before the module starts. It is not part of this sequence. See [Get the Questions and the Consent Phrase](#get-the-questions-and-the-consent-phrase).

:::warning
Voice consent for the record needs no API call. Steps 15 and 16 ask the back end to check the consent. They are not what records it. The Video Selfie recording already holds the audio and the video of the user who speaks the phrase, and steps 17 and 18 send that recording.

  If you collect the consent as a record only, omit steps 15 and 16. Omit them even when `voiceConsent(enabled: true)` is set. Make step 15 to check the words the user said. Make step 16 to match the face of the user during the consent.
:::

Steps 1 through 8 are the same as the [Basic Flow](#basic-flow). Step 19 is step 9 in the Basic Flow.

### Which Steps Apply

Each tagged step needs its Video Selfie step to have run and put its data in the result. Steps 17 and 18 are not tagged because Video Selfie always records.

| Steps | Make Them When | Configuration | Default | Result Field |
| --- | --- | --- | --- | --- |
| 9, 10 | Video Selfie scans the front of the document | `idScan(enabled: true)` | on | `idFront` |
| 11, 12 | Video Selfie scans the back of the document | `idScan(enabled: true)` | on | `idBack` |
| 13, 14 | Video Selfie takes a selfie | `selfieScan(enabled: true)` | on | `selfie` |
| 15 | The recording has audio **and** you want the back end to check what the user said | `voiceConsent(enabled: true)` or `randomQuestions(enabled: true)` | off | `audioData` |
| 16 | The consent has a face frame **and** you want the back end to match that face | `voiceConsent(faceRecognition: true)` | off | `voiceConsentSelfie` |

:::info
Make the comparison calls, then send the recording last. This is the order the SDK uses in `.standard` mode: it compares the documents during the session, then sends the recording in a final upload step.
:::

### Get the Questions and the Consent Phrase

Make this call only if you want the back end to check the words the user said.

Make it before the Video Selfie module starts. It returns the question and answer pairs of your API key. Give that text to the SDK with `randomQuestions(...)` and `voiceConsent(...)`, so the user reads the same text that the back end scores in step 15. If the text on screen is not the same text, the check fails for a user who said the phrase correctly.

In `.captureOnly`, the SDK does not make this call. It shows only the text you give it. In `.standard` mode, the SDK makes the call itself at the start of the step and shows what it returns.

```bash
curl --location 'YOUR_API_URL/omni/get/questionAndAnswer?numberOfQuestions=1&returnVoiceConsentQuestion=true' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN'
```

The response gives a list of questions. The entry with `id` `0` is the consent phrase.

### Steps 9 through 12: Compare the Documents

The comparison calls use the ID frames that the Video Selfie module captured, not the frames from steps 3 and 4. `VideoSelfieResult` gives each frame as a `UIImage`. Encode it as base64 before you send it.

| Step | Endpoint | Image |
| --- | --- | --- |
| 9 | `omni/videoselfie/compare-id/v2` | `idFront` |
| 10 | `omni/videoselfie/compare-ocr/v2` | `idFront` |
| 11 | `omni/videoselfie/compare-back-id/v2` | `idBack` |
| 12 | `omni/videoselfie/compare-back-ocr/v2` | `idBack` |

Send each pair of calls only when the matching frame is in the result:

- If `idFront` is set, make steps 9 and 10.
- If `idBack` is set, make steps 11 and 12.
- If a field is `nil`, omit the two steps for that side. If the Video Selfie scanned that side, a `nil` value means the capture failed.

This is the same rule the SDK applies in `.standard` mode. There, the SDK gives the Video Selfie one scan step for each side of the document, then compares each side as it captures it.

:::info
A passport has one side. The Video Selfie module makes a single scan step for it and puts the frame in `VideoSelfieResult.passport`.

Send `passport` to the step 9 and step 10 endpoints, and omit steps 11 and 12. The back end treats a passport the same as the front of a document.
:::

### Step 9: Compare the ID

This call makes sure that the document in the Video Selfie matches the document from step 3.

```bash
curl --location 'YOUR_API_URL/omni/videoselfie/compare-id/v2' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "ID_FRONT_BASE64_IMAGE"
}'
```

Response (`200`):

```json
{
    "typeOfId": "DriversLicense",
    "idTypeMatched": true,
    "issueYear": 2017,
    "faceRecognitionConfidence": 1.0,
    "idSideMatched": true
}
```

Response (`400`):

```json
{
    "timestamp": 1775238442064,
    "status": 4020,
    "error": "Image to compare not found.",
    "message": "BadRequestException: Unable to compare images - template not found",
    "path": "/omni/videoselfie/compare-id/v2"
}
```

| Error Code | Description |
| --- | --- |
| `1003` | Face cropping failure. |
| `4019` | Face not found. |
| `4020` | Image to compare not found. |
| `5003` | Unsatisfied image size. |
| `5004` | Document alignment failed. |
| `5005` | Document not found. |

### Step 10: Compare the OCR Data

This call makes sure that the data on the document in the Video Selfie is the same as the data from step 3.

```bash
curl --location 'YOUR_API_URL/omni/videoselfie/compare-ocr/v2' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "ID_FRONT_BASE64_IMAGE"
}'
```

Response (`200`):

```json
{
    "success": true
}
```

A `400` response has the same shape as step 9. The error codes are `5003`, `5004`, and `5005`.

### Steps 11 and 12: Compare the Back of the Document

These two calls are the back-side counterparts of steps 9 and 10. Send the base64 of `VideoSelfieResult.idBack`.

```bash
curl --location 'YOUR_API_URL/omni/videoselfie/compare-back-id/v2' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "ID_BACK_BASE64_IMAGE"
}'
```

```bash
curl --location 'YOUR_API_URL/omni/videoselfie/compare-back-ocr/v2' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "ID_BACK_BASE64_IMAGE"
}'
```

The responses have the same shape as steps 9 and 10.

### Steps 13 and 14: Send the Video Selfie Image

These two calls are optional. Make them when `selfie` is set in the Video Selfie module.

Steps 13 and 14 use the same two endpoints as steps 7 and 8, but they send a different image. Step 7 sends the image from the Selfie module. Step 13 sends the image that the Video Selfie module captured during the video. The `imageType` query parameter tells the back end which image it receives.

| Steps | Module | Image | imageType |
| --- | --- | --- | --- |
| 7, 8 | Selfie | `SelfieScanResult.frameWithDepth` | `selfie` |
| 13, 14 | Video Selfie | `VideoSelfieResult.selfie` | `videoSelfie` |

```bash
curl --location 'YOUR_API_URL/omni/add/face/third-party?imageType=videoSelfie' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "VIDEO_SELFIE_BASE64_IMAGE"
}'
```

Step 14 processes that frame. The response gives a `confidence` value. Keep it for step 16.

```bash
curl --location 'YOUR_API_URL/omni/process/face?imageType=videoSelfie' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{}'
```

Response (`200`):

```json
{
    "confidence": 1
}
```

### Step 15: Send the Voice Consent Audio

This call is optional. Make it to check the words the user said. It requires `audioData` to be set.

Omit it if you collect the consent as a record only. The recording in steps 17 and 18 holds the audio either way.

Before the module starts, get the questions and the consent phrase and give them to the SDK. See [Get the Questions and the Consent Phrase](#get-the-questions-and-the-consent-phrase).

Step 15 sends `VideoSelfieResult.audioData` as base64 in the `base64Media` field. Speech recognition runs inside this call. There is no process step after it.

```bash
curl --location 'YOUR_API_URL/omni/add/speech/v2' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Media": "AUDIO_BASE64_DATA",
    "audioOnly": true
}'
```

:::warning
The Android SDK uses a different speech endpoint. The iOS SDK calls `omni/add/speech/v2` with a JSON body. The Android SDK calls `omni/add/speech?audioOnly=true` with the audio file as the request body. Use the iOS form on this page. If you write one back end for both platforms, make it accept the shape each platform sends.
:::

### Step 16: Send the Voice Consent Face

This call is optional. Make it to match the face of the user during the consent. It requires `voiceConsentSelfie` to be set.

Omit it if you do not want this second face check. The consent is still recorded.

Send `VideoSelfieResult.voiceConsentSelfie` as base64, with the `confidence` value from step 14. The `threshold` is the bar the score is measured against. The SDK sends the recognition threshold, which you set with `IncdOnboardingManager.shared.recognitionThreshold`. The default is `0.6`.

```bash
curl --location 'YOUR_API_URL/omni/videoselfie/voiceconsent/add/face' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "base64Image": "VOICE_CONSENT_SELFIE_BASE64_IMAGE",
    "confidence": 1,
    "threshold": 0.6
}'
```

### Step 17: Get the Upload URL

This call reserves a pre-signed Amazon S3 URL for the Video Selfie recording. Make it after the comparison and face calls, then send the recording to that URL in step 18.

```bash
curl --location 'YOUR_API_URL/omni/generateVideoSelfieUrl' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN'
```

The response gives the URL for the recording:

```json
{
    "url": "PRESIGNED_UPLOAD_URL"
}
```

### Step 18: Send the Recording

Send `VideoSelfieResult.videoData` to the `url` from step 17.

```bash
curl --location --request PUT 'PRESIGNED_UPLOAD_URL' \
--header 'Content-Type: video/mp4' \
--data-binary '@/path/to/videoSelfie.mp4'
```

To send the recording in parts instead of one request, read [Alternative: Upload the Video Selfie in Parts](#alternative-upload-the-video-selfie-in-parts).

***

## Alternative: Upload the Video Selfie in Parts

The Video Selfie flow sends the recording to a pre-signed S3 URL in one request. Three Omni endpoints send the same recording in parts instead.

Use this route in place of steps 17 and 18 of the [Video Selfie Flow](#video-selfie-flow). Use one route or the other, never both.

Select this route when one of these is true:

- Your security policy does not permit a direct upload to Amazon S3.
- The recording is large, and one request for the whole file is not reliable.

:::info
The SDK does not use this route. In `.standard` mode, the SDK always reserves one URL and sends the recording in a single request. The endpoints below are an API-only route for Capture-Only mode.
:::

### Start the Upload

The response gives an `uploadId`. Every later call needs this value.

```bash
curl --location --request POST 'YOUR_API_URL/omni/video-selfie/upload/initiate' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN'
```

### Send the Parts

The API gateway rejects a request larger than 10 MB. Divide the video into parts of about 7 MB. For example, `split -b 7000000 archive.mp4 chunk_` writes the files `chunk_aa`, `chunk_ab`, and `chunk_ac`. Do not send parts near the 10 MB limit. Each request carries more than the part itself, so parts of 9 MB can still fail. 7 MB is the tested value.

Send each part in its own call. Increase `partNumber` by 1 for each part.

```bash
curl --location 'YOUR_API_URL/omni/video-selfie/upload/part?uploadId=UPLOAD_ID&partNumber=1' \
--header 'Content-Type: application/octet-stream' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data-binary '@/path/to/chunk_aa'
```

Each response gives an `eTag`. Keep each `eTag` with its part number.

### Complete the Upload

This call tells the back end to put the parts together into one recording. Send the `uploadId` from the first call and the `partNumber` and `eTag` of each part.

```bash
curl --location 'YOUR_API_URL/omni/video-selfie/upload/complete' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "uploadId": "abc123",
    "parts": [
        { "partNumber": 1, "eTag": "bcae19" },
        { "partNumber": 2, "eTag": "d4f921" },
        { "partNumber": 3, "eTag": "a1b2c3" }
    ]
}'
```

Follow these rules for the upload in parts:

- Read the `uploadId` from the response of the first call. Do not write it into the code.
- Number the parts in sequence, from 1.
- Send the parts in the same order as the divided files.
- Put every part in the `parts` array. A missing part makes the assembly fail.

***

## Flows Without Deepsight

Every flow on this page uses Deepsight. To run the same flow without it, make these two changes:

1. In the SDK, set `enabled: false` and `motion: false` on `DeepsightConfiguration`, and set `motion: false` on `addIdScan(...)`. Set the same values in the Dashboard flow.
2. Omit Deepsight video. With `enabled: false`, the SDK records no video, so `presignedVideoFileURL` is `nil`. Omit `POST /omni/recordings/import` and omit the `recordingId` query parameter of `omni/add/face/third-party`.

Every other call stays, in the same order.

***

## Additional APIs

These calls are optional. They read data from a session that is complete.

### Get the Session Score

```bash
curl --location 'YOUR_API_URL/omni/get/score' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN'
```

### Get the OCR Data of the ID

```bash
curl --location 'YOUR_API_URL/omni/get/ocr-data/v2' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN'
```

### Get the Session Images

Base64 strings are large. Ask for the images that you need, in one call.

```bash
curl --location --request POST 'YOUR_API_URL/omni/get/images' \
--header 'Content-Type: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN' \
--data '{
    "images": [
        "fullFrameFrontID",
        "fullFrameBackID",
        "selfie",
        "signatureImageID",
        "ghostSignatureImageID",
        "ghostPortraitImageID"
    ]
}'
```

### Get the Deepsight Video

```bash
curl --location 'YOUR_API_URL/omni/generateSessionRecordingDownloadUrl?interviewId=INTERVIEW_ID&type=selfie' \
--header 'Accept: application/json' \
--header 'api-version: 1.0' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'X-Incode-Hardware-Id: SESSION_TOKEN'
```

***

## Clean Up Local Data

Call `IncdOnboardingManager.shared.deleteLocalUserData()` after the flow exits. Call it from the success, error, and cancellation paths. See [Capture-Only Mode](/sdk-reference/ios-capture-only-sdk/).

Send or copy every capture you need before you make this call. On the error and cancellation paths, the uploads can be incomplete, and the call removes the files permanently.

***

For help, see [API Reference](/sdk-reference/ios-api-reference/) or contact Incode support.