> ## 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: Reset a user's PIN

> Let a user set a new PIN for their user-controlled wallet, requiring them to enter their current PIN.

Let a user change their PIN when they know their current one. For users who have
forgotten their PIN, use the
[Recover an account](/wallets/user-controlled/recover-account) flow instead.

<Warning>
  If a user loses both their PIN and the answers to their security questions,
  they're 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 and stores the
  user's `userId`.
* Integrated a user-controlled wallet client-side SDK in your app to walk the
  user through the PIN reset 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 PIN reset 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 PIN
    reset 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 PIN reset challenge">
    Use the `userToken` to create a PIN reset challenge. The SDK returns a
    `challengeId` that your client-side SDK uses to walk the user through
    authenticating with their current PIN and setting a new one.

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

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

      ```python Python SDK theme={null}
      request = user_controlled_wallets.ChangePinRequest.from_dict({
          "idempotencyKey": str(uuid.uuid4()),
      })
      response = api_instance.update_user_pin_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 reset their PIN">
    Pass the `userToken`, `encryptionKey`, and `challengeId` to your client-side
    SDK. The SDK presents the PIN entry UI to the user, who:

    1. Enters their current PIN to authorize the change.
    2. Enters and confirms a new PIN.

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

  <Step title="Check the challenge status">
    Confirm the PIN reset 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 user's PIN was successfully reset.

    <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`
        (`CHANGE_PIN` for a PIN reset).

        ```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": "CHANGE_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 PIN reset:

* **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 current PIN:** The user must enter their current PIN correctly to
  authorize the reset. After three incorrect attempts, PIN entry locks for 30
  minutes.
* **PIN entry locked:** If the user's PIN is locked, surface the lock duration
  to them and direct them to wait or use
  [Recover an account](/wallets/user-controlled/recover-account) instead.
* **User without PIN:** Reset PIN works only for users created with the PIN
  authentication method. Social login or email OTP users don't have a PIN to
  reset.

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