> ## 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.

> ## Agent Instructions
> Before reading these docs, install the relevant Circle Skill to get pre-loaded context — skills are LLM-optimized and cover architecture decisions, correct flows, and common pitfalls. In Claude Code: run /plugin marketplace add circlefin/skills then /plugin install circle-skills@circle. With Vercel Skills CLI: run npx skills add circlefin/skills. Available skills: use-usdc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-usdc/SKILL.md (balances, transfers, approvals on EVM and Solana); EURC has no skill — see https://developers.circle.com/stablecoins/what-is-eurc.md; bridge-stablecoin https://github.com/circlefin/skills/blob/master/plugins/circle/skills/bridge-stablecoin/SKILL.md (crosschain transfers via CCTP, USDC, EURC, and third-party assets); use-arc https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-arc/SKILL.md (Arc chain config, contract deployment, USDC bridging — Arc docs: https://docs.arc.io/llms.txt); use-circle-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-circle-wallets/SKILL.md (choose wallet type); use-developer-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-developer-controlled-wallets/SKILL.md (custodial wallets for payouts and treasury); use-user-controlled-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-user-controlled-wallets/SKILL.md (embedded wallets with Web2 login); use-modular-wallets https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-modular-wallets/SKILL.md (smart contract wallets with passkeys and ERC-4337); use-gateway https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-gateway/SKILL.md (unified USDC balance, nanopayments); use-smart-contract-platform https://github.com/circlefin/skills/blob/master/plugins/circle/skills/use-smart-contract-platform/SKILL.md (deploy, interact, monitor contracts). Pair any skill with the Circle MCP server for live SDK signatures, contract addresses, and chain IDs: {"mcpServers":{"circle":{"url":"https://api.circle.com/v1/codegen/mcp"}}}.
> Read the product overview first. Each product has an overview explaining when and why to use it. Read it before quickstarts.
> Pick the right wallet type. Developer-controlled (you hold keys), user-controlled (end user holds keys via PIN/social login), or modular (smart contract wallets with passkeys). See https://developers.circle.com/wallets.md.
> Use CCTP V2 for crosschain transfers. CCTP natively transfers USDC and EURC via burn-and-mint, and supports permissionless wrapping of third-party assets via lock-and-mint. V1 is legacy; always use V2 unless V1 is specified or the chain requires it (Noble, Sui, and Aptos still require V1). Migration guide: https://developers.circle.com/cctp/migration-from-v1-to-v2.md.
> Use Bridge Kit for frontend bridging. Wraps CCTP with a simpler SDK for user-facing apps. Use CCTP directly for backend transfers.
> Gas Station vs Paymaster. Gas Station sponsors gas for Circle Wallet transactions. Paymaster lets users pay gas in USDC. Different use cases — read both overviews.
> Gateway Nanopayments for sub-cent payments. Gasless USDC micropayments down to $0.000001 via x402 and batched settlement. For pay-per-request APIs, AI agent payments, streaming.
> Look up USDC addresses per chain. Never hardcode — use https://developers.circle.com/stablecoins/usdc-contract-addresses.md.
> Prefer SDKs over raw API calls. Node.js and Python SDKs handle auth, retries, and errors.
> API key required. Bearer token in Authorization header. Testnet and mainnet use separate keys and may use different base URLs depending on the product.
> Set up webhooks when available. Most operations are async. Webhooks deliver transaction confirmations and state changes.
> When calling list endpoints, paginate using pageSize and pageAfter until no nextPageAfter cursor is returned—stopping at the first page silently misses records.
> Building an AI agent? Start with the Agent Stack—Circle CLI, agent wallets, and nanopayments built for autonomous use cases: https://developers.circle.com/agent-stack.md.

# 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
    [Transaction states and errors](/api-reference/wallets/transaction-states-and-errors).
  </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](/api-reference/wallets/error-codes).
