> ## Documentation Index
> Fetch the complete documentation index at: https://developers.circle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How-to: Recover an account

> Restore a user's wallet access after they forget their PIN, using their pre-set security questions.

Restore a user's wallet access when they forget their PIN. The user answers the
security questions they set during sign-up, then sets a new PIN. The answers to
security questions are the user's responsibility to remember.

<Warning>
  Circle has no way to recover an account when both the PIN and the security
  answers are lost. The user is permanently locked out of their account and all
  wallets and assets.
</Warning>

## Prerequisites

Before you begin, ensure that you've:

* Obtained a Circle Developer API key from the
  [Circle Console](https://console.circle.com/).
* Completed the
  [Build a wallet app](/wallets/user-controlled/build-a-wallet-app) tutorial
  with the PIN method, which sets up a user-controlled wallet, stores the user's
  `userId`, and configures the user's security questions.
* Integrated a user-controlled wallet client-side SDK in your app to walk the
  user through the recovery challenge: [Web SDK](/sdks/user-controlled/web-sdk),
  [iOS SDK](/sdks/user-controlled/ios-sdk),
  [Android SDK](/sdks/user-controlled/android-sdk), or
  [React Native SDK](/sdks/user-controlled/react-native-sdk).
* Installed the user-controlled wallet server-side SDK in your backend to create
  the recovery challenge: [Node.js](/sdks/user-controlled-wallets-nodejs-sdk) or
  [Python](/sdks/user-controlled-wallets-python-sdk).

## Steps

<Steps>
  <Step title="Acquire a session token">
    Request a 60-minute session token for the user. The token authorizes the
    recovery challenge later in the flow.

    <CodeGroup>
      ```ts Node.js SDK theme={null}
      import { initiateUserControlledWalletsClient } from "@circle-fin/user-controlled-wallets";

      const client = initiateUserControlledWalletsClient({
        apiKey: process.env.CIRCLE_API_KEY!,
      });

      const response = await client.createUserToken({
        userId: "2f1dcb5e-312a-4b15-8240-abeffc0e3463",
      });

      const userToken: string = response.data!.userToken;
      const encryptionKey: string = response.data!.encryptionKey;
      ```

      ```python Python SDK theme={null}
      from circle.web3 import user_controlled_wallets, utils

      client = utils.init_user_controlled_wallets_client(api_key="<CIRCLE_API_KEY>")
      api_instance = user_controlled_wallets.UsersAndPinsApi(client)

      request = user_controlled_wallets.GenerateUserTokenRequest.from_dict(
          {"userId": "2f1dcb5e-312a-4b15-8240-abeffc0e3463"}
      )
      response = api_instance.get_user_token(request)

      user_token = response.data.user_token
      encryption_key = response.data.encryption_key
      ```
    </CodeGroup>
  </Step>

  <Step title="Initialize the recovery challenge">
    Use the `userToken` to create a recovery challenge. The SDK returns a
    `challengeId` that your client-side SDK uses to walk the user through answering
    their security questions and setting a new PIN.

    <CodeGroup>
      ```ts Node.js SDK theme={null}
      const response = await client.restoreUserPin({
        userToken,
      });

      const challengeId: string = response.data!.challengeId;
      ```

      ```python Python SDK theme={null}
      request = user_controlled_wallets.RestorePinRequest.from_dict({
          "idempotencyKey": str(uuid.uuid4()),
      })
      response = api_instance.create_user_pin_restore_challenge(user_token, request)

      challenge_id = response.data.challenge_id
      ```
    </CodeGroup>

    Include an `idempotencyKey` (a UUID) on the call to safely retry the request
    without creating duplicate challenges. See
    [Idempotent requests](/api-reference/idempotent-requests) for details on
    idempotency key usage.
  </Step>

  <Step title="Have the user answer their security questions">
    Pass the `userToken`, `encryptionKey`, and `challengeId` to your client-side
    SDK. The SDK presents the recovery UI to the user, who:

    1. Answers their pre-set security questions.
    2. Enters and confirms a new PIN.

    The SDK completes the challenge with Circle.
  </Step>

  <Step title="Check the challenge status">
    Confirm the recovery completed. Use webhooks (push) or polling (pull) to detect
    when the challenge reaches a terminal status: `COMPLETED`, `FAILED`, or
    `EXPIRED`. A `COMPLETED` status means the account was successfully recovered and
    the new PIN is active.

    <Tabs>
      <Tab title="Webhook">
        Subscribe to user challenge notifications and listen for the event matching your
        `challengeId`. The notification includes the challenge `status` and `type`
        (`RESTORE_PIN` for account recovery).

        ```json Webhook notification theme={null}
        {
          "subscriptionId": "d4c07d5f-f05f-4fe4-853d-4dd434806dfb",
          "notificationId": "acab8c14-92ae-481a-8335-6eb5271da014",
          "notificationType": "challenges.initialize",
          "notification": {
            "id": "c4d1da72-111e-4d52-bdbf-2e74a2d803d5",
            "userId": "2f1dcb5e-312a-4b15-8240-abeffc0e3463",
            "type": "RESTORE_PIN",
            "status": "COMPLETE",
            "correlationIds": ["54399e5a-1bf6-4921-9559-10c1115678cd"],
            "errorCode": 0,
            "errorMessage": ""
          },
          "timestamp": "2026-01-15T14:33:17.785131449Z",
          "version": 2
        }
        ```

        For webhook setup, see [Webhooks](/api-reference/webhooks).
      </Tab>

      <Tab title="Polling">
        Poll `getUserChallenge` until the challenge reaches a terminal status.

        <CodeGroup>
          ```ts Node.js SDK theme={null}
          async function pollChallenge(challengeId: string, userToken: string) {
            const TERMINAL = new Set(["COMPLETED", "FAILED", "EXPIRED"]);
            for (let attempt = 0; attempt < 30; attempt++) {
              const response = await client.getUserChallenge({ userToken, challengeId });
              const status = response.data!.challenge!.status;
              if (TERMINAL.has(status)) return response.data!.challenge!;
              await new Promise((resolve) => setTimeout(resolve, 2000));
            }
            throw new Error("Challenge polling timed out");
          }
          ```

          ```python Python SDK theme={null}
          import time

          def poll_challenge(challenge_id: str, user_token: str):
              terminal = {"COMPLETED", "FAILED", "EXPIRED"}
              for _ in range(30):
                  response = api_instance.get_user_challenge(user_token, challenge_id)
                  status = response.data.challenge.status
                  if status in terminal:
                      return response.data.challenge
                  time.sleep(2)
              raise TimeoutError("Challenge polling timed out")
          ```
        </CodeGroup>
      </Tab>
    </Tabs>

    For a full list of possible statuses, see
    [Asynchronous States and Statuses](/wallets/asynchronous-states-and-statuses).
  </Step>
</Steps>

## Error handling

Handle these common failure cases when integrating account recovery:

* **Expired session token (error code `155104`):** The `userToken` from Step 1
  expires after 60 minutes. If you get this error, request a new session token
  and retry.
* **Incorrect security answers (error code `155115`):** The user's answers must
  match what they set during sign-up. After three incorrect attempts, recovery
  locks for 30 minutes (`155120`).
* **Security questions not set (error code `155111`):** The user must have set
  security questions during sign-up to use account recovery. If they haven't,
  recovery isn't available.
* **Security questions already used:** Security questions can be set only once
  during sign-up and can't be reset (`155108`). If the user can't remember their
  answers, no recovery path exists.

For a complete error code reference, see
[Wallets error codes](/wallets/error-codes).
