---
title: Exporting the client state to authenticate the enrolled user
description: Having successfully enrolled a user through IDV Bridge SaaS, it is critical that integrators export the client state to support account recovery and ongoing authentication through the Mobile SDK.
component: recognize
page_id: recognize:idv-bridge:idv-bridge-exporting-client-state
canonical_url: https://docs.pingidentity.com/recognize/idv-bridge/idv-bridge-exporting-client-state.html
llms_txt: https://docs.pingidentity.com/recognize/llms.txt
docs_for_agents: https://developer.pingidentity.com/build-with-ai/docs-for-agents.md
section_ids:
  exporting-the-client-state-rsa-wrapped-aes-key: Exporting the client state (RSA-wrapped AES key)
  overview: Overview
  endpoint: Endpoint
  example-implementation-python: Example implementation (Python)
  example-response: Example response
  common-errors: Common errors
---

# Exporting the client state to authenticate the enrolled user

## Exporting the client state (RSA-wrapped AES key)

After a user successfully completes enrollment, you can export their client state. This produces an encrypted backup that can be restored or transferred securely to another device.

### Overview

The export process uses a hybrid encryption flow combining RSA and AES:

1. Generate a random AES-256 symmetric key (`client_state_key`).

2. Encrypt this key using the PingOne Recognize RSA public key with `RSAES-OAEP-SHA-256`.

3. Send the RSA-encrypted AES key (hex-encoded) in the `Kl-Client-State-Key` header.

4. PingOne Recognize decrypts it internally and uses the AES key to encrypt the client state with AES-GCM-SIV.

5. You receive a binary blob which can be decrypted locally using the same AES key.

|   |                                                                                                                                                                           |
| - | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|   | In sandbox environments, the returned ciphertext might also be compatible with AES-GCM, so standard AES-GCM decryption can be used if AES-GCM-SIV support is unavailable. |

### Endpoint

**Method:** `POST`

**Path:**

```
/v1/users/{customer}/{username}/export-client-state
```

**Required headers:**

| Header                      | Description                                                                                                 |
| --------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `Kl-Key-Id`                 | Registered RSA key alias (for example, `alias/kl-core-production-authentication-service-image-key-sandbox`) |
| `Kl-Key-Algorithm`          | Must be `RSAES-OAEP-SHA-256`                                                                                |
| `Kl-Client-State-Key`       | RSA-encrypted AES key (hex-encoded)                                                                         |
| `Kl-Client-State-Algorithm` | `AES-GCM-SIV`                                                                                               |
| `Kl-Client-State-Type`      | `BACKUP`                                                                                                    |
| `Kl-Api-Key`                | Your PingOne Recognize API key                                                                              |
| `Accept`                    | `application/octet-stream`                                                                                  |

### Example implementation (Python)

The following is a full working example using `requests` and `cryptography`.

|   |                                                                                                                                    |
| - | ---------------------------------------------------------------------------------------------------------------------------------- |
|   | Replace placeholders (`<your-api-key>`, `<your-customer>`, `<your-username>`, `<your-public-key>`) with real configuration values. |

```python
import os
import json
import requests
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.backends import default_backend

# ----------------------------
# CONFIGURATION
# ----------------------------
URL = "https://<your-keyless-endpoint>/v1/users/<customer>/<username>/export-client-state"

HEADERS_TEMPLATE = {
    "Kl-Key-Id": "alias/<your-key-alias>",
    "Kl-Key-Algorithm": "RSAES-OAEP-SHA-256",
    "Kl-Client-State-Algorithm": "AES-GCM-SIV",  # informational
    "Kl-Client-State-Type": "BACKUP",
    "Kl-Api-Key": "<your-api-key>",
    "Accept": "application/octet-stream",
}

PUBLIC_KEY_PEM = '''-----BEGIN PUBLIC KEY-----
<your-public-key>
-----END PUBLIC KEY-----'''

# ----------------------------
# STEP 1. Generate AES-256 symmetric key
# ----------------------------
sym_key = os.urandom(32)
print(f"[+] Generated AES key: {sym_key.hex()}")

# ----------------------------
# STEP 2. Encrypt AES key with RSA public key (OAEP-SHA256)
# ----------------------------
public_key = serialization.load_pem_public_key(
    PUBLIC_KEY_PEM.encode(), backend=default_backend()
)

encrypted_sym_key = public_key.encrypt(
    sym_key,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# ----------------------------
# STEP 3. Send hex-encoded RSA-encrypted AES key
# ----------------------------
headers = HEADERS_TEMPLATE.copy()
headers["Kl-Client-State-Key"] = encrypted_sym_key.hex()

print("[+] Sending request to Export Client State endpoint...")
response = requests.post(URL, headers=headers)
print(f"[+] Response status: {response.status_code}")

if response.status_code != 200:
    print(f"[!] Error {response.status_code}: {response.text}")
    exit(1)

# ----------------------------
# STEP 4. Decrypt returned client state with AES-GCM
# ----------------------------
encrypted_blob = response.content

if len(encrypted_blob) < 12:
    raise ValueError("Invalid response: cannot extract nonce and ciphertext")

nonce, ciphertext = encrypted_blob[:12], encrypted_blob[12:]
aesgcm = AESGCM(sym_key)

try:
    plaintext = aesgcm.decrypt(nonce, ciphertext, None)
    print("\n[+] Decrypted client state:")
    try:
        print(json.dumps(json.loads(plaintext.decode()), indent=2))
    except json.JSONDecodeError:
        print(plaintext.decode(errors='ignore'))
except Exception as e:
    print(f"[!] Decryption failed: {e}")
```

### Example response

**Status:** `200 OK`

**Content-Type:** `application/octet-stream`

Binary blob: `nonce || ciphertext`

After successful decryption, the plaintext contains the user's client state in JSON:

```json
{
  "user_id": "12345678",
  "enrolled_factors": ["face", "device"],
  "created_at": "2025-10-10T14:32:00Z"
}
```

### Common errors

| Code          | Message                              | Explanation                                                                                                 |
| ------------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `422`         | `bytes_invalid_encoding`             | `Kl-Client-State-Key` must be hex-encoded (not Base64).                                                     |
| `409`         | `IMAGE_ENCRYPTION_ERROR`             | The server couldn't decrypt the key. Ensure your RSA public key matches the alias and OAEP-SHA-256 is used. |
| `400` / `401` | `Unauthorized`                       | Invalid or missing API key.                                                                                 |
| —             | `cryptography.exceptions.InvalidTag` | AES key or nonce mismatch. Check byte order and header setup.                                               |
