Skip to main content

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.

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

Prerequisites

Before you begin, ensure that you’ve:
  • Obtained a Circle Developer API key from the Circle Console.
  • Completed the 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, iOS SDK, Android SDK, or React Native SDK.
  • Installed the user-controlled wallet server-side SDK in your backend to create the signature challenge: Node.js or Python.

Steps

1

Acquire a session token

Request a 60-minute session token for the user.
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;
2

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.
The signing endpoints accept either walletId or walletAddress + blockchain to identify the wallet. Use whichever you have stored for the user.
Sign an arbitrary text message. Useful for SIWE authentication challenges.
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;
Include an idempotencyKey (a UUID) on every signing call. Retrying with the same key prevents duplicate challenges. See Idempotent requests for details on idempotency key usage.
3

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

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.
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.
Webhook notification
{
  "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 Webhook Notifications.
For a full list of possible statuses, see Asynchronous States and Statuses.
5

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 or viem on your backend:
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).

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.