> ## 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: Request a signature

> Have a user sign a message or typed data with their user-controlled wallet for offchain authentication and consent.

Request a cryptographic signature from a user's wallet without broadcasting a
transaction. Signatures prove the user controls the wallet and let your app
verify offchain actions like Sign-In With Ethereum (SIWE) authentication,
EIP-712 typed-data approvals (Permit2, DEX orders, marketplace listings), and
arbitrary text messages signed with EIP-191.

Signing is offchain and doesn't cost gas. For onchain transactions, use
[Transfer tokens](/wallets/user-controlled/transfer-tokens) instead.

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

    <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="Create the signature challenge">
    Call the SDK method that matches your signature type. Each returns a
    `challengeId` that the user authorizes in the next step.

    <Tip>
      The signing endpoints accept either `walletId` or `walletAddress` +
      `blockchain` to identify the wallet. Use whichever you have stored for the
      user.
    </Tip>

    <Tabs>
      <Tab title="Personal sign (EIP-191)">
        Sign an arbitrary text message. Useful for SIWE authentication challenges.

        <CodeGroup>
          ```ts Node.js SDK theme={null}
          const challenge = await client.signMessage({
            userToken,
            walletId: "01899cf2-d415-7052-a207-f9862157e546",
            message: "Sign in to MyApp\nNonce: 12345\nIssued: 2026-01-01T00:00:00Z",
            idempotencyKey: crypto.randomUUID(),
          });

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

          ```python Python SDK theme={null}
          signing_api = user_controlled_wallets.SigningApi(client)

          request = user_controlled_wallets.SignMessageRequest.from_dict({
              "idempotencyKey": str(uuid.uuid4()),
              "walletId": "01899cf2-d415-7052-a207-f9862157e546",
              "message": "Sign in to MyApp\nNonce: 12345\nIssued: 2026-01-01T00:00:00Z",
          })
          challenge = signing_api.sign_message(user_token, request)

          challenge_id = challenge.data.challenge_id
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Typed data (EIP-712)">
        Sign structured offchain data. Useful for Permit2 approvals, DEX orders,
        marketplace listings, and other typed-data consent flows.

        <CodeGroup>
          ```ts Node.js SDK theme={null}
          const challenge = await client.signTypedData({
            userToken,
            walletId: "01899cf2-d415-7052-a207-f9862157e546",
            data: JSON.stringify({
              domain: {
                name: "MyApp",
                version: "1",
                chainId: 1,
                verifyingContract: "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
              },
              primaryType: "Order",
              types: {
                EIP712Domain: [
                  { name: "name", type: "string" },
                  { name: "version", type: "string" },
                  { name: "chainId", type: "uint256" },
                  { name: "verifyingContract", type: "address" },
                ],
                Order: [
                  { name: "maker", type: "address" },
                  { name: "amount", type: "uint256" },
                ],
              },
              message: {
                maker: "0x7b777eb80e82f73f118378b15509cb48cd2c2ac3",
                amount: "1000000",
              },
            }),
            idempotencyKey: crypto.randomUUID(),
          });

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

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

          typed_data = json.dumps({
              "domain": {
                  "name": "MyApp",
                  "version": "1",
                  "chainId": 1,
                  "verifyingContract": "0xEb9614D6d001391e22dDbbEA7571e9823A469c1f",
              },
              "primaryType": "Order",
              "types": {
                  "EIP712Domain": [
                      {"name": "name", "type": "string"},
                      {"name": "version", "type": "string"},
                      {"name": "chainId", "type": "uint256"},
                      {"name": "verifyingContract", "type": "address"},
                  ],
                  "Order": [
                      {"name": "maker", "type": "address"},
                      {"name": "amount", "type": "uint256"},
                  ],
              },
              "message": {
                  "maker": "0x7b777eb80e82f73f118378b15509cb48cd2c2ac3",
                  "amount": "1000000",
              },
          })

          request = user_controlled_wallets.SignTypedDataRequest.from_dict({
              "idempotencyKey": str(uuid.uuid4()),
              "walletId": "01899cf2-d415-7052-a207-f9862157e546",
              "data": typed_data,
          })
          challenge = signing_api.sign_typed_data(user_token, request)

          challenge_id = challenge.data.challenge_id
          ```
        </CodeGroup>
      </Tab>

      <Tab title="Transaction object">
        Sign a transaction without broadcasting it. Useful for building offline signing
        flows or batching signed transactions for later submission.

        <CodeGroup>
          ```ts Node.js SDK theme={null}
          const challenge = await client.signTransaction({
            userToken,
            walletId: "01899cf2-d415-7052-a207-f9862157e546",
            rawTransaction: "0x02ef0180843b9aca0084773594008252089...",
            idempotencyKey: crypto.randomUUID(),
          });

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

          ```python Python SDK theme={null}
          request = user_controlled_wallets.SignTransactionRequest.from_dict({
              "idempotencyKey": str(uuid.uuid4()),
              "walletId": "01899cf2-d415-7052-a207-f9862157e546",
              "rawTransaction": "0x02ef0180843b9aca0084773594008252089...",
          })
          challenge = signing_api.sign_transaction(user_token, request)

          challenge_id = challenge.data.challenge_id
          ```
        </CodeGroup>
      </Tab>
    </Tabs>

    Include an `idempotencyKey` (a UUID) on every signing call. Retrying with the
    same key prevents duplicate challenges. See
    [Idempotent requests](/api-reference/idempotent-requests) for details on
    idempotency key usage.
  </Step>

  <Step title="Have the user authorize the signature">
    Pass the `userToken`, `encryptionKey`, and `challengeId` to your client-side
    SDK. The SDK presents the signing details and the appropriate authorization UI:

    * **Social login or email OTP:** Circle displays a confirmation UI showing the
      message or typed data. See
      [Confirmation UIs](/sdks/user-controlled/confirmation-uis) to customize or
      replace it.
    * **PIN:** The user enters their PIN (or uses biometrics) to authorize.

    The SDK completes the challenge with Circle and returns the signed output.
  </Step>

  <Step title="Fetch the completed signature">
    Wait for the challenge to complete, then retrieve the signature. Use webhooks
    (push) or polling (pull) to detect when the challenge reaches a terminal status:
    `COMPLETED`, `FAILED`, or `EXPIRED`.

    <Tabs>
      <Tab title="Webhook">
        Subscribe to user challenge notifications and listen for the event matching your
        `challengeId`. The notification includes the challenge `status`, `type`
        (`SIGN_MESSAGE`, `SIGN_TYPEDDATA`, or `SIGN_TRANSACTION`), and the signed
        output.

        ```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": "SIGN_MESSAGE",
            "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, then read
        the signature from the challenge.

        <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");
          }

          const challenge = await pollChallenge(challengeId, userToken);
          const signature: string = challenge.signature!;
          const signedBy: string = challenge.walletAddress!;
          ```

          ```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")

          challenge = poll_challenge(challenge_id, user_token)
          signature = challenge.signature
          signed_by = challenge.wallet_address
          ```
        </CodeGroup>
      </Tab>
    </Tabs>

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

  <Step title="Verify the signature in your backend">
    Verify the signature against the original message and the expected wallet
    address. For EIP-191 verification, use a library like
    [ethers](https://docs.ethers.org/) or [viem](https://viem.sh/) on your backend:

    ```ts theme={null}
    import { verifyMessage } from "ethers";

    const recovered: string = verifyMessage(originalMessage, signature);
    const matches: boolean =
      recovered.toLowerCase() === expectedWalletAddress.toLowerCase();
    ```

    For EIP-712, use the typed-data variant (`verifyTypedData` in ethers,
    `verifyTypedData` in viem).
  </Step>
</Steps>

## Error handling

Handle these common failure cases when integrating signature requests:

* **Expired session token (error code `155104`):** The `userToken` expires after
  60 minutes. Request a new session token and retry.
* **Invalid typed data:** Malformed EIP-712 structures fail before reaching the
  user. Validate the `domain`, `types`, and `message` against the EIP-712 spec
  before initiating the challenge.
* **User declines or fails to authorize:** If the user cancels or enters an
  incorrect PIN, the signature isn't generated. Surface the cancellation and let
  them retry.
* **Signature verification fails in your backend:** If the recovered address
  doesn't match the wallet, the user likely signed a different message than what
  you're verifying. Make sure the message your backend verifies matches exactly
  what was signed, including whitespace and encoding.

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