> ## Documentation Index
> Fetch the complete documentation index at: https://docs.compliance.legaltalent.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Zero-retention sessions

> Run the mobile flow in a single request with no applicant data stored on the platform

<Info>
  Available on iOS. Android support is in progress.
</Info>

In a zero-retention session the platform never stores the applicant's images, answers, extracted fields or biometric results. The SDK captures every step on the device, sends **one** request, and the decision plus the only copy of the extracted data come back to your app.

## Enable it

1. Ask support to enable ephemeral sessions for your tenant: `sessions.create` in `data_processing_config.allowed_ephemeral_endpoints`, or the tenant in ephemeral mode.
2. Use a workflow marked `zero_retention_ready`. These workflows only contain `form_fill`, `document_upload` (image identity documents), `selfie` and `terms_acceptance` steps.
3. Create the session with the `X-Data-Retention: ephemeral` header (or `"data_retention": "ephemeral"` in the body):

```bash theme={null}
curl -X POST https://kyc.legaltalent.ai/kyc/sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "X-Data-Retention: ephemeral" \
  -H "Content-Type: application/json" \
  -d '{ "workflow_id": "YOUR_ZERO_RETENTION_WORKFLOW_ID" }'
```

You get a normal `access_token`. Nothing changes in how you present `LegalTalentFlowView`: the SDK detects the mode from the session.

## What changes in the flow

* Every step is captured **locally**; the applicant can go back through the pages until the request is sent.
* Liveness is the passive on-device check on the selfie. The server only runs the 1:1 face match. No capture clips are recorded.
* Images are downscaled (\~1600 px JPEG) and EXIF is stripped before sending.
* After the last step the SDK sends one request and shows a "verifying" page. The server budget is 20 seconds.
* A transient failure (`503` / `504`) is retried once with the same bytes; retries are not charged twice. A `413` is re-encoded smaller and retried.

Not available in this mode: OTP, server-side liveness, corrections, subsessions and file fields.

## Receive the result

```swift theme={null}
LegalTalentFlowView(
    accessToken: token,
    environment: .production,
    showsSingleSubmitOutcome: false,
    onComplete: { _ in dismiss() },
    onSingleSubmitResult: { outcome in
        switch outcome {
        case .decided(let result, let sentImageHashes):
            // result.status, result.reasonCodes, result.extracted,
            // result.faceMatch, result.risk, result.imageSHA256, result.attestation
            backend.store(result, sentImageHashes: sentImageHashes)
        case .decidedAfterTimeout(let sessionID):
            // Decided after the SDK's timeout: read it with GET /kyc/sessions/{id}
            backend.fetchOutcome(sessionID)
        }
    }
)
```

`onSingleSubmitResult` fires right before `onComplete`. Keep what you need immediately: the result exists only in this callback. Afterwards the public session answers a uniform `404`, and `GET /kyc/sessions/{id}` on your backend returns a censored record (hashes, checks, attestation, no personal data).

* `status` is only `approved` or `rejected`. There is no manual review: anything that would have gone to a reviewer comes back `rejected` with reason codes, and your app decides what to do.
* `reason_codes` are stable strings such as `face_match_failed`, `list_match`, `document_invalid`, `document_forensics_suspicious` or `rule:<rule_type>`. Screening results never include list names or matched identities.
* By default the applicant sees a neutral "done" page. Set `showsSingleSubmitOutcome: true` to let the SDK show the verdict; reason codes are rendered as applicant-safe sentences and screening hits are never disclosed.

## Verify integrity

Send the result and `sentImageHashes` to your backend and check that:

1. `sentImageHashes` (SHA-256 of the exact bytes the SDK sent) match `result.imageSHA256`.
2. `result.attestation.signature` verifies against `result.attestation.public_key_pem` (`ECDSA_SHA_256`). The signature can be `null` with an `unsigned_reason` in environments without a signing key.

## Errors

Errors carry a stable `LegalTalentError.singleSubmitCode`:

| Code                                           | HTTP | Meaning                                                                   |
| ---------------------------------------------- | ---- | ------------------------------------------------------------------------- |
| `ZDR_QUOTA_EXCEEDED`                           | 402  | Tenant out of quota. Nothing was processed                                |
| `ZDR_SESSION_NOT_ACTIVE`                       | 409  | Session not active, attempts exhausted, or a submission already in flight |
| `ZDR_NOT_ENABLED`                              | 409  | The session is not zero-retention                                         |
| `ZDR_INVALID_SUBMISSION`                       | 400  | Malformed or incomplete submission                                        |
| `ZDR_MISSING_DOCUMENT_SIDE`                    | 400  | A document side or the selfie required by the workflow is missing         |
| `ZDR_UNSUPPORTED_MIME`                         | 400  | Image is not JPEG or PNG                                                  |
| `ZDR_IMAGE_TOO_LARGE`, `ZDR_PAYLOAD_TOO_LARGE` | 413  | The SDK re-encodes smaller and retries                                    |
| `ZDR_PROCESSING_TIMEOUT`                       | 504  | The SDK retries once                                                      |
| `ZDR_PROCESSING_UNAVAILABLE`                   | 503  | The SDK retries once                                                      |
| `ZDR_DECIDED_AFTER_TIMEOUT`                    | —    | Decided after the SDK gave up; delivered as `.decidedAfterTimeout`        |

The session is finalized as `rejected` after the third failed submission.
